Using Peios
Install Peios, sign in, manage software and services, and troubleshoot a running machine. Start with the task you need to do.
Single-page view · as markdown
Operate a Peios machine
Peios / Using Peios / Start here
Use this guide to get a Peios machine running and look after it. You can use the desktop in a browser or the command-line tools on the machine. Start with the task you have now; you do not need to read the kernel or protocol manuals first.
Install and sign in #
- Install Peios. Install on a PC covers the live medium, connecting to the installer and choosing a destination disk. Read the disk-erasure warning before continuing.
- Complete first boot. First-boot setup sets the machine's identity, network and initial administrator account after an installation through the graphical or text installer.
- Open the desktop. Sign in from a browser explains how to reach the machine and check its certificate before entering credentials.
For manual disk preparation and the legacy peios-install script, use
Installing to disk.
That route has different account and first-boot behavior; follow the
instructions for the installer you actually used.
For a local console or SSH session, see Sign in.
Do everyday work #
- Manage the machine: accounts, software, services, networking, storage and configuration.
- Set up your desktop: wallpaper, clock, dock, shortcuts and default applications.
- Troubleshoot a problem: find the relevant state and records before changing anything.
Understand permissions when you need them #
Peios tools act with the authority of the account running them. A desktop app does not grant extra rights simply because it offers a button. Machine-wide settings are commonly restricted to Administrators; the individual task guides say what access is needed.
If an action is denied, start with the task's permissions section. Use Security fundamentals when you need to understand identities, privileges and access rules in more detail.
Find technical documentation #
- Peios technical reference contains technical reference manuals, specifications and implementation reference.
- Developing for Peios covers building and shipping software, the SDK and developer tools.
The event and command references remain available from Using Peios for looking up an event you have seen or a command you are about to run.
Manage the machine
Peios / Using Peios / Start here
Choose the task below. The linked guides explain the controls, required permissions and how to check the result. Use the machine-wide settings only when you intend to affect everyone who uses the machine.
Accounts and desktop settings #
Manage local accounts and groups collects the account tasks and access precautions in one place.
- Create an account or change local membership:
Principals Manager
on the desktop, or Creating accounts
and the
lpsreference in a terminal. - Sign in remotely from a terminal or manage SSH keys: Signing in over SSH.
- Change your own password:
The
passwdcommand. - Change your own desktop: Desktop Settings.
- Set defaults for everyone or change how the desktop is served: Settings for all users. Changing the listen address can disconnect open desktops; an overlay prevents normal sign-in while it is set.
Software #
- Find, install or remove a package: Package Manager on the desktop, or Installing and removing packages in a terminal. Review the plan, including dependencies, before applying it.
- Update or undo a package change: Keeping a system current.
- Set up or enable a feature delivered by a package: Feature Manager. Installing a package puts files in place; feature setup is a separate action. Remove a feature before its package, while the scripts needed to undo its setup are still available.
- Handle an interrupted package operation: Transactions and recovery. Feature operations have their own interrupted-state guidance.
Services and running programs #
- Inspect, start, stop or restart a service: Controlling services.
- See running programs, resource use and jobs: Task Manager.
- Read a service's output: Event Viewer or Service output and logging.
- Shut down or reboot: Shutdown.
Network, time and storage #
- Inspect connections or change network settings:
Network Manager, or
The
netcommand to inspect state and Configuring profiles to change configuration. Read the confirmation and rollback behavior before changing the network through a remote desktop. - Allow or block a network service: Check and change network policy safely.
- Set the time zone or clock: Time zone and setting the clock.
- Find software, configuration or service data on disk: Where files live.
- Inspect disks and filesystems: Disk Manager is read-only. For changes, follow Managing mounts, Partitioning or Formatting with security descriptors. Partitioning and formatting can destroy data; follow the warnings in those guides before making changes.
Configuration beyond the settings apps #
Start with Find where a setting lives to locate its documentation and the component that reads it.
Use Registry Editor or
reg for settings that require a
registry change. Consult regman
first: it explains what a key means, accepted values and when changes
apply. A value being stored does not by itself prove the service accepted it.
Follow Change a setting and verify it
for the complete workflow.
For configuration backup and recovery, read Backup and restore before changing the registry's stores or layers.
Troubleshoot a problem
Peios / Using Peios / Start here
Start with what failed and when. Keep the error text and the name of the service, package, account or interface involved. Read the current state and relevant records before restarting a service, changing permissions or repeating an operation that changes the machine. For a reviewable event capture, follow Save incident event evidence.
The browser cannot reach the desktop #
- Check the address and port against
Sign in from a browser.
A machine configured to listen only on
127.0.0.1is not reachable from another device. - If the browser shows a certificate warning, follow The machine's certificate. Do not enter credentials when the certificate fingerprint does not match.
- At a console you can still reach, use
net statusto inspect the address, link and readiness. Check the GXWI service,gxwid, through Controlling services. - If this happened during installation or first boot, return to that installer's guide. Network changes, reboot and completion of setup can change the address or what the browser shows.
Sign-in is refused #
An unknown account and a wrong credential intentionally produce the same sign-in failure. Check that you are using an account on this machine and follow Creating accounts for local-account administration. Do not infer that an account is missing from the sign-in error alone.
For an administrator investigating the failure, the sign-in audit trail explains the authority's and local account service's events. Those records are available only to accounts permitted to read them.
A service will not start, stops, or keeps restarting #
Use Service troubleshooting. It covers configuration, identity, dependency and crash-loop failures. Inspect the service's state and its latest output before retrying: a running process is not always a ready or healthy service.
In Services Manager, select the service and choose Logs… to open Event Viewer on its output, including hooks and health checks. Widen the time range if the failure is older than the default last 24 hours.
Networking is missing or changed unexpectedly #
Use Network Manager or
net status to check the interface's
verdict, selected profile, address, gateway and warnings.
- Not managed / IGNORE: check which profile rule applies.
- A rejected configuration: the last working rules can remain active; read the rejection before assuming the new settings took effect.
- An address but no route: distinguish local connectivity from a route out of the machine.
- Addresses work but names do not: follow Name resolution.
Make lasting changes through the supported network settings or registry profiles. Manual changes to a managed interface can be reverted by netd. Network Manager labels some firewall displays Example data; do not use those displays as evidence of live traffic. For the next check for your symptom, follow Network diagnostic tools.
The clock or displayed time is wrong #
Start with Keeping the time: inspect the effective time zone, clock state and source failures before changing the clock. A wrong time zone and an unsynchronised clock need different corrections.
A write fails or a filesystem is full #
Keep the exact error and affected path. Find why a filesystem is full distinguishes block space, inode pressure and incomplete directory measurements before choosing a supported remedy. Do not turn a permission or I/O failure into a deletion task without evidence.
A package or feature change was interrupted #
For a package operation, follow Transactions and recovery before starting another transaction. For a feature, follow Interrupted features; their recovery uses the feature's own lifecycle, rather than package transaction recovery.
An action is denied, or records seem to be missing #
For a refused operation, follow Debugging a denial. Collect the failing operation, identity and required access before changing permissions. The task guide explains the access needed for that operation.
Event queries omit records you may not read. Event Viewer can show whether its read policy hides records, or whether it could not read that policy. An empty list is not proof that nothing happened. Also check the time range and filters, and remember that logs are best effort.
Follow Find missing records to distinguish filtering, access, retention and collection failures. What the machine records explains logs, events and metrics; Event Viewer explains filtering, older records and access notices.
Install Peios on a PC
Peios / Using Peios / Installing Peios
Use this guide for a fresh installation on a physical PC. At the end, the PC boots from its own disk and you can sign in with the administrator account you created during setup.
If the disk already holds a Peios installation you want to keep, choose Upgrade an installation or Repair an existing system instead.
What you need #
- An x86-64 PC with UEFI firmware. Legacy BIOS boot is not supported.
- A USB stick of at least 2 GB that you can erase.
- A Peios
.isoimage. If you need to build one, follow the peiso quick start. - A screen and keyboard for checking the machine, its network address, and its certificate. After setup, you can use another device's browser on the same network.
- A wired connection for browser access. Console setup works offline. Peios can detect supported Wi-Fi hardware, but joining a wireless network is not yet available.
Keep the machine on a network you trust while installing and setting it up: the live installer and pending first-boot setup can be opened without signing in.
Write the medium to a USB stick #
Write the .iso as a raw disk image. Copying the file onto an existing filesystem does not make a Peios boot medium.
On Linux:
-
Identify the stick by size, model, USB connection, and removable status (
RMis1):| -
Unmount any of the stick's filesystems that the desktop mounted. Otherwise pending filesystem writes can overwrite part of the new image later.
-
Select the stable identifier for the whole stick, not a partition. Replace the placeholder below with the identifier you just checked. Verify the destination again before running
dd; it overwrites that device. -
Wait for the command to finish successfully before unplugging the stick.
A stable identifier avoids relying on /dev/sdX letters, which can change as devices are attached. It does not replace checking that you selected the right stick.
On another operating system, use a raw image-writing tool. Choose DD mode or image mode when the tool offers a choice between raw writing and copying files.
Set up the firmware #
Enter firmware setup at power-on; the screen usually names the key, often Del or F2.
- Turn Secure Boot off. The Peios boot image is not signed with a key that PC firmware trusts out of the box.
- Use UEFI boot. Note the one-time boot-menu key, often F7, F11, or F12, so you can choose the stick without changing the permanent boot order.
- Leave the storage-controller mode unchanged unless disks are missing. If the installer reports an unclaimed RAID, Intel RST, or VMD controller, check the firmware's storage settings. AHCI is the documented alternative. Record the previous setting before changing it; a controller-mode change can affect an existing operating system.
Boot the live system #
Insert the stick and select it from the one-time boot menu. If there are two entries for it, choose the one marked UEFI.
During startup, look for a message like this; the device name can differ:
OK live-boot: mounted boot medium /dev/sdb at /mnt/medium
A medium with the console installer starts its form. A live login uses the peios account with no password. When you have a shell, check the hardware and networking:
A wired interface with readiness routed and an address is on the network. A wireless interface showing down, no-carrier is expected while Wi-Fi joining is unavailable; its presence still indicates that the driver and firmware loaded.
The live system keeps changes in memory and discards them at reboot. The current installer copies the shipped image, not your live session's accounts or changes.
If the boot stops #
| Symptom | Check or next step |
|---|---|
| Firmware reports a security violation or refuses to start the stick | Check that Secure Boot is off. |
no boot medium carrying /rootfs.squashfs found after 50 attempts | Check that the image was written raw; rewrite it if needed. If the stick appeared too slowly, try another USB port. |
| The stick is absent from the boot menu | Check the raw image write and that firmware allows UEFI boot rather than only legacy boot. |
| The installer cannot see an internal disk | Look for its unclaimed-controller warning, then check storage-controller mode in firmware. Use Rescan after checking the hardware. |
Install #
Use the console form, or open https://<address-from-net-status>:7780 on another device. Browser installation requires a medium with the graphical installer enabled.
Before trusting a browser certificate warning, compare its SHA-256 fingerprint with this file read at the machine's own console:
Do not enter credentials if the fingerprints differ. See the machine's certificate for verification and browser-trust details.
- Choose the installation operation.
- Select the internal disk by its device, model, size, and bus. The USB medium is marked and cannot be selected.
- Review the erasure confirmation, including the existing systems and used filesystems it found. Go back if anything is unexpected. An unrecognised or encrypted filesystem may not have its contents listed; it will still be erased.
- Confirm the install. In the browser, hold down the button that starts it.
- Leave the machine and medium connected until the installer reports Installation complete. Reboot to start Peios. Closing a surface does not cancel the daemon's installation.
Installing to disk explains progress, failures, the disk layout, and the older command-line script.
Restart from the installed disk #
After the installation-complete message, remove the USB stick before choosing Reboot now. Do not remove it while installation is still running.
The installer does not change firmware boot order. If the machine returns to the installer, it probably booted from the stick again: remove the stick and restart. You do not need to install again.
Once disk boot is working, you can put the internal disk first in firmware boot order. Remove obsolete boot entries only after identifying them as entries for the system you replaced.
First boot #
First-boot setup creates your administrator account, sets the machine's name, and optionally applies a manual wired-network address. It is available on the console and, when the image includes it, at the browser address.
installerd attempts to carry the live medium's GXWI key and certificate into
the installed system. The transfer can be skipped or fail without failing the
installation, so the completion message does not prove that the certificate is
unchanged. If the installed fingerprint differs, verify it through the
installed machine's trusted console before entering credentials. See
certificate carry-over and first boot.
Automatic waiting and reconnecting also depend on browser trust; if the tab
shows a browser error after restart, reopen the address manually.
Check the result:
- The PC starts from its internal disk with the USB stick removed.
- Setup finishes and gives you a sign-in prompt.
- You can sign in with the account and password you just created.
- If you changed the network address, the new address reaches the sign-in page.
If setup repeatedly fails, keep the install medium available and follow first-boot recovery.
Machines known to work #
| Machine | Documented result |
|---|---|
| Beelink Mini S (Intel Alder Lake-N) | Ethernet (r8169), NVMe, and USB work without configuration. Intel Wi-Fi is detected, but joining a wireless network is not yet available. |
Sign in
Peios / Using Peios / Sign in
Choose the sign-in route available on your machine. If you have just installed Peios, complete first-boot setup first. Manual and legacy installation routes can provision accounts differently; Creating accounts explains those distinctions.
Choose how to connect #
- Use the desktop in a browser: Open the desktop. Check the machine's certificate before entering credentials.
- Use a local console: The login command explains the prompt, live-medium autologon and multiple-console behavior. Switching consoles does not lock or log out the session you leave.
- Use a remote terminal: Sign in over SSH. Verify the host fingerprint and read the access precautions before changing the SSH service or credential policy.
Manage your own account #
Use passwd to change your own password. For your SSH keys, follow Changing your own keys. To change the name shown beside your account name, follow Changing your own display name. Changing another account's credentials is an administration task; start with Manage local accounts and groups.
A stored password and the account's credential policy are separate. Follow Creating accounts when changing whether a password or SSH key is required; setting a password alone does not change a passwordless account's policy.
If sign-in fails #
Start with Troubleshoot a problem. Keep a working administrator route available while investigating. Do not delete the local principal store to try to restore access: it holds the machine's identity, and a corrupt store is different from a missing one. See The local store.
For the model behind the prompts, read How authentication works and Logon sessions in Security Fundamentals.
The login command
Peios / Using Peios / Sign in
login is the terminal client for signing in. It collects an identifier, renders whatever credential prompts the authority sends, installs the token it is granted, and replaces itself with your shell.
login [name] [options]
login --try <name> [options]
login --try-no-password <name> [options]
It does not know what a password is. It renders the prompts it is asked to render and returns the answers, so adding a new credential type — a one-time code, a smartcard — changes the authority and leaves login alone.
Naming a principal, or not #
With no name, login asks for one:
Username: alice
Logging in as alice
Password:
With a name, the first prompt is skipped. The Logging in as line comes from the authority rather than from login, so it will gain a realm once authorities have them.
A principal who needs no credential is signed in immediately, with no prompt at all — see passwordless accounts.
--try and --try-no-password #
Both attempt a named principal and fall back to an ordinary prompt rather than failing. They differ in what they offer to collect.
--try <name> attempts the principal with login's full capabilities. A principal whose policy requires no credential is signed in immediately; one whose policy calls for a password is prompted for it.
--try-no-password <name> attempts the principal while declaring that it can collect nothing, so only a principal who needs no credential can succeed. This is what a console autologon uses.
Neither flag asserts that anyone is authenticated. The authority still decides, and understating what you can collect only denies you prompts you could have rendered — so running either by hand gains you nothing you did not already have.
The first two columns below describe credential policy, not just whether a password is stored. They assume an enabled account with console logon permitted and, for password sign-in, a password set.
| No credential required | Password required | No such principal | |
|---|---|---|---|
login alice | signed in | prompts | user alice does not exist |
login --try alice | signed in | prompts, then falls back if wrong | falls back silently |
login --try-no-password alice | signed in | falls back silently | falls back silently |
A fallback restarts the sign-in from Username:, so you are never locked to the principal that was attempted.
When a fallback explains itself #
login says why it fell back only if something was already on your terminal.
A --try that reached a password prompt has interrupted you, and dropping to Username: without a word would read as a fault — you typed a password and got asked for a name. So it reports first:
$ login --try alice
Logging in as alice
Password:
login: Password incorrect
Username:
A --try-no-password that was refused before anything was rendered has interrupted nobody, and stays quiet. That is what keeps a line about a failed autologon off the console of every machine where the principal requires a credential.
Falling back is limited to denials another principal could survive. A failure of the authority itself is reported and login exits, because offering a prompt that cannot work either would spin a console.
Where existence is checked #
login asks /run/ident.sock whether a principal exists, not the logon socket. The logon socket will not tell it — an authority never distinguishes an unknown principal from a bad credential — while the identity socket answers plainly, which is what it is for.
If that lookup cannot be answered, login attempts the sign-in anyway rather than reporting an absence. An unreachable source reported as "no such user" would turn an outage into a fact.
Autologon on a console #
A console that signs in on its own is a passwordless principal plus --try-no-password. On a live image, peinit starts:
/bin/login --console --try-no-password peios
The same service definition suits an image where peios requires a password: the attempt is refused before anything is rendered, and an ordinary prompt appears. No second seed, and no conditional configuration.
To require a password instead of autologon, first set the principal's password with lps password, then select lps policy <name> password. Setting the password alone leaves the none credential policy and autologon unchanged.
Taking --try-no-password out of the login-console service's Arguments stops the automatic attempt, but does not change the account's credential policy: a principal whose policy is still none can still sign in at that prompt without a credential. To move the prompt to another terminal, change the service's TTYPath — see controlling services.
System Settings chooses it in its Startup & Shutdown section, under Console Sign-In: Automatic Sign-In is Off, or one of the local accounts. The choice is written the moment it is made, as Arguments: --console, followed by --try-no-password and the account if one is chosen. It applies the next time login-console starts, at the latest the next boot. Changing it needs write access to Machine\System\Services\login-console, which as shipped only Administrators have.
GXWI information on the console #
Managed login --console sessions show GXWI's browser address and certificate
SHA-256 fingerprint before authentication when GXWI is installed. Compare the
fingerprint on this trusted local console with the browser's certificate
details before accepting its warning. See the machine's certificate.
The banner reads /var/state/gxwi/certificate.sha256 and the configured
Machine\Software\GXWI\Listen address and port. With the IPv4 wildcard
listener Peios ships (0.0.0.0:7780), it lists the machine's active
non-loopback IPv4 addresses. If the value is missing or unreadable, the
banner shows GXWI's own fallback, the loopback address 127.0.0.1:7780. An IPv6
wildcard lists IPv6 addresses; link-local addresses that need an interface zone
are omitted. A specific listen address is displayed as configured. This is
connection information, not a check that GXWI is currently running.
Information is read once each time login starts. Missing or malformed
fingerprints and unavailable addresses are reported without preventing login.
Ordinary login invocations without --console do not show this banner.
Options #
| Option | Effect |
|---|---|
--try <name> | Attempt name, falling back to a full prompt. |
--try-no-password <name> | Attempt name collecting nothing, falling back to a full prompt. |
--console | Reset terminal input, wait for first-boot setup to finish, and show available GXWI connection information before collecting credentials. Serial speed is preserved; setup-registry errors prevent logon. |
-p | Keep the inherited environment instead of building a fresh one. |
-h <host> | Record the remote peer for a sign-in originated on its behalf. Unverified. |
-H | Accepted and ignored. Suppresses the hostname banner on other systems. |
-- | Treat everything after as a name, not an option. |
-f is not implemented. On Linux it means "trust me, they are already authenticated", gated only by the caller being root. On Peios that decision belongs to the authority, taken from the verified peer, so accepting a flag here would put an authentication bypass in an unprivileged process.
The environment your shell starts in #
Without -p, login builds a fresh environment: HOME, SHELL, USER, LOGNAME, PATH=/bin, and TERM. The shell's argv[0] gets a leading dash, which every shell reads as "this is a login shell, run the profile files".
TERM is carried through if login was started with one. Otherwise login decides it from the terminal it is on, the way a getty does elsewhere: linux on a virtual console (/dev/tty1 and its siblings, the display), vt220 on anything else (a serial line, a hypervisor's paravirtual console). /dev/console is whichever of those the kernel chose, and login resolves it through the kernel's own record of the active console rather than guessing, so one service definition on /dev/console gets the right answer on a machine with a display and on one without.
The home directory comes from the profile the authority sent, and a missing one is not fatal. login reports it and starts you in /. Refusing to proceed over an absent directory would turn a cosmetic problem into being locked out.
login does not create it. Two reasons, and either would be enough. It has already made your token its own by the time it changes directory, so it is you rather than the system — and /home lets you walk through it but not create in it. And directory provisioning has no business inside the one program that has to keep working when everything else is broken. The authority creates the directory when it grants the logon, before login is handed anything; reaching this fallback means the authority named no home, or could not make one.
Exit status #
login does not return on success — it replaces itself with your shell, so what you see afterwards is the shell's exit status.
| Code | Meaning |
|---|---|
1 | A usage error, a denied sign-in, or the shell could not be started. |
Multiple local consoles #
The Experimental edition provides independent login sessions on tty1, tty2
and tty3. Switch between them with Alt+F1, Alt+F2 and Alt+F3 at a text console,
or use chvt 1, chvt 2 and chvt 3 with the required console authority.
Switching does not lock or log out the session you leave.
login-console follows the kernel's primary console: peinit resolves a display
console to the fixed tty1, or uses the selected serial/paravirtual terminal.
It retains the live-medium passwordless peios attempt. Additional services
login-tty1, login-tty2 and login-tty3 show ordinary username prompts. On a
display-primary machine, the duplicate login-tty1 service is skipped because
login-console already owns that terminal. On a serial-primary machine all
three local prompts can run alongside the serial session. Missing terminals
are skipped.
First-boot setup retains the primary console. Additional terminals wait until both setup service definitions have been removed; they collect no credentials while setup is pending. Logging out terminates that terminal service's remaining processes and produces a fresh prompt with canonical input and echo restored. A normal logout does not consume the crash-restart budget. This resets terminal input settings; it does not implement a session lock or promise to erase all screen history.
The passwd command
Peios / Using Peios / Sign in
passwd changes your own password.
passwd
It takes no arguments. The account it changes is the one you are signed in as: the authority reads it from your token, and the request has no field in which to name anybody else. To set another principal's password, an administrator uses lps password.
What it asks #
$ passwd
Changing the password for alice
Current password:
New password:
Retype new password:
password changed
Your current password is always asked for, even though you are signed in. Being signed in shows that someone authenticated as you; it does not show that the person at the keyboard now is you, and a password changed from an unattended terminal could not be recovered by signing in again.
If the new password and its confirmation differ, or the new one is empty, you are told why and asked again. After three attempts the change ends with the password unchanged.
The prompts come from the principal source that holds your account, not from passwd. passwd renders what it is asked and returns the answers, so a source with different requirements asks for what it needs without passwd changing.
How it works #
passwd opens a credential-change conversation with the authority on /run/logon.sock (PGSS §2.20). authd finds the source that holds your account from your token's SID and relays the conversation to it. lpsd checks your current password, sets the new one, and writes its store to disk before it reports success, so password changed means the next sign-in checks the new password. It records the change as an lpsd.credential.changed event, naming your account by SID, and records a refused change the same way with the reason, such as wrong-credential for a current password that did not verify.
Nothing is minted. Your current session carries on as it was, and so does every other session you hold.
Other ways to change it #
passwd is one client of this conversation, not the only one. A program can hold the same conversation for you. On the desktop, My Settings (type my in the launcher) does: its Password section collects your current password and the new one twice, and hands them over when the authority asks. The line along the bottom of the window shows what the authority said, then whether the password changed, and the fields are emptied whatever happened. An account with no password sees why in place of the form. The authority asks the same questions, applies the same rules and gives the same refusals whichever program asks; only how the questions are shown differs. A program written in Rust gets the conversation from the credential module of libauthd-client, which passwd itself uses.
Adding or removing your own SSH keys is a sibling conversation on the same socket, and asks for your current password the same way: see Changing your own keys.
From a script #
When standard input is not a terminal, passwd prints no prompts and reads one line for each, in order:
printf '%s\n' "$current" "$new" "$new" | passwd
When it refuses #
| Message | Meaning |
|---|---|
Authentication failed. | The current password was wrong, or the password was reset by an administrator while you were changing it. |
This account has no password to change. … | The account has no password available for this change, or exists only to run a service. For an ordinary account, an administrator can set a password with lps password and select a policy that allows it with lps policy. A service principal cannot be given a credential. |
This account is disabled. | An administrator has disabled the account. |
This account has no credential that can be changed here. | You are signed in as an identity that no principal source holds, such as SYSTEM. |
Too many attempts. The password is unchanged. | The new password was refused three times. |
cannot reach the authority … | authd is not running. |
permission denied connecting to /run/logon.sock … | This machine's logon socket does not admit you. See below. |
A message ending whether the password changed is not known means the authority went away after the change was asked for and before it said how it ended. Sign in with each password to find out which one holds.
Who can reach the socket #
Every authenticated principal can connect to /run/logon.sock so that passwd works for them. That does not let them originate a logon for anybody else, which still takes a LogonTypes record on their own policy entry.
A machine whose administrator has replaced the socket's descriptor with LogonSocketDescriptor may not admit you. See assigning privileges.
Exit status #
| Code | Meaning |
|---|---|
| 0 | The password was changed. |
| 1 | The change was refused or failed, or the authority could not be reached. |
| 2 | The command line was wrong. |
See also #
- How authentication works — the conversation this is one kind of.
- The
lpscommand — setting another principal's password, as an administrator. - PGSS §2.20 — the credential-change conversation, specified.
Signing in over SSH
Peios / Using Peios / Sign in
The SSH server lets people sign in to this machine from another over the network, with their password or one of their SSH keys. It is the sshd service, from the dev.peios.openssh package. Every SSH sign-in is a RemoteInteractive logon through the same authority as any other: see How authentication works.
That includes commands without a PTY and SFTP: the account must permit remote-interactive, not just network. An account with credential policy none cannot sign in over SSH. Password authentication requires an actual password and a policy allowing it; key authentication requires an enrolled key and a policy of key or either. The SSH implementation contract records the release-qualification and compatible-build requirements.
Four things about the server are settings:
| Setting | Where it is | Shipped as |
|---|---|---|
| Whether it runs | the sshd service's Disabled value | on, on images that include the service |
| Its port | Machine\Software\OpenSSH Port (REG_DWORD) | 22 |
| Whether it accepts passwords | Machine\Software\OpenSSH PasswordAuthentication (REG_DWORD, 0 or 1) | 1 |
| Its host key | /var/state/sshd/ssh_host_ed25519_key, made on first start | — |
The port and password sign-in are machine policy in the registry, not lines of sshd_config. start-sshd reads them each time the service starts and gives them to sshd on its command line, and a Port or PasswordAuthentication line in /lcl/etc/ssh/sshd_config has no effect. Other sshd options still go in that file.
Change them with ssh-settings on a terminal, or SSH Settings on the desktop (type ssh in the launcher), which is the same program and does the same things.
Looking #
$ ssh-settings
server running
port 22
passwords accepted
host key SHA256:Wm3…
Anyone may look, except at the host key, which only SYSTEM and Administrators may read; others see why instead.
Connect from another computer #
-
From a trusted administrator session on the Peios console, or a certificate-verified GXWI session as an administrator, read the machine's address, SSH service state, port and host fingerprint:
net status ssh-settings ssh-settings fingerprintCheck that the server is running. Choose an address on an interface reachable from the other computer; Reading
net statusexplains the interface blocks. For a direct connection, use the port shown byssh-settings, which is 22 as shipped. The QEMU Makefile's host-forwarded port 2222 is a separate route described in the build quick start. -
On the other computer, use your account name, the machine's address and that observed port:
ssh -p <observed-port> <name>@<machine-address>Before accepting a first-connection prompt, compare its SSH host fingerprint with the trusted reading from step 1. This is the SSH host key, not GXWI's TLS certificate fingerprint. If they differ, stop and investigate; do not accept the key or discard a remembered key just to suppress a warning.
-
Sign in with the intended password or enrolled key and check that a new shell opens. A running service or an already-open session alone does not prove that a new sign-in works.
Turning it on and off #
ssh-settings on
ssh-settings off
on sets the service to start at boot and starts it now; off stops it and keeps it from starting. People signed in over SSH are disconnected when it stops. Each needs write access to the service's key, Machine\System\Services\sshd, and the right to start and stop sshd, which as shipped only Administrators have; see who can manage a service. Without the second, the setting is still saved and takes effect at the next boot, and you are told so.
Moving the port #
ssh-settings port 2222
A port is three things that must agree, and ssh-settings changes all three in one registry transaction, so none is ever moved without the others:
- the
Portvalue; sshd's port reservation, the descriptor underMachine\System\Network\TcpIp\PortReservationsthat lets onlysshdbind the port (see port reservations);- the
sshfirewall rule,Machine\System\Network\Rules\Flow\ssh, whoseDstPort.Equalis the port let in (see network policy).
It then restarts sshd onto the new port, which disconnects everyone signed in over SSH, you too if you are. Connect again on the new port: ssh -p 2222 alice@host.
A port another program has reserved is refused: a reservation naming that one port and nothing else, such as tcp:80 for a web server, makes it that program's. A wider range that contains the port, such as tcp:1-1023, is fine, since reservations nest. Moving the port needs write access to all three keys, which as shipped only Administrators have.
Keys only #
ssh-settings passwords off disables password authentication for SSH. It does not require an account-wide policy of key. Keep the account policy either when its password must also work through GXWI: GXWI does not currently offer SSH-key authentication, so a key-only account cannot use that sign-in path.
With SSH passwords off, an account whose policy is password cannot sign in over SSH, though it can still use its password at the console and on the desktop, subject to its other sign-in restrictions.
Before changing the server:
- Enroll a key for each person who needs SSH. They can use My Settings where permitted, or an administrator can use
lps key add <name> <public-key-file> [label]. Adding a key does not change credential policy. For a password-enabled account that will also use keys, an administrator setslps policy <name> either; retain that policy if GXWI password access is needed. - Inspect
lps show <name>andlps key list <name>to check the enabled account, enrolled key, credential policy and permittedremote-interactivelogon type. While SSH passwords are still available, make a fresh SSH connection that actually authenticates with the enrolled key, following the host-key check above. A connection that falls back to a password does not count as a key test. If the key test fails, stop before disabling passwords and check those account settings and the service state. - Verify a fresh administrator sign-in through a separate non-SSH route, such as the local console or certificate-verified GXWI, and keep that route available. The last-administrator guard does not prove that a usable route exists. Keeping another SSH session open is insufficient: this change restarts
sshdand ends every SSH session.
Only after both access paths work, disable SSH passwords:
ssh-settings passwords off
Then make another new key-authenticated SSH connection and verify that its shell opens. If it fails, use the surviving console or GXWI administrator route to inspect the settings and restore SSH password acceptance with ssh-settings passwords on. This reversal also restarts sshd; it does not change account credential policy. Keep the separate administration route until fresh SSH access has been verified again.
The host key #
ssh-settings fingerprint
The first time someone connects, their SSH client shows the server's host key fingerprint and asks whether to trust it. Compare it with this one, read on the machine itself or by someone you trust who can read it, before answering yes. The key is made on the server's first start and never replaced automatically.
On the desktop #
SSH Settings shows the same four things on one page: the server, with how it is and a switch to turn it on or off; Port, with Apply once it is changed; Password Authentication, as a switch; and Host Key Fingerprint, with Copy. Turning the server on happens at once. Anything that restarts or stops it — turning it off, moving the port, password sign-in either way — ends every SSH session, so it asks first, under its row, and changes nothing until it is told to go ahead. What you may not change is shown read-only, with the reason, once. Each change says what it did along the bottom of the window, including whether sshd was restarted.
Exit status #
| Code | Meaning |
|---|---|
| 0 | Done, or shown. |
| 1 | The change was refused or failed; the reason is printed. |
| 64 | The command line was wrong. |
Manage local accounts and groups
Peios / Using Peios / Manage local accounts and groups
Local accounts and groups are held by lpsd, the local principal source.
Use Principals Manager
on the desktop or the lps command
in a terminal to manage them. The guides distinguish changes you can make to
your own account from administrator operations.
Choose a task #
- Create an account: Creating accounts covers credentials, membership and the different first-account provisioning routes. The desktop equivalent is Making a user.
- Change an account or its sign-in policy: Changing a user or the lps reference. A password being set does not by itself require that password at sign-in.
- Create a group or change membership: Groups in Principals Manager or Grouping people together.
- Inspect or change privileges and integrity policy: Assigning privileges. This is the machine's local policy, separate from the account's identity. A policy change affects newly minted sign-in tokens, not existing tokens.
- Change your own password, SSH keys or display name: Sign in links the self-service procedures.
Keep a usable administrator route #
Before changing an administrator's credentials, membership or sign-in access, keep a working administrator session and verify the replacement route. The last-administrator guard checks account state; it does not prove that you can actually sign in through a usable route. SSH configuration changes can end every SSH session, so follow the separate-access precautions there.
Read Disabling rather than deleting before removing an account. Recreating a name does not restore the old SID or the access granted to it.
Understand the model when needed #
Security Fundamentals explains authorities and principal sources, local identity storage, name resolution and privileges. Tool authors and administrators investigating a refused request can consult the local principal interfaces in the technical reference.
Creating accounts
Peios / Using Peios / Manage local accounts and groups
Creating an account #
Run lps add with no arguments and it asks for what it needs:
$ lps add
Name: alice
Full name [optional]: Alice Chen
Additional groups [comma-separated, optional]: Administrators
Primary group [the daemon's default]:
Home directory [the daemon's default]:
Shell [the daemon's default]:
Password for alice:
Again:
name alice
full name Alice Chen
primary group (the daemon's default)
home (the daemon's default)
shell (the daemon's default)
groups Administrators
Create this principal? [Y/n]:
created alice with RID 1002
An empty answer takes the default, and nothing is sent until you confirm. Or say it all on one line:
$ lps add alice --group Administrators
Anything you supply is not asked for, so either style works and they mix freely.
Groups are named however you like — Administrators, a local group's name, or a literal SID such as S-1-5-32-544. lpsd resolves it.
You need to be an administrator to do this. See the lps command for the full surface.
What a new account gets #
| RID | The next one. Never chosen, never reused. |
| uid | The RID, plus this machine's base — so RID 1000 signs in as 1001000. |
| Primary group | Authenticated Users, unless you say otherwise. |
| Home | /home/<name> |
| Shell | /bin/sh |
No per-user group is created. That Linux convention exists so a file's group ownership means something, and under KACS it means nothing — access is decided by the token, not by the mode bits. A group per user would be ceremony with a RID attached.
The home directory is not created here. lps records the path and nothing more. The directory appears at the principal's first sign-in, made by the authority when it grants the logon — and only when the account has a home recorded, which is why a service principal never grows one.
The shipped authd service explicitly retains SeRestorePrivilege so it can
assign the new directory's owner and group to that principal. Removing it
from RequiredPrivileges can leave a newly created home accessible only to
SYSTEM and Administrators, preventing an ordinary user's session from
starting there. Authd reports this failure and leaves existing directories
alone; an administrator must repair their descriptors deliberately.
It is created owned by the principal, with a protected DACL: full control for its owner, for SYSTEM and for Administrators, and nothing for anybody else. Protected matters — without it the root's inheritable "Everyone may read" ACE would apply and every account could read every other account's files.
A principal whose home could not be created still signs in, starting in /, and login says so. A directory is not worth failing a logon over.
An existing directory is left exactly as it is, and one owned by somebody else is reported rather than taken over. So pointing a new account's home at another principal's directory does not hand it over — it produces a warning in the log and an account that starts somewhere it does not own.
Accounts with no password #
lps add <name> --no-password creates a principal whose credential policy is none. It signs in without being asked for anything where the client and authority permit credential-free sign-in.
The sign-in is otherwise completely ordinary. lpsd still vouches for the principal, authd still mints the token and still applies this machine's privilege and integrity policy to it, and the session that results is indistinguishable from one reached by typing a password. Nothing is bypassed; there is simply nothing to collect.
It is a property of the account. Nothing ties it to a particular terminal: any client that permits credential-free sign-in can use it, subject to the account's logon-type restrictions and the authority's policy for that client. SSH does not permit it; SSH requires an actual password or an enrolled key and a policy that allows that credential. A passwordless account enables console autologon, but do not assume that its use is limited to that console.
An empty password is not the same thing and is refused. See the lps command.
Make the first account an administrator #
An ordinary account is usable on a stock image. The descriptor the system tree ships with grants Everyone read and execute — and on Peios execute is also traverse — so any principal can list directories, read the system tree and run what is in it. What it cannot do is write there. A non-administrator writes only where something grants it: its own home directory, which is created at first sign-in with a protected DACL admitting the owner, LocalSystem and BUILTIN\Administrators and nobody else, and /tmp, where anyone may create files and only their owner may remove them. See Installing to disk for the root descriptor itself.
What an ordinary account cannot do is administer the machine. lps refuses it, so it cannot create the next account; it cannot mount a filesystem, install a package or change a service definition either. The first account is therefore an administrator because nothing else on the machine can make one, and it stays one because lps will not leave the machine without an enabled administrator (see The last-administrator guard below). The accounts after it need Administrators only if they are going to administer.
Where the first account comes from #
A machine with no store provisions one at first boot, and it is empty — lpsd generates the domain, because a source with no domain can answer nothing, but it does not invent accounts.
So something has to create the first one, and there is a chicken-and-egg problem: lps talks to lpsd, so lpsd must already be running, which means this cannot be done by an early-boot script. Autorun scripts run before any service has started.
The provisioning path depends on the edition. An installed edition can enable first-boot setup; a development image can instead ship a oneshot service, ordered after lpsd, that runs lps to create a known account. An image with no first-account provisioning path needs an already-authorised way to create the first account.
On an installed edition with first-boot setup #
Complete first-boot setup creates the first administrator after installation. Use the console form, or the browser form when the image includes oobe-gxwi, then verify sign-in with the new account. Use lps add as that administrator for further accounts.
This path is enabled only when the edition includes dev.peios.oobe and opts into its oobe-service seed through install_autoapply. The oobed service runs as SYSTEM to create the account; the user-facing form does not need an existing administrator sign-in. Do not assume every custom image includes or enables it. The development-account seed below is a separate provisioning choice.
On a development image #
Images built for development ship two things: a script that creates a known account, and a registry seed defining the oneshot service that runs it. If your image has them, it boots with an account already present, and lps list will show it.
On a live image that account has no credential at all, and it is an administrator. The script creates it with lps add peios --group Administrators --no-password, so anyone who reaches a prompt that permits credential-free sign-in on that machine can become it — and the console signs in as it automatically, without asking anything.
That is deliberate rather than an oversight. A live ISO is an unauthenticated medium: anyone holding it can boot it and read everything on it, so a password would be a formality rather than a boundary, and one printed in the image at that. What it is not is a posture to carry anywhere else.
Give it a password the moment the machine becomes something you care about, then say it signs in with that password. The two together turn it into an ordinary account and stop the console signing in on its own:
$ lps password peios
New password for peios:
Again:
set the password for peios
$ lps policy peios password
credential policy updated
The password alone changes nothing: until lps policy says otherwise, the account still signs in with nothing. Set the password first, since a policy of password is refused for the last administrator while they have none.
Better still, build an image without the development-account seed, with first-boot setup or another deliberate account-provisioning path. Omitting the development seed alone does not create an account; see On an image with no account provisioning below.
On an image with no account provisioning #
On a fresh image with no accounts, no enabled first-boot setup and no account-provisioning seed, lpsd starts, provisions an empty store, and logs that no principals exist and no logon can succeed until one is created. The login prompt will appear and nothing will satisfy it.
That is the correct behaviour rather than a fault, but it does mean you need a way in. Create the first account from a console session that is already SYSTEM — lps accepts LocalSystem as well as administrators, precisely for this case.
Disabling rather than deleting #
$ lps disable guest
disabled guest
A disabled account keeps its RID, its SID, and its group memberships. It cannot start a new sign-in. Re-enable it with lps enable.
Disabling an account, changing its password or removing a key does not revoke already issued tokens or end its existing sessions. Use Task Manager's sign-out action when existing sessions must end. Signing out ends processes and can lose unsaved work; session lifecycle explains its permissions and limits, including token references held outside those processes.
Prefer this to lps remove. A removed principal's SID keeps appearing in the security descriptors of everything they owned, and because RIDs are never reused nothing will ever hold that SID again — the files become owned by a principal that no longer resolves.
The last-administrator guard #
This is a guard on the local account store, not a guarantee that a usable sign-in path is reachable.
lps refuses to remove the last enabled administrator, to disable them, or to take Administrators away from them. It refuses as firmly to leave them no way to sign in: no kind of sign-in at the machine, remotely or over the network (lps logon-types), or nothing to sign in with (lps policy and lps key remove).
$ lps disable jack
lps: jack is the only principal who can administer this machine; disabling them would leave no way back in
The check counts only enabled administrators, so a disabled standby account does not license removing the working one.
The guard does not check that a console, remote service or network route is available. After changing credentials or sign-in policy, inspect the account with lps show <name> and verify a fresh sign-in through the intended path before closing your working administrator session. A stored credential and a permitted logon type are not evidence that the whole path works.
This guard exists because there is currently no offline repair. lps reaches the store only through lpsd, and lpsd only authenticates — so a machine with no enabled administrator has no way back short of editing its disk from another system.
Grouping people together #
For anything beyond the built-in groups, create one:
$ lps group create developers
created the group developers with RID 1001
$ lps group add alice developers
added alice to developers
A local group is a real object with its own SID and gid, so it can be named in a security descriptor and it shows up in getgroups. Deleting one is refused while anybody is still in it.
Changing your own password #
Run passwd. It asks for your current password and then the new one:
$ passwd
Changing the password for alice
Current password:
New password:
Retype new password:
password changed
It goes over PGSS Logon rather than through lps, so it works the same way whichever source holds your account — a domain principal changes their password exactly as a local one does. lps password is the other operation: an administrator setting somebody else's.
An account created with --no-password has no current password to prove, so it cannot give itself one. An administrator sets one with lps password, and lps policy <name> password to have it used, or with Principals Manager's Set password, which does both.
Where to go next #
For the full command surface — every flag, the group and claim subcommands, and the exit statuses — read The lps command.
For what the store holds and why RIDs are never reused, read The local store.
The lps command
Peios / Using Peios / Manage local accounts and groups
lps administers the local principal store — this machine's own accounts and groups, held by lpsd.
lps subcommand [arguments]
It is a client and nothing more. It holds no state, opens no store, and has no privilege of its own: every command is a request over /run/lpsd/admin.sock that lpsd decides whether to honour. lpsd must be running.
You must be a member of BUILTIN\Administrators — enabled, not deny-only — or be LocalSystem.
Naming a group #
Anywhere lps takes a group, you may write it three ways:
lps group add jack Administrators # a well-known name
lps group add jack BUILTIN\\Administrators # the same, qualified
lps group add jack developers # a local group
lps group add jack S-1-5-32-544 # a literal SID
Names are matched case-insensitively, and lpsd resolves them, not lps. The tool carries no copy of the well-known table and does not know this machine's domain — a second copy of the machine's identity scheme would be free to disagree with the first.
A well-known name always wins over a local group of the same name, which is why you cannot create a local group called Administrators.
Inspecting #
lps list #
Every principal, with RID, uid, state, and how many groups each is in.
$ lps list
NAME RID UID STATE GROUPS
jack 1000 1001000 enabled 1
guest 1001 1001001 disabled 0
A - in the UID column means nothing numbers that principal — authd has assigned this machine no identifier range, and they will sign in as nobody. See the local store.
lps show <name> #
One principal in full.
$ lps show jack
name jack
display name Jack Palfrey
rid 1000
sid S-1-5-21-2847362817-1094533892-3310298447-1000
uid 1001000
state enabled
primary group Authenticated Users [S-1-5-11]
home /home/jack
shell /bin/sh
logon types default (interactive, remote-interactive, network, network-cleartext, batch, new-credentials)
credential password
groups Administrators [S-1-5-32-544]
developers [S-1-5-21-2847362817-1094533892-3310298447-1001] (gid 1001001)
claims Department (string) = "Engineering"
logon types are the kinds of sign-in the principal may be used for (see lps logon-types below); default means none were chosen, and the machine's default applies. credential is what it signs in with: password, key (an SSH key), either, none (nothing is asked for), or denied.
Groups show a name where this machine knows one, and always show the SID. The two are kept side by side deliberately: the name is what you recognise, the SID is what a security descriptor actually holds, and the moment they disagree is exactly when you need to see both.
Only local groups show a gid. lpsd numbers the groups it owns and nothing else — Administrators and Authenticated Users are numbered by authd, from a table below every source's range, so lpsd genuinely does not know their gid and does not guess. To see the numbers a running session actually holds, read Groups: in /proc/self/status.
lps domain #
This machine's domain SID — the namespace every local principal and local group is numbered under.
$ lps domain
S-1-5-21-2847362817-1094533892-3310298447
Generated at first boot and unique to this machine.
Creating and removing principals #
lps add [name] [options] #
Creates a principal. Run with no arguments, it asks for what it needs:
$ lps add
Name: alice
Full name [optional]: Alice Chen
Additional groups [comma-separated, optional]: Administrators, developers
Primary group [the daemon's default]:
Home directory [the daemon's default]:
Shell [the daemon's default]:
Password for alice:
Again:
name alice
full name Alice Chen
primary group (the daemon's default)
home (the daemon's default)
shell (the daemon's default)
groups Administrators, developers
Create this principal? [Y/n]:
created alice with RID 1002
Any option you supply is not asked for, so scripted invocations keep working unchanged:
| Option | Effect |
|---|---|
--group <group> | A membership. Repeatable. |
--primary-group <group> | The group that becomes the POSIX gid. |
--home <path> | Home directory. Must be absolute. |
--shell <path> | Login shell. Must be absolute. |
--display-name <text> | A human's name for a human to read. |
--disabled | Create it unable to sign in. |
--no-password | Create a principal that authenticates with no credential at all. See below. |
--service | Create a principal that exists to run a service, and can do nothing else. See below. |
--no-prompt | Fail rather than ask for anything missing. |
Nothing is sent until you confirm, so answering n creates nothing. Everything is sent in one request, so a principal is created whole or not at all: if lpsd refuses the shell, say, no principal is made.
lps never prompts when standard input is not a terminal. A prompt down a pipe would consume the next line of whatever is driving the tool. See From a script below.
The RID is allocated by lpsd and reported back. You do not choose it, and the uid follows from it.
--service #
Creates a principal that may be used only to run a service, and for nothing else. It cannot sign in at a console, over the network, or as a batch job — a sign-on of any other kind is refused even with the correct credential.
Most services do not need one. A service already gets an identity
without an account: it runs as LocalService and carries a per-service
SID derived from its name, which you can name in a security descriptor
like any other principal. Reach for --service when a service needs
something a bare SID cannot be — membership of a group, a home
directory, or a profile.
A service principal has no credential and cannot be given one. The authority mints its token on the service manager's attestation instead, and only the service manager may ask: nothing else on the system can obtain a token for it, with or without a password.
The restriction is a property of the account rather than of this machine, so it travels with the principal. It is also the reason the flag exists at all: without it, an ordinary account could be named as a service's identity, and the machine would then be able to obtain a token for that account with no credential.
Every principal created without this flag permits every kind of sign-on a person uses and never a service logon, so an existing account cannot become a service identity by accident.
--no-password #
Creates a principal with credential policy none, which signs in without being asked for anything where the client and authority permit credential-free sign-in. No password is collected at creation, including in the interactive flow — an operator who has said the account needs no credential is not then asked to invent one.
$ lps add kiosk --group Administrators --no-password --no-prompt
created kiosk with RID 1003
This is a property of the account, not of a terminal. Nothing scopes it to one console: any client that permits credential-free sign-in can use it, subject to logon-type and originator policy. SSH requires an actual password or an enrolled key and a policy allowing that credential; it refuses none. Credential-free sign-in is the right posture for a live image, where the medium is unauthenticated anyway and anyone holding it can read everything on it. It is the wrong posture for almost anything else.
An empty password is not a way to spell this, and lps refuses one:
$ lps add alice
...
Password for alice:
Again:
lps: an empty password is not a password: pass --no-password to create a
principal that authenticates without one
The two produce accounts that behave differently — an empty password is still collected, still prompted for, and still has to be answered — so having both spellings would mean two things that look identical at creation and diverge at every sign-in. lpsd refuses an empty password too, so the rule holds however the request arrives.
lps remove <name> #
Deletes a principal.
The RID is not reclaimed. Files the principal owned keep naming a SID that now resolves to nobody, and nothing will ever hold that SID again. lps disable is usually what you wanted — see creating accounts.
lps rename <name> <new-name> #
Renames a principal.
$ lps rename erin erin.k
renamed erin to erin.k
Its SID stays, so every file, permission and group membership that names it still does, and it signs in by the new name from now on. Its home directory stays where it was. lps set --home changes the recorded path only; it does not move the directory or its files.
A name held by another principal or by a local group is refused. Changing only the case, erin to Erin, is allowed.
Enabling and disabling #
lps enable <name> / lps disable <name> #
A disabled principal keeps everything except the ability to start a new sign-in. It does not revoke already issued tokens or end existing sessions. Use Task Manager’s sign-out action when existing sessions must end. Signing out can lose unsaved work; session lifecycle explains its permissions and limits, including token references held outside the session's processes.
Both are refused if they would leave the machine with no enabled administrator.
lps logon-types <name> <type>... #
Sets the kinds of sign-in a principal may be used for. Any other kind is refused, even with the right credential.
$ lps logon-types backup network batch
set the logon types for backup
$ lps logon-types backup default
set the logon types for backup
| Type | Signing in |
|---|---|
interactive | at this machine: its console or desktop |
remote-interactive | to a remote desktop or over SSH, including commands without a PTY and SFTP |
network | to a network resource, such as a file share |
network-cleartext | over the network, with the password sent to this machine |
batch | as a scheduled job |
new-credentials | as a second identity for outgoing connections |
service | as a service, started by the service manager |
The SSH mapping here follows the current PGSS SSH implementation contract, including its release-qualification and compatible-build limits.
default returns the principal to the machine's default: every kind a person uses, and never service. There is no setting for "no sign-in at all"; lps disable is that.
The last enabled administrator must keep interactive, remote-interactive or network. This is an account-store guard; it does not check whether the corresponding console, service or network path is reachable.
Passwords #
lps password <name> #
Sets a principal's password, prompting twice.
$ lps password jack
New password for jack:
Again:
set the password for jack
This is an administrator resetting somebody else's password. A principal changes their own with passwd, which goes over PGSS Logon instead, so that it works identically whichever source holds the account.
An empty password is refused here for the same reason it is refused at creation. A password doesn't change what a principal signs in with: one created with --no-password keeps signing in with nothing until lps policy <name> password says otherwise (see credentials below). There is no command that deletes a password; lps policy decides whether it is used.
From a script, lps reads a single line from standard input when it is not attached to a terminal, and does not ask for confirmation:
printf '%s\n' "$password" | lps add alice --group Administrators --no-prompt
Credentials and SSH keys #
A principal's credential policy says what it signs in with. Its SSH keys are the public keys it may sign in with over SSH. The two are kept apart: adding a key, or setting a password, never changes the policy, and changing the policy never adds or removes either. Enrolling a key and letting it be used are two decisions.
lps policy <name> <policy> #
$ lps policy erin either
credential policy updated
| Policy | Signs in with |
|---|---|
password | their password |
key | one of their SSH keys |
either | their password or one of their SSH keys |
none | nothing, where credential-free sign-in is permitted; SSH refuses this policy |
denied | nothing: every sign-in is refused, whatever is offered |
none is what lps add --no-password gives, and carries the same warning. denied stops a principal signing in and keeps everything else, as lps disable does, but nothing shows it as disabled, so lps disable is usually clearer.
It applies from their next sign-in. lps show and lps key list both say what it is now.
It is refused if it would leave the last enabled administrator nothing to sign in with: denied, key when they have no key, or password when they have no password.
lps key list <name> #
$ lps key list erin
credential key
ID FINGERPRINT ADDED LABEL
893dab4342ba215def78c4036b598b65 SHA256:QDzrP/cBqV7YSxqoCgCJQqKJlo6ijyJ8U05BIEKmYnI 2026-10-04 laptop
The first line is the credential policy. ID is what lps key remove takes, FINGERPRINT is as ssh-keygen -l prints it, and ADDED is the day the key was added, in UTC.
lps key add <name> <file> [label] #
$ lps key add erin erin.pub laptop
key enrolled; credential policy unchanged
The file is one line of an OpenSSH public key file, such as ~/.ssh/id_ed25519.pub. Ed25519 keys are accepted, and RSA keys of 3072 to 8192 bits; nothing else is. Without a label, the key's own comment is its label.
A key the principal already has is refused, as is a 33rd.
lps key remove <name> <id> #
$ lps key remove erin 893dab4342ba215def78c4036b598b65
key removed
Removing the last key of the last enabled administrator, when a key is all they may sign in with, is refused.
Changing your own keys #
lps is for administrators, but a principal does not need one to manage their own SSH keys. Anyone signed in to an account this machine holds can add a key to it or remove one, through a program that asks on their behalf, such as a desktop settings app. Nothing is changed for anybody else's account: the account is always the one the program's token belongs to.
Three things differ from lps key:
- The current password is asked for, every time. Being signed in shows that someone signed in as you, not that you are at the keyboard now, and a key added from an unattended session would be a way back into the account. A wrong password changes nothing and says
Authentication failed. - An account with no password cannot do it. That is an account whose policy is
keyornone, or one whose policy ispasswordbut that has no password set. There is nothing to prove who you are with, so the change is refused and you are told to ask an administrator, who can make it withlps key. - A key is removed by its fingerprint, the
SHA256:valuessh-keygen -lprints for your.pubfile, rather than bylps key list'sID.
Everything else is as lps key add and lps key remove: the same key types and sizes, a key you already have or a 33rd refused, the key's comment as its label. Adding a key does not let you sign in with it unless your credential policy is key or either, which only an administrator can set.
Each change, and each refusal once the account is known, is recorded as an lpsd.credential.changed event, as a change of your own password is. operation.name is ssh-key-added or ssh-key-removed, and outcome.reason says what stopped a refused one, such as wrong-credential. A change an administrator makes with lps key is recorded as lpsd.account.modified instead: see what is recorded.
On the desktop, the program is My Settings, the app for your own account; type my in the launcher to open it. Its SSH Keys section lists your keys by label, fingerprint, type and the day each was added. Add Key… opens a form under the list for the one line of your .pub file and your password. Remove asks for your password under the key before it is taken away, and asks again if it was wrong. An account with no password sees why its keys are an administrator's to change, in place of these forms.
Your password is changed with passwd, or in My Settings.
Changing your own display name #
Programs acting on your behalf can show you your own account as lpsd holds it — your name, display name, credential policy, whether you have a password, and your keys — and set your display name, the human name shown beside your account name. Setting a display name asks for no password, since it is not something you sign in with. Both are served on /run/lpsd/self.sock; see the administrative socket.
Open My Settings (type my in the launcher). In Account, Display Name sets your display name, with Apply once it is changed. The desktop reads it when you sign in, so it shows from your next session.
Profiles #
lps set <name> [options] #
Changes part of a profile, leaving the rest alone.
$ lps set jack --shell /bin/bash --display-name "Jack Palfrey"
updated jack
| Option | Effect |
|---|---|
--home <path> | Home directory. Absolute. |
--shell <path> | Login shell. Absolute. |
--display-name <text> | Empty clears it. |
--primary-group <group> | Which group projects to the POSIX gid. |
None of this is identity — no security descriptor names a home directory, and the token carries none of it. It is what login needs in order to start a session, and it reaches login on the logon reply itself.
lps does not create the home directory. It records the path. A principal whose home does not exist still signs in, starting in /, and login says so.
The primary group need not be a membership: authd adds it to the token if it is missing, so the order of two commands does not matter.
Groups #
Local groups are objects with a name, a RID, a gid and a description — unlike well-known groups, which exist on every Peios machine and are not stored here.
lps group list #
$ lps group list
NAME RID GID MEMBERS SID DESCRIPTION
developers 1001 1001001 2 S-1-5-21-2847362817-1094533892-3310298447-1001 Builds the software
MEMBERS counts principals in this store, which is the only count lpsd can answer for. DESCRIPTION appears when any group has one.
lps group create <name> [description] / lps group delete <name> #
$ lps group create developers "Builds the software"
created the group developers with RID 1001
A group's description says what it is for. Anyone may read it — getent doesn't show it, but Principals Manager does, to everyone.
Deleting is refused while anyone is still a member, or while the group is anybody's primary group. Either would leave a record pointing at something that no longer exists.
lps group rename <name> <new-name> #
$ lps group rename developers engineers
renamed the group developers to engineers
It keeps its SID, so its members, and every permission that names it, stay as they are. A name held by a principal or another group is refused.
lps group describe <name> <description> #
$ lps group describe engineers "Builds and ships the software"
set the description of the group engineers
An empty description, lps group describe engineers "", clears it.
lps group add <name> <group> / lps group remove <name> <group> #
$ lps group add alice Administrators
added alice to Administrators
Removing BUILTIN\Administrators is refused if it would leave no enabled administrator.
Claims #
Claims are named, typed attributes carried on the token and read by conditional ACEs. They are how a security descriptor can say anyone in Engineering rather than naming principals one by one.
lps claim set <name> <claim> <type> [value]... #
$ lps claim set jack Department string Engineering
set the claim Department on jack
$ lps claim set jack Level int64 7
set the claim Level on jack
| Type | Values |
|---|---|
int64 | Signed integers |
uint64 | Unsigned integers |
boolean | true/yes/1 or false/no/0 |
string | Text |
sid | SIDs, in S-1-… form |
octet | Hexadecimal, even length |
A claim may hold several values, and may hold none — lps claim set jack Department string empties it, which is different from removing it.
Claim names are matched case-insensitively, so setting department replaces Department.
lps claim remove <name> <claim> #
$ lps claim remove jack Department
removed the claim Department from jack
A claim reaches a principal at their next sign-in. A token's claims are fixed when it is minted, so setting one changes nothing for a session already running.
Exit status #
| Code | Meaning |
|---|---|
| 0 | Succeeded |
| 1 | lpsd refused the request, or could not be reached |
| 2 | The command line was wrong |
The split lets a script distinguish "I called this incorrectly" from "it was refused".
Common failures #
cannot reach lpsd … it does not appear to be running — lpsd is not up. It exits if it cannot reach authd, or if the store will not read; check its logs before restarting it.
permission denied — you are not an administrator, or the socket's descriptor does not admit you. See the administrative socket.
there is no principal named … — names are case-insensitive, so this means the account genuinely is not there. lps list will show what is.
there is no group named … and it is not a SID — the name matched no well-known group and no local one, and does not parse as a SID. lps group list shows the local ones.
Principals Manager
Peios / Using Peios / Manage local accounts and groups
Principals Manager shows this machine's principals, the users and
groups it knows, and lets an administrator manage the local ones. A
local principal is one this machine holds itself, in lpsd's store; a
built-in one, such as SYSTEM or Administrators, is the same on
every Peios machine and is defined by authd.
Start it from the launcher: Principals Manager.
Users and groups #
Users and Groups, in the bar, switch between the two lists.
- A user is listed with their full name, if they have one, whether they are local or built-in, and whether they are enabled. A disabled user keeps everything but can't sign in, and is greyed.
- A group is listed with whether it is local or built-in, how many
members it has, and its description: what it is for. Some built-in
groups, such as
Everyone, have no list of members at all: who is in them is decided by a rule as each person signs in, and they say By rule.
Type in Find to narrow the list to names, full names, descriptions or SIDs containing what you type.
A user or group in full #
Select one to see it in full on the right.
For a user, that is their user ID, primary group, home folder, shell and SID; the kinds of sign-in they may use, such as at the machine or over the network; the groups they are in; and their claims, the named values that permissions written with conditions are checked against. Copy SID copies their SID.
An administrator also sees what a local user signs in with, such as a password or an SSH key, and their SSH keys: the public keys they may sign in with over SSH, each with its label, its fingerprint and the day it was added.
For a group, it is its description, group ID and SID, and who is in it.
Who is in a built-in group such as Administrators is recorded on this
machine, as a local group's is, so it is listed too. Someone whose
primary group it is, is in it.
Select a group a user is in, or a member of a group, to go to it.
Making a user #
New user, in the bar, or Ctrl+N, asks for:
- a name, which they sign in with;
- a full name, if you want one shown;
- a password, typed twice; or tick Signs in without a password, and nothing is asked for on sign-in paths that permit credential-free access; SSH does not permit this;
- whether they are an Administrator, who may change the machine and its users and groups;
- a home folder and a shell, if not
/home/<name>and/bin/sh.
Create makes the user with all of it, or not at all: if anything is refused, such as a name already taken, nothing is made and the window says why.
Changing a user #
Select a local user, and their buttons are below their details:
- Edit changes their full name, home, shell and primary group. Their home folder is made at their first sign-in, and is not moved if you change it. Their primary group is the one their new files belong to, and they are in it whatever their other groups.
- Rename changes the name they sign in with. They keep their SID, so their files, permissions and groups stay theirs. Their home stays where it is; change it under Edit if it should follow.
- Set password gives them a new password. A user who signed in without one needs it from then on. Their sessions already signed in carry on.
- Sign-in chooses what they sign in with, and the ways they may sign
in.
- Signs in with: a password, an SSH key, either, nothing (no credential is asked for where credential-free sign-in is permitted, excluding SSH), or nothing accepted (every sign-in is refused, though nothing shows them as disabled). Adding a key doesn't change this, so a key added isn't used until this allows one.
- May sign in: as the machine's default allows (at the machine, on a remote desktop, over the network, and the rest a person uses), or only the ways you tick. To run a service lets the service manager start a service as them without any password, and is never part of the default.
- Add to group puts them in a local group, or in a built-in one whose
members are recorded here, such as
AdministratorsorUsers. Remove, beside a group they are in, takes them out of it. - Add, beside SSH keys, adds a public key: paste one line of their
.pubfile, such as~/.ssh/id_ed25519.pub. Ed25519 keys are accepted, and RSA keys of 3072 to 8192 bits. Without a label, the key's own comment is its label. Remove, beside a key, removes it. - Add, beside Claims, gives them a claim: its name, its type, and its values, one to a line. Each value is checked against the type, and one that doesn't fit is named. A claim of SIDs may name a user or group instead of giving a SID. Edit changes a claim's type and values, and Remove takes it away. A claim with no values is still a claim they have, which conditions treat differently from one they don't.
- Disable stops them signing in and keeps everything else; Enable lets them again.
- Delete removes them, after asking. Their SID is never given to anyone again, so files and permissions that name it name nobody; Disable instead is offered, and is usually what you want.
Changes to how a user signs in, to their keys, to the groups they are in and to their claims apply from their next sign-in. Disabling an account does not end existing sessions. Use Task Manager’s sign-out action when those must end; it can lose unsaved work. Session lifecycle explains the permissions and limits.
Groups #
In Groups, New group (or Ctrl+N) makes a local group, with a name and a description of what it is for. Anyone may read the description.
Select a local group, and its buttons are below its details:
- Edit changes its description; empty, it has none.
- Rename changes its name. It keeps its SID, so its members, and the permissions that name it, stay as they are.
- Add member puts a local user in it. Remove, beside a member, takes them out; a member whose primary group it is has to be given another under Edit first.
- Delete removes it, after asking, once nobody is in it. Its SID is never given to anything again.
A built-in group whose members are recorded here, such as
Administrators, has Add member and Remove too, but can't be
renamed, described or deleted: authd defines it, the same on every
Peios machine.
The local account store protects the last enabled administrator under its sign-in checks. Disabling or deleting the last one, taking away every way they could sign in to administer, or leaving them nothing to sign in with, such as removing the only key of one who signs in only with a key, is refused, and the window says why.
This last-administrator guard does not check whether a console, remote service or network path is reachable. Verify fresh access before closing a working administrator session.
Privileges #
What a person may do beyond what permissions allow them, such as back up
any file or shut the machine down, is this machine's local policy:
one record per user or group in the registry, which authd reads at
every sign-in. Each record may give privileges, an integrity
level, an owner for what they make, and a default DACL, the
permissions an object they make gets when it has no parent to inherit
from. See Assigning privileges
for what each does and how records combine.
Privileges, in the bar, lists the records: who each is for, how many privileges it gives, and its integrity. A service's record is named by the service. Select one to see it in full, each privilege with what it lets its holder do. Denied to everyone, the first row, is the privileges no record can give.
A user's page says what they would get in all if they signed in now: the
privileges, integrity, owner and default DACL that their own record,
their groups' records and Everyone's come to, and which records those
are. Signed in chooses the kind of sign-in, such as at the machine or
over the network, since a record may be for one kind of sign-in. A
group's page says what its record gives its members.
Changing them:
- New record (or Ctrl+N) makes a record, for a user or group
here, a well-known name such as
EveryoneorNetwork, or a SID. Give them their own record, on a user's page, and Give it a record, on a group's, do the same for them. - Edit changes what a record gives: tick its privileges, choose its
integrity, and type an owner or a default DACL, in SDDL. Anything left
empty, the record says nothing about. A default DACL that isn't SDDL
authdcould use is refused. - Delete removes a record, after asking. What it gave is no longer given.
- Edit, on Denied to everyone, chooses what no record may give.
Taking SeChangeNotifyPrivilege from Everyone or Administrators is
asked about first: without it, nobody who has it only from there can
pass through a folder, so no shell starts for them.
A record names a user or local group by its SID, since authd doesn't
ask lpsd what a name means; the window does this for you. What authd
warns about in the records is shown above the list, with why: a record it
ignores, such as one named by a name it doesn't know, or two records for
the same principal, such as Administrators and S-1-5-32-544, which
both apply and should be one.
On a machine with no policy at all, authd gives Everyone
SeChangeNotifyPrivilege and nothing more, and the list says so. The
first record saved makes the policy, with that in it, since once there
is a policy it is all there is.
Changes apply from each person's next sign-in.
Changes made elsewhere #
The lists are read again every few seconds, so a user, group or
privileges record added, changed or removed with lps or reg, or by
another administrator, appears without your doing anything. Refresh, or F5, reads everything at
once.
What you may see and change #
Anyone may see the users and groups: the identity socket that Principals Manager reads them from answers everyone.
What a user signs in with, and their SSH keys, are shown here only to an
administrator: lpsd holds them, and tells nobody else, except each user
about their own (see Changing your own keys). The privileges
records are shown to anyone signed in, and changed by whoever the
registry key's permissions let, which as shipped is Administrators.
Changing them needs BUILTIN\Administrators, enabled in your token, as
lps does. Without it, the window says You may look, but not change
anything, and why, and offers no changes. Built-in principals and
groups can't be renamed or deleted, and the window says so when one is
selected.
In a terminal #
lps list lists the users, lps show NAME shows one in full, and lps group list lists the local groups. getent group Administrators lists
who is in a group, and lps key list NAME lists a user's keys. lps add,
lps set, lps rename, lps password, lps policy, lps key, lps logon-types, lps claim, lps group, lps disable and lps remove make
the same changes as the window. See The lps command.
The privileges records are registry keys under
Machine\Generic\Authn\Policy, read and changed with reg, and token show --all lists what a session's token actually holds.
Assigning privileges
Peios / Using Peios / Manage local accounts and groups
A principal source says who someone is — their SID, their memberships, their POSIX identifiers. It never says how much this machine trusts them. Privileges and integrity are local policy: authd's alone, decided here, and there is no message with which a source could ask for one.
That policy lives in the registry:
Machine\Generic\Authn\Policy
authd reads it at every logon, not once at startup. A policy change takes effect the next time someone signs in, rather than the next time you restart the one daemon on the system that is most disruptive to restart.
Principals Manager shows and edits it from the desktop. Its Privileges list is the records on this page, and each user's page says what they would get in all, worked out by the same code authd mints from.
One record per principal #
Each subkey is a principal, and holds everything this machine grants them:
Machine\Generic\Authn\Policy
DeniedPrivileges REG_MULTI_SZ ["SeDebugPrivilege"]
\Everyone
Privileges REG_MULTI_SZ ["SeChangeNotifyPrivilege"]
DefaultDacl REG_SZ "D:(A;;GA;;;S-1-3-4)(A;;GA;;;SY)(A;;GA;;;BA)"
\Administrators
Privileges REG_MULTI_SZ ["SeBackupPrivilege", "SeRestorePrivilege"]
Integrity REG_SZ "High"
Owner REG_SZ "Administrators"
Keyed by principal rather than by privilege on purpose. One key shows the totality of a principal's authority — reg ls on \Administrators answers "what can an administrator do on this machine?" completely. Authority scattered across twenty per-privilege values is authority nobody audits: a right granted somewhere unexpected does not surface when you look at the principal, and you would have to know to check every other place.
It is also the only shape that holds more than privileges. Integrity, the default owner, the default DACL and the logon types a peer may originate all live on the same record.
Naming a principal #
A subkey's name is either a well-known name or a literal SID:
\Administrators
\S-1-5-21-2847362817-1094533892-3310298447-1000
Names are matched case-insensitively. The recognised ones are SYSTEM, Everyone, Authenticated Users, Administrators, Users, Guests, Local Service, Network Service, Local (everyone signed in on this machine), and the logon types Interactive, Remote Interactive, Network, Batch, Service and Anonymous.
Two limits are worth knowing before you hit them:
BUILTIN\Administrators cannot be a subkey name. A backslash is the registry's path separator, so the qualified spelling is unrepresentable. Write the bare name.
A local group's name does not resolve. authd cannot know what developers means without asking lpsd, and policy must never depend on a principal source being up — that would make "what may this principal do" unanswerable exactly when a source is broken. Name local groups by SID, which lps group list will show you.
A subkey that is neither a known name nor a parseable SID is ignored with a warning. It is worth checking for that warning after editing: a record naming nobody sits in the key looking authoritative and applying to no one.
Two subkeys naming one principal, such as \Administrators and \S-1-5-32-544, both apply, and authd warns about them. Their privileges add up like any two records', but for integrity, Owner and DefaultDacl whichever is read first wins, which is an order nobody chose. Merge them into one. Principals Manager shows the warning above its Privileges list.
If the key exists, the key is the whole policy #
This is the most important behaviour on the page.
authd carries a compiled-in floor, but it applies only when the key is absent entirely. It is not merged in value by value. So a policy you write is the complete policy: anything not granted below is not granted.
| State | What a principal gets |
|---|---|
| Key absent | The compiled floor — Everyone gets SeChangeNotifyPrivilege, and nothing else |
| Key present | Exactly what the records say |
| Key present but unreadable | Nothing, and a loud log line |
The alternative — a compiled default that each record replaces — fails in the wrong direction. An administrator who writes one record believing they have locked the machine down would still be handing out whatever a compiled table said for every principal they did not mention, from a table they cannot read. That is a security failure, and a silent one. Failing towards less privilege makes things stop working, which is far easier to diagnose than authority you did not know you were granting.
An unreadable key is treated as granting nothing rather than as absent, for the same reason: reading it as absent would silently restore every privilege an administrator may have deliberately removed, at the one moment nobody can check.
What a machine ships with #
The defaults are registry data, not compiled in — authd-policy.reg, shipped to /usr/share/regim/ and applied if the image opts in. So they can be read, edited, and replaced wholesale by an image that wants a different policy, rather than being invisible inside a binary.
A stock machine grants:
| Principal | Privileges | Integrity |
|---|---|---|
Everyone | SeChangeNotifyPrivilege, SeCreateSymbolicLinkPrivilege | (Medium, by default) |
Administrators | the operational set below, plus both of the above | High |
The operational set is SeBackupPrivilege, SeRestorePrivilege, SeShutdownPrivilege, SeRemoteShutdownPrivilege, SeSystemtimePrivilege, SeSecurityPrivilege, SeLoadDriverPrivilege, SeImpersonatePrivilege, SeIncreaseQuotaPrivilege, SeIncreaseBasePriorityPrivilege and SeProfileSingleProcessPrivilege.
SeDebugPrivilege is deliberately not granted. It writes to any process regardless of integrity label, so it is the single privilege that most directly defeats the integrity boundary, and ordinary administration does not need it. Add it when a machine genuinely does.
Three privileges are never granted by the shipped policy and should not be added: SeCreateTokenPrivilege mints any identity without going near authd, which would make every other control here decorative; SeTcbPrivilege and SeAssignPrimaryTokenPrivilege belong to the trusted computing base.
Note that Administrators is granted SeChangeNotifyPrivilege directly, not only through Everyone. Privileges accumulate across every SID on a token, so that repetition is insurance: if someone edits the key and drops the Everyone record, ordinary users lose the ability to traverse a directory — but administrators keep working and can repair it.
How the values compose #
Privileges accumulate. A token holds the union of every record naming a SID it carries — the principal's own, and each of their groups. A group marked USE_FOR_DENY_ONLY contributes nothing.
DeniedPrivileges wins. Listed on the Policy key itself rather than on a record, it is applied after the union, so no record you have not read can defeat it. It is on the parent key so it cannot collide with a principal who happens to be called Denied.
Integrity, Owner and DefaultDacl are single values, so they cannot accumulate. The principal's own record wins outright; otherwise the groups decide:
- Integrity takes the maximum across the groups that name one.
OwnerandDefaultDaclhave no ordering to compare, so two groups naming different values is a misconfiguration: it logs a warning and falls back to the default rather than picking by whichever the registry enumerated first. Two groups naming the same value is not a conflict.
The user's record winning outright is what makes a principal possible to lower. Under a plain maximum, a guest whose own record said Low would still come out Medium the moment any group they belonged to named Medium. The consequence is worth stating plainly: a group cannot impose an integrity floor on a member. If Administrators names High and a member's own record names Low, that member gets Low.
Writing each value #
Privileges — REG_MULTI_SZ #
Full ABI names, SeBackupPrivilege rather than SeBackup, matched case-sensitively. A name this build does not recognise is dropped with a warning and the rest of the list still applies — a policy written for a newer Peios should not lose the privileges it spelled correctly.
An empty list is meaningful: it grants nothing, and is different from the value being absent.
Integrity — REG_SZ or REG_DWORD #
A tier name, matched case-insensitively:
| Name | Value |
|---|---|
Untrusted | 0 |
Low | 4096 |
Medium | 8192 |
High | 12288 |
System | 16384 |
Or a raw REG_DWORD. The kernel compares integrity numerically and any value is legal, so the numeric form reaches levels between the tiers — 8193 sits just above Medium. Use the name unless you need that.
A principal no record names gets Medium. That default is compiled in and is not affected by the key existing, because unlike a privilege it is not a grant: every token must carry some level to be valid at all.
Owner — REG_SZ #
Which principal owns objects this token creates. Names a principal, exactly like a subkey name does; authd converts it to the index the token actually carries, which is a number meaningful only within one token and different on the next logon.
Absent — the ordinary case — means objects are owned by their creator.
The case this exists for is a shared administrative estate: objects an administrator creates being owned by Administrators rather than by the individual, so they remain manageable when that person's account goes away.
DefaultDacl — REG_SZ #
The DACL an object gets when this token creates it with no parent to inherit from, written as SDDL. The shipped policy names it on the Everyone record:
D:(A;;GA;;;S-1-3-4)(A;;GA;;;SY)(A;;GA;;;BA)
That is the owner, LocalSystem and BUILTIN\Administrators, full control, and nobody else — the same shape authd stamps on a home directory. S-1-3-4 is OWNER RIGHTS, which the access check resolves against the object's owner at the time of the check. You can also write it as the alias OW. It has to be OWNER RIGHTS rather than CREATOR OWNER: the kernel copies a default DACL onto a new file verbatim and substitutes no placeholders, so a CREATOR OWNER ACE would name nobody.
The value is a fallback and is reached rarely. Whenever the parent has inheritable ACEs the child takes those and the default DACL is not consulted (see Inheritance), and every filesystem root on Peios carries inheritable ACEs. What reaches it is an object with no parent, such as an abstract socket, or a child of a container whose descriptor was written without inheritable ACEs. There is no kernel fallback beneath it: a token with no default DACL leaves such an object with a null DACL, which grants every caller everything.
SDDL rather than raw bytes because the whole argument for policy living in the registry is that an operator can read it. Conditional ACEs are preserved, so D:(XA;;GA;;;WD;(@USER.Department == "Engineering")) works and keeps its condition.
A value that does not parse is dropped with a warning and the token is minted with no default DACL — a typo costs the customisation, not the session, but it does leave the null-DACL case open until it is corrected.
LogonTypes — REG_MULTI_SZ #
The logon types this principal may originate — request of authd over /run/logon.sock on somebody else's behalf. This is a statement about the asker, not about the account being signed in: login originates Interactive logons for whoever is at the console, a graphical greeter would too, and a web console would originate Network ones.
\S-1-5-80-…-… (a greeter's service SID)
LogonTypes REG_MULTI_SZ ["Interactive"]
The names are Interactive, Network, Batch, Service, NetworkCleartext, NewCredentials and RemoteInteractive, matched case-sensitively; an unrecognised name is dropped with a warning and the rest still applies.
The proposed logon type is not cosmetic. It selects the group SIDs the minted token carries — Interactive puts S-1-5-4 on it, RemoteInteractive puts S-1-5-4 and S-1-5-14 — and policy records can key on those SIDs, so the type must be what the peer is permitted, never merely what it proposed. A greeter granted ["Interactive"] cannot mint itself a network-shaped token, nor the reverse.
A principal with no LogonTypes originates nothing. SYSTEM is the exception and is permitted every type unconditionally — it administers this very key, so a registry constraint on it would enforce nothing, and authd does not pretend otherwise.
An empty list revokes: it is "may originate nothing", distinct from the value being absent only in that it says so on the record.
LogonSocketDescriptor — REG_SZ, on the Policy key itself #
The security descriptor /run/logon.sock carries, as SDDL, read once at authd startup. The built-in value gives SYSTEM and Administrators full access, and gives every authenticated principal connect access alone, so that anyone signed in can change their own password and sign themselves out:
O:SYG:SYD:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;0x100082;;;AU)
Connect access is FILE_WRITE_DATA; 0x100082 grants it with stat and sync. Reaching the socket does not let a peer originate a logon: that is the LogonTypes record above, and a peer without one originates nothing however it connected. So admitting a new originator that is not already an authenticated principal takes both edits, and one that is takes only the record.
A replacement descriptor that leaves out the AU entry turns off self-service password change, and signing yourself out, for every principal it leaves out.
SessionEndSecurity — REG_SZ, on the Policy key itself #
Who may sign somebody else out: end a logon session that is not their own. authd does the work when asked over /run/logon.sock (Session lifecycle); this value, as SDDL, is the security descriptor it access-checks the caller's token against, for the one right 0x1 (every generic right maps to it). It is read on every request, so an edit applies to the next one. With the value absent, the descriptor is:
O:SYG:SYD:(A;;0x1;;;SY)(A;;0x1;;;BA)
— SYSTEM and Administrators. Granting 0x1 to a group lets its members sign anybody out.
Signing yourself out needs nothing here: a person may always end their own session. And nothing here lets anyone end SYSTEM's session, Anonymous's, or a service's — a service is stopped through the service manager.
A value that is present but unusable — not a REG_SZ, empty, or not valid SDDL — lets nobody sign anybody else out until it is fixed or removed, and authd logs why on each request. It does not fall back to the default, which might grant more than the site meant.
Locking a machine down #
To forbid a privilege regardless of what any record says:
Machine\Generic\Authn\Policy
DeniedPrivileges REG_MULTI_SZ ["SeDebugPrivilege", "SeLoadDriverPrivilege"]
That is one edit, in one place, that a record you have not read cannot defeat. Removing the privilege from each record individually relies on having found them all.
What this cannot express yet #
Account sign-in restrictions are a separate control. These privilege records decide what a session gets. The account store also supports lps logon-types to limit which kinds of sign-in an account may use, and lps disable to prevent new sign-ins altogether. LogonTypes above constrains what the asking service may request; it does not replace the account restriction. Follow the account command reference for supported types and the last-administrator guard.
Privilege constants do not establish assignment support. Earlier privilege guidance described SeTakeOwnershipPrivilege, SeRelabelPrivilege and SeSystemProfilePrivilege as absent from the ABI and therefore impossible to assign. The current generated KACS ABI defines constants for all three. That contradicts the ABI-absence claim, but does not by itself establish the accepted privilege-name table in authd or a service configuration, or a release in which assignment became available. Confirm support for the deployed build and inspect the actual token rather than relying on either blanket claim. Even when take-ownership is available, changing the owner does not guarantee DACL repair.
Seeing what a token actually got #
$ token show --all
[privileges] lists what the token holds and integrity shows the label. A privilege the tool cannot name appears as <privilege bit N> rather than being omitted, so the list is always complete even when the name table is not.
To see what a sign-in would get before anyone signs in, open the user in Principals Manager: it works the policy out for them, for each kind of sign-in, and says which records it came from.
See also #
- Privileges — the model these records feed.
- The token command — reading the privileges and integrity a live token actually carries.
- Authorities and principal sources — the accounts these records apply to.
Task Manager
Peios / Using Peios / Threads and processes
Task Manager shows what is running on the machine now: every process, which is a program that is running, with how much of the processor and memory it is using, what it belongs to, and who it runs as. What is defined to run, rather than what is running, is Services Manager's.
Start it from the launcher: Task Manager.
It looks at the machine as you, so it shows exactly what you may see of each process, and says what you may not, and why. It offers to change something only where you may.
Processes, Performance, Jobs and Signed in, in the bar, switch between its four views.
Find the right process before acting #
- Open Processes. Use Find for a name, PID, person or service, or sort CPU or Memory to investigate resource use.
- Select the process. Check its command, who it runs as, what started it, and the service or job it belongs to. Some details may be hidden by permissions.
- Choose the scope of the action. To stop a service and everything it started, use Stop service; to stop a submitted job, use Stop job. Use End process when you intend to end only that process.
- Read the confirmation and refresh the view to check the result. Ending a service's main process may cause peinit to restart it.
A process is one running program. A service is a persistent definition managed by peinit, and can start a new process after the old one ends. A submitted job is work someone asked peinit to run and watch. For those managed lifetimes, see Services and jobs. For process states and terminal signals, see Process lifecycle.
The processes #
Each process is listed with its name, its PID (the number the system knows it by while it runs), who it runs as, and its share of the processor (CPU) and the memory it holds. The CPU share is of the whole machine, since the last look, and is filled in from the second look on. The bar at the bottom gives the totals.
Group by chooses how they are arranged:
- None, which it opens with, lists them all together.
- Service puts each process under the service or job it belongs to, as peinit, the service manager, started it. A service's heading says its state; a job, which is a program someone asked peinit to run and watch, such as a person's desktop session, says who it runs as. Not in a service holds peinit itself.
- Person puts each under the person or service it runs as.
Name, PID, CPU and Memory, at the top of the list, sort by that. Find narrows the list to processes whose name, PID, person or service contains what you type.
The kernel's own threads, which no person runs, are left out; tick Kernel threads to show them.
The list is read again every two seconds. Refresh, or F5, reads it at once.
A process in full #
Select a process to see it in full on the right: what it is doing, how long it has run, its CPU, memory and threads, what started it (select it to go to it), the command it was started with, the service or job it belongs to and which part of it this is, who it runs as, the signed-in session it is part of, and its integrity.
Protection says whether the process is protected: signed as part of Peios, which closes it to every process not signed at its level, however much authority that process has. Mitigations are the hardening the process runs with, such as no memory being both writable and executable, each with what it does. See The process security block.
Ending a process, or stopping its service or job #
Below a process's details are what you may do to it:
- End process asks first, then asks the process to end. One that hasn't ended after five seconds is offered End it now, which ends it at once, without its finishing what it is doing. The process is held onto from the moment you ask, so another process given the same PID later can't be ended by mistake.
- Stop service, for a process that belongs to a service, asks peinit to stop the service, and everything it started. It stays stopped until it is started again. Ending a service's main process instead counts, to peinit, as the service crashing, and it may start it again; the window says so before you do.
- Stop job, for a process in a job, asks peinit to stop the job.
Each is there only where you may do it: End process where the process's permissions let you end it, and Stop where peinit says you may stop that service or job. Where you may not, the pane says why instead. A protected process can't be ended by any program not signed at its level.
Performance #
Performance shows the machine as a whole: how busy its processors are, how much of its memory is in use, and how fast each disk and network interface is moving data. Each is listed down the side with a small graph; select one to see it in full, with a graph of the last two minutes, newest at the right.
- CPU graphs how much of the processors' time is in use, and how much of that is the kernel's own work. Each processor gives each logical processor a graph of its own. Below are the processor's model and speed, the number of processes and threads, how long the machine has been up, and the load average: how many threads were running or waiting to, on average over the last 1, 5 and 15 minutes.
- Memory graphs how much is in use, and shows what all of it is made of: in use, cache the kernel gives back when programs need it, and free. Available is what programs could be given now. Committed is what programs have been promised, against how much may be.
- Each disk graphs how much of the time it was busy (its active time), and how fast it read and wrote. Its capacity, and what it has read and written since the machine started, are below.
- Each network interface graphs how fast it is receiving and sending, in bits a second as networks are measured. Its state, link speed and hardware address are below; its addresses and settings are Network Manager's.
The figures are the kernel's own, which anyone may read, so everyone sees the same view. Speeds are worked out between one look and the next, every two seconds. Nothing is kept once the window closes.
Jobs #
A job is a program someone asked peinit to run and watch, outside any service: a person's desktop session is one. Jobs lists them: what each is, its state, who it runs as, and how far it says it has got. A job in full shows what it last said of itself, its progress, its program and process, who it runs as and who asked for it, when it started and ended, and how it ended. Stop job is there where you may stop it.
peinit lists a job only to those who may see it: by default, whoever asked for it, and Administrators. The others are left out, and the window says so. A job that has ended stays in the list for a minute.
Who is signed in #
Signed in lists each session, which is one sign-in: who, how (at the machine, on the desktop, over SSH, or over the network), since when, and how many of its processes you can see. A session in full lists its processes, each of which leads to it, and its desktop session, if it has one. Services sign in too, each in a session of its own; tick Services' sessions to show them.
The full list is the kernel's, which only Administrators may read. Anyone else sees the sessions their own processes are in, and the window says so.
Signing someone out #
Sign out, in a session's details, ends the session: authd ends every process running in it, asking each to end and, after a few seconds, ending any that haven't. The window asks first, and says so when the session is its own. What came of it is said afterwards, including any process authd couldn't end, which keeps the session open.
You may sign out your own sessions, and anyone's the machine's permissions allow, which as shipped is Administrators. Where you may not, the window says why. A service's session ends when its service is stopped, and the kernel's own sessions never end. See Ending a session.
What you may see #
What you may see of a process is its own permissions' to say:
- Its name, CPU, memory and what it belongs to are visible to everyone, by default.
- Who it runs as, and the command it was started with, are visible to the person it runs as and to Administrators. Anyone else sees the process without them, and a note above the list says how many processes that is.
- A protected process, such as peinit or authd, is closed to everyone, Administrators included: only processes signed at its level may look into it. It is still listed, with its protection, and a note above the list says how many there are. Where peinit can say which service a protected process is, it is listed under that service.
A process in a job you may not see is listed under A job, and says so.
In a terminal #
The toolset documented here does not yet include ps; use Task Manager for
the process list. The documented terminal tools cover these narrower tasks; replace PID, NAME and ID with
the process, service or job/session you have checked.
| Task | Command |
|---|---|
| Read a process's protection, mitigations and GUID | logonse psb --pid PID |
| List signed-in sessions and their visible processes | logonse list |
| Read a service's status and main process | svctl status NAME |
| Stop a service | svctl stop NAME |
| List submitted jobs | svctl job list |
| Read a submitted job's status | svctl job status ID |
| Stop a submitted job | svctl job stop ID |
| Sign a session out | logonse end ID |
| Ask one process to terminate | kill PID |
The same access restrictions apply to terminal tools. logonse list can show
only a partial view; a missing entry is not proof that nothing is running.
Read Controlling services for
service and job commands, and End, suspend, or resume a process
before sending signals.
Threads and processes
Peios / Using Peios / Threads and processes
To see what is running, open Task Manager from the launcher. Find a process by name or PID, check its CPU and memory, and select it to see who it runs as and which service or job it belongs to. Start with the Task Manager guide for the controls and the limits on what you can see.
A process is a running program, together with its memory, open resources, and the state the system keeps for it. A thread is one line of execution inside that process. Every process starts with one thread and can create more; its threads share memory and open resources while each makes progress through its own work.
The actors of the system #
Every action, such as opening a file or sending data, is performed by a thread. The system checks whether that thread is allowed and records which thread acted. Each thread acts as a person, a service, or the system itself; there is no "nobody" state. Its token carries that identity, and a new child process begins with its parent's identity.
The thing to stop depends on what you want to achieve:
| You want to stop | Use | Why |
|---|---|---|
| One running process | Task Manager's End process, or kill PID | Asks that process to end; unsaved work may be lost. |
| A managed service and its processes | Stop service, or svctl stop NAME | Ending only its main process can look like a crash and cause a restart. |
| A submitted job | Stop job, or svctl job stop ID | Asks peinit to stop the managed job. |
A service is a persistent definition; a process is one thing running now. A submitted job is work someone asked peinit to run and watch. A terminal's job-control group is another use of "job", described in Process relationships and job control.
What a process has #
| A process has | What to look for |
|---|---|
| Private memory | Its working space, shared by its own threads and separate from other processes. |
| Open resources | Files, connections, and other resources it holds open. |
| An identity | Who it runs as; its token controls what it may reach. |
| A place in a family tree | Its parent, shown by a parent PID (PPID). |
| A lifecycle | Whether it is running, sleeping, stopped, or has finished. |
| One or more threads | The lines of execution doing its work. |
The PID identifies a process while it exists and can be reused afterwards. The Process GUID identifies that one process permanently. Neither tells you who it runs as. For a lasting event record, use the GUID; before acting on a PID, check the current process again.
The Process Security Block (PSB) holds the GUID, protection, hardening and the process's own permissions. Being able to see a process does not mean you may stop or inspect all of it.
Where to start #
- Task Manager: find a process, investigate resource use, and choose a process, service, or job action.
- Process lifecycle: interpret states and safely end, suspend, or resume a process.
- The process and thread model: understand threads, PIDs, GUIDs, and identity.
- Creating processes: understand why starting a program adds a process, and why a process can change programs.
- Process relationships and job control: understand parents, terminal process groups, and sessions.
- The Process Security Block: understand protection and access refusals.
The syscall and threading-library contracts are in the developer Process runtime reference.
The process and thread model
Peios / Using Peios / Threads and processes
A process row in Task Manager represents a running program; its thread count tells you how many lines of execution it contains. Use the PID to find it now, the Process GUID to match historical records, and its token identity to understand who it runs as.
A running program has two separable parts: the threads, which run, and the process, which contains them.
The thread is the part that runs — one line of execution, a single sequence of steps the system works through. A machine with several cores runs that many threads genuinely in parallel, one per core; a single core runs many threads by switching between them. Which thread runs, on which core, and for how long is decided by the system's scheduler — a subject of its own. Some tools and logs call a thread a task; it is the same thing.
The process is the boundary around a group of threads. It does not run — it contains. A process holds the things its threads share: their private memory, their open files and connections, and their identity. Every thread inside one process sees the same memory and the same open resources; threads in two different processes do not. That separation is what stops one program from reaching into another's memory or files.
The thread does the work; the process is the shared space the work happens in. A program that does one thing at a time is a process with a single thread. A program that does several things at once is a process with several threads, all sharing one space.
Why the thread is the more basic idea #
Threads, not processes, are the fundamental unit: what the system runs is threads, and a "process" is the answer to the question "what does this thread share, and with whom?" Bundle some threads together so they share one memory, one set of open resources, and one identity, and that bundle is a process. Give a thread its own fresh, separate memory instead of sharing an existing one, and you have made a new process.
This matters because it explains creation. Making a new thread and making a new process are the same mechanism with a different answer to "how much does the new line of execution share with the one that made it?" Share everything, and the result is another thread in the same process. Share nothing, and the result is a new process. Everything in between is possible too. How the operation is actually performed is covered in Creating processes.
What threads share, and what they don't #
Within one process, every thread shares:
| Shared across threads | What that means in practice |
|---|---|
| The process's memory | One thread can read and change data another thread is using — cooperation is cheap, but concurrent access must be coordinated. |
| The open resources | A file one thread opens is usable by all of them. |
| The identity | By default every thread acts as the process's identity. |
Each thread keeps its own place in its sequence — which step it is on right now. That is the point of having more than one: each thread makes progress through its own work independently of the others.
There is one exception to the shared identity. A single thread can temporarily act as a different principal — for example, a service handling a request can act as the requester for that one piece of work, then revert. Only that one thread is affected, and only until it reverts. This is covered in Impersonation; here it is enough to know that "every thread in a process shares the process's identity" is the default, not an unbreakable rule.
Naming a process #
The system gives each process a number called its process ID, or PID, so that people and other programs can refer to it — to inspect it, signal it, or wait for it to finish. Threads are individually identifiable too, by a thread ID — see the developer thread operations reference.
A PID is short and convenient, but it only names a process while that process exists. Once a process ends, its PID can later be reused for a completely different process. A PID tells you which process is which right now, but it is useless as a lasting record — two unrelated processes can hold the same PID at different times.
For that, every process also has a Process GUID (globally unique identifier): a name that is never reused, assigned to each process when it starts and fixed for the rest of its life. Where the PID is the convenient short handle for the moment, the Process GUID is the permanent one — it identifies that one specific process and no other, even long after it has ended. This is what lets a record of something that happened on the system name exactly the right process, with no chance of confusing two that happened to share a PID.
Both the PID and the GUID refer to a process. Neither says who the process is acting as — that is a separate question, answered by its identity rather than by any number or name.
Where to go next #
For interpreting a state or safely sending a signal, continue with Process lifecycle. To understand what a new program starts with, read Creating processes.
Creating processes
Peios / Using Peios / Threads and processes
Starting a command or application can create new processes. In Task Manager, select one to see its parent, command and service or job. A new process gets a new PID and Process GUID, but a running process can also change the program it runs without changing either identifier.
These two cases explain most changes you see in the process list. You do not need to call the creation APIs to start an application from the launcher or run a command in a terminal.
Splitting: one process becomes two #
A running process can create a near-copy of itself. The original is the parent; the new process is its child. This is called fork.
The child gets private memory copied from the parent, copies of its open resources, and the same identity. Changes to one process's private memory do not change the other's. The child has its own PID and Process GUID, and some per-process state starts fresh. Identity inheritance is covered in Token lifecycle; the precise inheritance list is in the developer creation reference.
Replacing: same process, different program #
A process can replace its current program with another, called exec. Its PID and Process GUID stay the same, and it retains its identity; the previous program's memory and code are replaced. A changed program name therefore does not by itself mean a different process has taken over that PID.
For the identity rules, including programs marked to run as another principal, see Token lifecycle. The exact preserved and reset state is in The exec family.
Putting them together #
Launching a new program commonly combines the two steps: create a child, then replace the child's program. Spawn combines this pattern for callers. Thread creation uses the same underlying creation mechanism with resources shared instead of private; the general mechanism is called clone.
For operating the system, check the resulting process and what manages it. If a service keeps creating replacement processes, inspect the service's status and supervision policy, rather than repeatedly ending each new PID. API details belong in the developer process creation reference.
Getting hold of the new process #
A PID can be reused after a process ends. Recheck the current name, command, owner and service or job before signalling a PID from a terminal, especially if it came from an old log or an earlier inspection.
Programs can hold a pidfd, a file-descriptor handle tied to one specific process, obtained at creation or for a process that already exists. Unlike a bare PID, that handle cannot later refer to a different process; it can be used to wait for the process or signal it. Task Manager holds onto the process from the moment you ask to end it, protecting that action against PID reuse. For code that needs this guarantee, see Process handles.
Where to go next #
Process lifecycle explains states, termination, and cleanup. If you are implementing a launcher or threading library, use the developer Process runtime reference.
Process lifecycle
Peios / Using Peios / Threads and processes
Read a process's state before deciding what to do. A sleeping process can be healthy, a stopped process is paused rather than finished, and a zombie has already ended. Use Task Manager to check the process, its parent, and whether a service or job manages it.
The states it passes through #
While it exists, a process is in one of a handful of states, and the system moves it between them. Most of the time it is either running — executing, or ready to run the moment a core is free — or sleeping, paused while it waits for something to happen, such as data to arrive or a timer to fire. A sleeping process uses no processor time until what it is waiting for is ready; most processes spend most of their lives asleep.
A few states are worth knowing by name:
- Stopped — suspended (for example by job control when a job is paused at the terminal); it does nothing until told to resume.
- Traced — stopped under the control of a debugger that has attached to it, so the debugger can inspect and step it.
- Uninterruptible sleep — waiting on something the system will not interrupt, almost always a brief piece of disk or device I/O. A process here cannot be woken or even killed until the operation finishes; it normally lasts an instant, and a process stuck in it is a sign that hardware or a filesystem is stuck too.
- Zombie — finished, but still held as a result waiting to be collected (see below).
Once its result has been collected the process is gone, removed from the system entirely.
End, suspend, or resume a process #
First check the scope. Stop service or svctl stop NAME asks peinit to stop
a service and its processes; Stop job or svctl job stop ID stops a
submitted job. Ending a service's main process directly can count as a crash
and trigger a restart. See
Controlling services.
For one process, Task Manager's End process asks it to end, then offers End it now after five seconds if it is still there. The immediate option does not let the program finish its work. Task Manager holds the process so a reused PID cannot redirect that action.
From a terminal, use kill. Replace 4821
in these examples with the PID you have just checked:
$ kill 4821 # send TERM: ask the process to terminate
Give it time to finish, then refresh Task Manager and check the result. Only
if TERM has failed, and losing unfinished work is acceptable, consider:
$ kill -KILL 4821 # force termination without application cleanup
To pause a process and later let it continue, use the documented stop and continue signals:
$ kill -STOP 4821 # suspend
$ kill -CONT 4821 # resume
Suspension is not completion: the process does no work until resumed. Check that pausing it will not interrupt work other programs or people depend on. This targets one process, not a whole service or terminal pipeline.
TERM and KILL require permission to terminate the process; STOP and CONT
require permission to suspend or resume it. Protected processes also require
sufficient program trust. Being an administrator or sharing a terminal
session does not bypass protection. See
Process permissions.
A successful kill exit status means the signal was sent, not that the
process has finished. Exit status 1 means a signal could not be sent, for
example because the process no longer exists or you may not signal it. Check
the refreshed process or managed-service/job status before declaring it stopped.
Two ways a process ends #
A process ends in one of two ways.
Most often it ends on its own: it finishes its work and stops. A process that ends this way leaves behind an exit status — a small record of how it went, usually just "succeeded" or "failed" (and, on failure, a number giving a rough reason). The act of a process ending itself is called exit.
The other way is that a process is ended by a signal from outside. Its exit status records the terminating signal. This is different from the stopped state above, which means suspended and able to resume.
Either way, a record of the process remains until its result is collected.
Collecting the result #
Whenever a child changes state — ends, is stopped, or resumes — the system
sends its parent a signal, SIGCHLD, prompting it to check. The parent
then reads the child's exit status. This is called waiting for the
process.
Between the moment a process ends and the moment its parent collects the result, the finished process is a zombie: no longer running, but with the system still holding its exit status so the parent can read it. Despite the name, a zombie is harmless — it does no work and uses almost nothing; it is a result waiting to be collected. The instant the parent collects it, the zombie is gone for good.
If a parent never collects, zombies accumulate — finished processes whose results no one read. That is a fault in the parent, not normal behaviour. Inspect the parent and its service or job; a zombie has already finished, so sending it another termination signal does not make the parent collect it.
A parent holding a pidfd for its child can wait on it directly — the dependable way to be told the exact moment the child ends.
The exact calls — every wait variant, and how an exit status is read — are in the
developer process lifecycle reference.
When the parent ends first #
A parent does not always outlive its children. If a parent ends while one of its children is still running, that child becomes an orphan.
Orphans are adopted automatically, and keep running with a new parent. The system's first process — PID 1, which on Peios is peinit — is the final adopter when no nearer subreaper takes responsibility. When the adopted child ends, its new parent collects the result.
PID 1 is the catch-all. A subreaper can instead adopt orphaned descendants within its own subtree; service and container managers use this to retain responsibility for the work they launched. For the programmatic contract, see Orphan adoption and subreapers.
What the system cleans up #
When a process ends, the system reclaims what it was using. Its private memory is freed and its open files and connections are closed.
It also releases its identity. A process acts as a principal, carried on its token; when the process ends, its hold on that token is released. If other processes were sharing the same identity — for example, siblings from the same login — the identity lives on for them, and the system clears it away only once nothing is using it any more. The details are in Token lifecycle.
The process's PID becomes available for the system to assign to a future process. Its Process GUID is not reused — it remains a permanent marker of that one process, so records of what it did still point unambiguously back to it long after it is gone.
Where to go next #
Processes are arranged into a family tree and grouped together in ways that matter for running them. That is Process relationships and job control.
Process relationships and job control
Peios / Using Peios / Threads and processes
Use a process's parent to trace what started it. Use its service or submitted job to choose a managed stop. At a terminal, use the process group to understand which commands receive keyboard signals together. These are different relationships; stopping one PID need not stop all the work it belongs to.
Processes are related by descent, forming a family tree, and by grouping, which lets terminal job control handle several together.
The family tree #
Every process was started by another, so every process has a parent: the process that created it. A process can refer to its parent by the parent's PID, sometimes called the PPID (parent process ID).
Follow the parents upward and they all lead back to the same place — PID 1, the first process, which the system starts at boot. Every other process descends from it, directly or through a chain of parents, so the processes on a running system form a single family tree rooted at PID 1. This is the same tree that makes orphan adoption work: when a process loses its parent, PID 1 takes over the role, as covered in the lifecycle.
A few processes at the very root of the tree are special. PID 1 is the first
process started at boot — on Peios, peinit — and the ancestor of all the rest; the
system protects it, refusing to kill it with any signal it has not chosen to handle,
because losing it would bring down the whole tree. Beneath the userspace processes
the kernel runs threads of its own: PID 0 is the idle process (what a core runs
when it has nothing else to do), and PID 2, kthreadd, is the parent of every
kernel thread. The kernel's threads — among them the worker threads (kworker)
that run deferred work and the threads that service hardware interrupts — appear in
a process listing as the system's own work rather than as programs loaded from disk.
Process groups #
Several processes often do one job together. In a pipeline — the output of one command feeding into the next — each command is its own process, but they form one job: you stop, pause, or resume the whole job at once, not a piece at a time.
To make that possible, related processes are collected into a process group. A process group is a set of processes treated as a unit, so that an action — most often a signal, like the one that stops a job — can be delivered to all of them together instead of one by one.
A terminal job, such as a pipeline, is not the same thing as a submitted
job watched by peinit. Task Manager's Jobs view and svctl job operate on
submitted jobs; terminal job control operates on process groups. See
Jobs and operations.
Sessions and the terminal #
Process groups are themselves collected into a larger unit: a session, tied to the terminal the work is running at.
A terminal can only take input for one job at a time, so within the session one process group is the foreground group: the one connected to the keyboard, the one typing reaches, the one a stop-or-interrupt keystroke acts on. Any other groups are in the background, running without holding the keyboard. Moving a job between foreground and background — and starting, stopping, and resuming jobs — is job control.
Ctrl-C sends the terminal's interrupt signal to the foreground process
group; Ctrl-Z sends its stop signal. A command such as kill -STOP PID
targets the named process instead. Resume that process with kill -CONT PID
when permitted; see suspending and resuming.
Keyboard signals and explicit kill commands take different permission paths,
as explained in Signals in detail.
A different kind of "session" #
The word "session" has two meanings here, and they are unrelated:
- A job-control session, described above, organises processes at a terminal — which groups share a terminal, which one is in the foreground. It says nothing about who anyone is.
- A logon session records one sign-in — a single authentication event — and ties together every identity token that came from it. It is part of the identity model, covered in Logon sessions.
They often line up in practice — signing in at a terminal creates a logon session, and the processes run there share a job-control session — but they are separate things answering separate questions: "which processes share this terminal?" versus "where did this identity come from?"
Where to go next #
Beyond its place in the tree and its groups, every process carries a bundle of its security-related settings — its Process Security Block. That is The Process Security Block.
The Process Security Block
Peios / Using Peios / Threads and processes
When Task Manager hides details or refuses End process, inspect the
process's Protection and Mitigations. In a terminal,
logonse psb --pid PID shows its protection,
hardening and Process GUID without changing them.
A process's identity — who it is acting as — is carried on its token, and can change moment to moment, since a thread can impersonate another principal. A process has a second aspect that is independent of identity: what it is. What program is it running? How trusted is that program? How hardened is it against attack? Who is allowed to operate on it?
Those facts are gathered in one place — the Process Security Block, or PSB. Every process has one. Where the token answers who this process is acting as, the PSB answers what this process is. Unlike the token, the PSB never changes when a thread impersonates: impersonation changes who, never what.
What the PSB holds #
- Its permanent name. The process's Process GUID — the never-reused identifier — lives on the PSB.
- How trusted its program is. When a process starts running its program, the system checks the program's cryptographic signature and records from it how trusted the program is. This is the process's PIP (Process Integrity Protection) label, and it decides which other processes are allowed to inspect, signal, or interfere with this one. The barrier is based on what program is running, not on who is running it: even a fully privileged process cannot disturb a more-trusted one. The full treatment is Process integrity protection.
- How it is hardened. A set of mitigations — restrictions the process carries on what it may do with its own memory and code, so that a bug or injected code has far less room to do harm. They can only ever be tightened, never loosened. The catalog and rules are Process mitigations.
- Who may operate on it. Every process has its own security descriptor — the rules for who is allowed to act on the process itself: inspect it, signal it, and so on. It lives on the PSB alongside the rest.
A few more specialised settings live here too — a process can be marked so that it may no longer create children, for instance — but those four are the core.
Inspecting and managing the PSB #
A process's PSB can be read by anyone the process's permissions let see its name and CPU use, which by default is everyone — even where the process is protected and nothing else about it can be seen. That is what lets a tool say why a process is closed to you.
- On a terminal,
logonse psb --pid PIDshows a process's protection, mitigations and GUID, and with--mitigationsturns mitigations on. - On the desktop, Task Manager shows them for the process you pick.
- From a program, read
/proc/<pid>/psb, or callpeios_process_psbin the SDK.
When an action is refused #
Being able to see a process's name or PSB does not grant control of it. Its security descriptor determines which actions your identity may perform. Signals also have to pass Process Integrity Protection (PIP): the program sending the signal must have enough trust to act on the target.
| Action | Process right |
|---|---|
| Read the PSB | PROCESS_QUERY_LIMITED |
| Read detailed process information | PROCESS_QUERY_INFORMATION |
Send terminating signals, including TERM and KILL | PROCESS_TERMINATE |
Suspend or resume with STOP or CONT | PROCESS_SUSPEND_RESUME |
The PSB inspection surface does not require PIP dominance, which is why a
tool can explain that a process is protected even when its other details are
closed. Detailed inspection and signalling still require it. Even
SeDebugPrivilege does not bypass PIP, and impersonating a different identity
does not change the calling program's trust.
For a managed service, use Stop service or svctl stop NAME when permitted.
That asks peinit to perform the managed action under the service's own
permissions, instead of trying to signal its protected process directly.
See Who can manage a service.
Before changing mitigations #
logonse psb --pid PID is an inspection command. Adding --mitigations MASK
changes hardening: those flags can only be turned on, never cleared during the
process's lifetime. Check the mitigation rules
and the logonse reference before using
that option. It is not a way to gain permission to inspect or end the process.
Where to go next #
The two largest parts of the PSB each have a topic of their own: Process integrity protection, for the trust label that governs which processes may interfere with which, and Process mitigations, for the self-hardening flags and how they are applied.
Manage services and jobs
Peios / Using Peios / Services & jobs
Use Services Manager on the desktop or svctl in a terminal to see what is running and control it. Both talk to peinit, the service manager that runs as PID 1. Start with a status check; change the definition only when the running service needs different configuration.
$ svctl list
$ svctl status jellyfin
$ svctl boot
Replace jellyfin with the service name you want to inspect. list shows only services you have permission to query.
Where to start #
| What you need | Start here |
|---|---|
| Check, start, stop, restart, or reload a service | Controlling services |
| Understand Failed, Backoff, Skipped, or Abandoned | Read service states and Troubleshooting |
| Find output from a failed run or hook | Service output and logging |
| Create a service or change its settings | Defining a service |
| Change automatic starts or a schedule | Triggers and timers |
| Diagnose a reduced boot or reboot loop | Boot modes |
| Shut down or reboot deliberately | Shutdown |
For exact field types and defaults, use the Registry key reference. For implementation and protocol details, use the peinit Technical Reference Manual.
Three kinds of object #
| In status or logs | Meaning | What to inspect |
|---|---|---|
| Service | A named definition and its current state. | svctl status <service> |
| Job | One execution of a service, hook, health check, or submitted program. A restart gets a new job ID. | The job ID in status and the matching logs. |
| Operation | One request to start, stop, restart, reload, or reset a service. | svctl operation-status <id> when a command returned without waiting. |
See Jobs and operations to follow a run or submit a one-off job.
peinit in one sentence #
peinit starts services from registry definitions, runs each under its configured identity, supervises it, and stops services in dependency order at shutdown. You use svctl or Services Manager to request those actions.
What peinit is not #
Service definitions live under Machine\System\Services\ in the registry. There are no systemd unit files or Windows SCM commands to edit or translate. Use Defining a service for configuration and Service types for programs that need to stay in the foreground.
Logs and durable history belong to eventd. Open Logs… for a service in Services Manager, or follow Service output and logging. The TRM compatibility notes describe the interface boundaries.
The one operational invariant #
peinit uses a cached service configuration. A saved change may wait for the next start or another relevant operation; it does not necessarily change a running process. Check when configuration changes apply. The event-loop design explains why.
Where peinit sits in the system #
The service’s Identity controls what its process may access. Its ServiceSecurity permissions control who may manage it. Permission to edit its registry definition is separate again. See Service identity and Who can manage a service. The TRM system map covers the components behind these checks.
Controlling services
Peios / Using Peios / Services & jobs
Use svctl to inspect and control services from a terminal, or Services Manager on the GXWI desktop. Start by checking the service before issuing a command that interrupts it.
$ svctl list
$ svctl status jellyfin
Use the name from the list in place of jellyfin. The command form is svctl <command> [service] [flags]. To investigate a failure, keep the state, cause, job ID, and warnings, then open the service’s logs.
Reading status #
Read state and cause together, then check health and warnings:
$ svctl status jellyfin
$ svctl list
| Detail | How to use it |
|---|---|
state, cause | Tell whether the service is running, waiting to retry, skipped, or failed, and why. See service states. |
status_text, progress | The latest information the service reported. These clear on restart and may be absent. |
current_job | Identifies this execution. Use its GUID to separate its logs from previous runs. |
current_operation | Identifies an action in progress. Poll its ID with svctl operation-status <id>. |
health | healthy, unhealthy, unknown (no check result yet), or null (no check configured). |
warnings | Notices such as leaked cgroups. Read these before trying another start. |
definition_removed | A deleted definition still has a draining instance. stop still works; start, restart, and reload do not. |
granted | Your query, start, stop, and reload rights. A visible service does not imply permission to change it. |
timers | Next scheduled occurrence, actual firing after jitter, last firing, or a reason the timer could not be armed. |
An Active service has met its configured readiness check. With Readiness=Alive, that only confirms the process exists; it does not prove the application is serving requests.
For a scheduled service:
$ svctl status logrotate
logrotate: inactive
cause: clean_exit
timers:
*-*-* 02:00:00
next: 2026-06-02T02:00:00Z, firing at 2026-06-02T02:07:12Z after jitter
last fired: 2026-06-01T02:03:40Z
svctl prints timer times in UTC, to the second. Services Manager shows them in local time. svctl list has a NEXT TIMER column for each service’s soonest firing. Jitter is already included in the actual firing time.
list omits services you cannot query. job list does the same for jobs. An operation ID expires after its terminal retention grace; a submitted job is retained for 60 seconds after it ends. For older results, look in eventd. The TRM client examples retain the full JSON views and field formats.
From the desktop: Services Manager #
On a GXWI desktop, Services Manager does the same from a window. It lists every service, shows what the selected one is doing (its state and why, its process, the account it runs as, how long it has been running, anything it has reported), and offers Start, Stop, Restart, Reload and Reset. The same commands are on each row's right-click menu. Type in Find a service to narrow the list by name or description.
Services Manager is another client of the same control socket, so it has exactly your authority and no more:
- A command is offered only where it will act. A button is available only if you hold the right the command needs (see the table below) and the service is in a state where the command does something (see the command × state matrix). Otherwise the button is unavailable. The details pane says which commands you may use on the selected service.
- Commands do not block the window. A command is sent without waiting. Services Manager follows its operation and reports the result, such as "SSH server was restarted." or the reason it failed.
- Hidden services are listed, not silently dropped.
listomits the services you cannot query. If you can read the service definitions in the registry, Services Manager still lists those services, marked Not yours to see, and says how many there are. - If peinit does not let you connect at all, Services Manager says so. By default the control socket admits everyone who is signed in, but it can be locked down. Any definitions you can read are still listed, and no command is offered.
- Its definition opens in a window of its own, from Edit definition… in the details pane or on a row's menu. New service… on the bar defines one. Both are covered in Defining a service, with
svctl definition, which does the same from a terminal. - Who may control a service, and who may change its definition, open from the details pane in the permissions editor. They are covered in Who can manage a service.
- What it has logged opens in Event Viewer, from Logs… in the details pane or on a row's menu. The window shows the service's lines and those of its hooks and health checks, newest first and as they are written. See Event Viewer.
- Timers show when they run. The Next run column shows when each service's soonest timer fires. The details pane lists each timer under its schedule, with its next run and its last run, in the machine's local time. A timer with jitter says that its run is put back by a random delay, and the next run shown already includes that delay. A timer peinit could not arm says why it never runs. These are peinit's own figures, read from
status, not worked out from the definition.
peinit doesn't announce state changes, so Services Manager asks it for the services' state every two seconds. Press F5, or select Refresh, to ask straight away. What the registry holds is different. That covers which services are defined, who may control them, and what you may change. Services Manager watches the Services key for changes and reads it again only when something changes, so a change made elsewhere, from svctl or another window, shows at once.
Before changing a service #
- Start begins the full start sequence and may start dependencies.
- Stop interrupts the service. A
BindsTodependent stops with its target; aRequiresdependent that is already running does not. See Dependencies. - Restart stops and starts as one operation, replacing the current process. Use it when a definition change needs a new process.
- Reload asks the running program to re-read its own configuration.
reload-configinstead asks peinit to re-read service definitions. Neither promises to apply launch-only settings to an existing process. - Reset clears Failed, Abandoned, or Skipped to Inactive; it does not start the service or repair the cause.
After the action finishes, run svctl status <service> again. If it failed or only returned an advisory reload result, follow the troubleshooting guide before repeating it.
Service commands #
| Command | Does | Required right |
|---|---|---|
start | Run the service through its full start sequence. | SERVICE_START |
stop | SIGTERM, then SIGKILL after StopTimeout. | SERVICE_STOP |
restart | Stop then start, as one operation. | SERVICE_STOP + SERVICE_START |
reload | Re-read configuration (ExecReload, or SIGHUP). | SERVICE_INTERROGATE |
reset | Clear Failed/Abandoned/Skipped → Inactive. | SERVICE_STOP |
status | Report state, cause, PID, uptime, health, current job and operation, warnings, and when each timer fires. | SERVICE_QUERY_STATUS |
list | List services, their states and their next timer firing (filtered to what you can query). | (per-service SERVICE_QUERY_STATUS) |
$ svctl start jellyfin
$ svctl restart jellyfin
$ svctl reload nginx
$ svctl reset failed-migration # clear a Failed state without starting
Wait semantics #
By default, a lifecycle command blocks until its operation reaches a terminal state — start waits for Active (or Failed), stop waits for Inactive, and so on. Pass --no-wait to get the operation_id back immediately and poll with operation-status instead.
| Command | Default | Waits for |
|---|---|---|
start | wait | Active (Simple) / Completed or Inactive (Oneshot), or Failed |
stop | wait | Inactive |
restart | wait | the successful start target, or Failed |
reload | no-wait | (with --wait) the Reloading state to resolve |
reset | immediate | — |
reload is the exception — it returns immediately by default, because a reload may have no observable completion. With --wait, the response carries a mode:
mode | Meaning |
|---|---|
confirmed | The service signalled READY=1 (and, for a command reload, the command exited 0). |
advisory | The reload was issued and the detection window elapsed without explicit confirmation. |
failed | The ExecReload command exited non-zero or timed out. The service stays Active — a failed reload never takes down a running service. |
The detection window is a fixed 2 seconds — it is built in and is not configurable via the registry. If the service says nothing within that window, the reload resolves as advisory and the service stays Active.
System commands #
| Command | Does | Required right |
|---|---|---|
shutdown <type> | Graceful shutdown. type is poweroff, reboot, or halt. | SYSTEM_SHUTDOWN |
boot | Report this boot’s mode, reason, and confirmation status. | SYSTEM_QUERY_STATUS |
reload-config | Re-read all definitions and rebuild the graph (atomic). | SYSTEM_RELOAD_CONFIG |
operation-status <id> | Report the state of an operation by GUID. | SERVICE_QUERY_STATUS on its target |
$ svctl shutdown poweroff
$ svctl reload-config
$ svctl operation-status a1b2c3d4-...
Job commands #
Submitted jobs are reachable from two places, and svctl job covers both. Querying and stopping a job is administration and goes over the control socket; submitting a job, waiting on it, and signalling it belong to its submitter and go over the jobs socket at /run/services/peinit/jobs.sock (--jobs-socket overrides the path). Every job command is checked against the job's own descriptor, which by default admits the submitter, SYSTEM, and Administrators.
| Command | Socket | Does | Required right |
|---|---|---|---|
job list [filters] | control | List the jobs you can query. Filters: --submitter SID, --identity SID, --logon-session N, --state created|running|completed|failed|abandoned; all given filters must match. | per-job JOB_QUERY (others omitted) |
job status JOB_ID | control | The job view. | JOB_QUERY |
job stop JOB_ID | control | SIGTERM, then SIGKILL of the job's cgroup after its stop_timeout. Waits for the job to end by default; --no-wait returns the view as it then is. | JOB_STOP |
job submit [options] IMAGE [ARG...] | jobs | Submit IMAGE as a job running as your own primary token, and print its view once it is running (or has failed to start). With --wait, wait for it to end too. | connect to the jobs socket |
job wait [--for ready|terminal] JOB_ID | jobs | Block until the job is terminal (default) or has sent READY=1, then print the view. --for ready on a job without a readiness protocol is INVALID_STATE. | JOB_QUERY |
job signal JOB_ID SIGNAL | jobs | Send one signal (a number, or a name like SIGUSR1) to the job's main process. Only the main process, only while running. | JOB_SIGNAL |
$ svctl job list --state running
JOB STATE SUBMITTER IDENTITY PROGRESS DESCRIPTION
5f2a... running S-1-5-21-...-1001 S-1-5-21-...-1001 3/5 items nightly backup
$ svctl job status 5f2a...
$ svctl job stop 5f2a...
$ svctl job submit --description "nightly backup" --timeout 3600 -- /usr/bin/backup --full
$ svctl --wait job submit --env MODE=full /usr/bin/backup # exits 0 only if the job completed
$ svctl job wait --for ready 5f2a...
$ svctl job signal 5f2a... SIGHUP
job submit takes the definition fields as options: --description TEXT, --cwd DIR, --env NAME=VALUE (repeatable), --timeout SECS, --stop-timeout SECS, --readiness none|notify, --readiness-timeout SECS, --success-exit-code N (repeatable), --security-descriptor SDDL. Two hand the job something of yours: --fd NAME=FD passes a descriptor of the svctl process to the job under NAME (from descriptor 3, with LISTEN_FDS/LISTEN_FDNAMES set), and --output attaches svctl's standard output as the job's output sink, so the job's lines appear on your terminal as well as in eventd. Everything after IMAGE belongs to the job, options included; use -- before an image path that starts with a dash.
The CLI has no way to attach a token, so a svctl-submitted job always runs as the caller. Running a job as someone else is a programmatic act — a service attaching the token of the client it is impersonating — and is described in Jobs and operations.
The command × state matrix #
A command sent to a service in an unexpected state returns an error, not a silent no-op. This matrix is the authority on what each command does in each state:
| Command | Inactive | Starting | Active | Reloading | Stopping | Completed | Backoff | Failed | Abandoned | Skipped |
|---|---|---|---|---|---|---|---|---|---|---|
| start | Start | MERGE | ALREADY | ALREADY | QUEUE | Start | DEFER | Start | ERROR | Start |
| stop | NOOP | Cancel+Stop | Stop | Stop | MERGE | Clear | Cancel | NOOP | ERROR | NOOP |
| restart | Start | QUEUE | Restart | Restart | QUEUE | Start | Restart | Start | ERROR | Start |
| reload | ERROR | ERROR | Reload | MERGE | ERROR | ERROR | ERROR | ERROR | ERROR | ERROR |
| reset | NOOP | ERROR | ERROR | ERROR | ERROR | ERROR | ERROR | Clear | Clear | Clear |
| status | OK | OK | OK | OK | OK | OK | OK | OK | OK | OK |
Legend:
- MERGE — an operation of this type is already running; your command merges into it and you get its GUID.
- DEFER — your
startis accepted and creates a Pending start operation, but it does not run yet: it waits out the service's existing backoff deadline and then starts. If a deferred start is already waiting, you merge into it and get its GUID. - ALREADY — already in the target state; returns the current status, not an error.
- QUEUE — queued as Pending; runs after the current operation finishes.
- NOOP — no effect; returns the current status.
- Clear / Cancel — clear the state to Inactive / abort the current operation, then proceed.
- ERROR — invalid for this state; returns an error with an explanation.
The Backoff column is the subtle one: the service is down with an automatic restart pending, so start is deferred — it creates a Pending start that honours the remaining backoff delay and only runs once that deadline expires (a second start merges into the one already waiting), stop cancels the pending restart and any deferred start (the service goes Inactive), restart cancels the automatic one and does an admin restart, and reload/reset are invalid because no process exists.
Error codes #
An error response is {"status": "error", "code": "...", "message": "..."}. The code is one of:
| Code | Meaning |
|---|---|
ACCESS_DENIED | AccessCheck denied the command against the target descriptor. |
UNKNOWN_SERVICE | No such service definition (also returned for start/restart/reload on a definition-removed service). |
UNKNOWN_OPERATION | No such operation GUID — never existed, or dropped after its retention grace. |
UNKNOWN_JOB | No such job GUID — never existed, or dropped after its retention grace. A known job you cannot query returns ACCESS_DENIED. |
MALFORMED_REQUEST | The request line is not a single valid JSON object. |
REQUEST_TOO_LARGE | The request exceeds MaxRequestSize. |
INVALID_COMMAND | The command field is missing or unknown. |
INVALID_ARGUMENTS | A required field is missing or malformed (e.g. shutdown with no valid type). |
INVALID_STATE | Not valid for the service's current state (an ERROR cell above), or rejected because the system is already shutting down. |
OPERATION_TIMEOUT | A --wait operation did not reach a terminal state within its timeout. |
INTERNAL_ERROR | peinit hit an internal failure executing the command. |
The jobs socket uses the same envelope and codes, plus two of its own:
| Code | Meaning |
|---|---|
QUOTA_EXCEEDED | The submission would exceed MaxJobsPerSubmitter live jobs for your SID. Nothing was created. |
BAD_TOKEN | The token attached to a submission cannot be a job identity — more than one was attached, its impersonation level is below Impersonation, or it could not be duplicated. |
The message is human-readable and non-normative — read it for context, key off the code.
Exit status #
| Code | Meaning |
|---|---|
0 | The command succeeded. For job submit --wait, the job also completed. |
1 | peinit refused the command — an access denial, unknown service or job, invalid state, or timeout — or a job submit --wait job ended other than completed. |
64 | A usage error. |
69 | peinit could not be reached — the socket is missing or refused the connection. |
70 | A protocol error — peinit answered with something the client could not read. |
How the control interface works #
Both tools use /run/services/peinit/control.sock. Connection admission and each service action are access-controlled. The default socket admits signed-in users; the target’s permissions decide which commands they may run.
Lifecycle commands create an operation ID you can follow. Concurrent identical requests can merge into one operation. For JSON framing and complete response examples, see the control-client reference.
Connection limits #
peinit enforces hard limits on the control socket, all tunable under Machine\System\Init\:
| Key | Default | Limits |
|---|---|---|
MaxControlConnectionsPerUser | 16 | Concurrent client connections one user may hold; SYSTEM is exempt (excess are refused at connect). |
MaxControlConnections | 256 | Concurrent client connections in all (excess are refused at connect). |
MaxRequestSize | 65536 | Bytes per request. |
ConnectionTimeout | 30 | Seconds an idle connection may sit before it is closed. |
The jobs socket has its own set, under the same key:
| Key | Default | Limits |
|---|---|---|
MaxJobsConnections | 64 | Concurrent jobs-socket connections (excess are closed at connect, with no response). |
MaxJobMessageSize | 32768 | Bytes per message — which bounds a submission's whole definition. |
JobsConnectionTimeout | 30 | Seconds an idle jobs connection may sit before it is closed. A connection blocked on a wait or a pending submit is not idle. |
MaxJobsPerSubmitter | 64 | Live jobs one submitting SID may hold. SYSTEM is exempt. |
Where to start #
To understand the states the matrix refers to, read The service lifecycle.
To understand the operations every lifecycle command creates and how to poll them, read Jobs and operations.
To configure who may run each command, read Who can manage a service.
Find service logs
Peios / Using Peios / Services & jobs
In Services Manager, select the service and choose Logs…. It opens Event Viewer with the service’s lines and the output of its hooks and health checks, newest first and updating as they arrive.
For terminal queries, use evctl. peinit forwards output; eventd stores and queries the records it receives.
Find the failed run #
- Run
svctl status <service>and note its state, cause, current job ID, and warnings. - Open Logs… and inspect the time around the failure. Hook and health-check output can explain a start or health failure even when the main process wrote nothing.
- Match the job GUID to isolate one execution. A restart has a new job ID.
- For earlier attempts, look for
peinit.job.started,peinit.job.ended, and the operation events in eventd. The live job or operation may already have expired.
Log origins identify the producer: the service name, a hook such as jellyfin/ExecStartPre[0], or jobs/<id> for a submitted job. A log’s stream tells you stdout or stderr, not whether the line necessarily describes an error.
Capturing output #
peinit captures stdout and stderr, including hooks and health checks, and tags lines with the origin, stream, timestamp, and job GUID. A submitted job cannot opt out of forwarding; --output requests an additional live copy.
Pipe wiring and tagging are described in the output-flow reference.
Logs and events have different limits #
Service logs and structured events take separate paths:
| Record | Path | What to expect |
|---|---|---|
| Service stdout/stderr | peinit’s output buffer and eventd’s log socket | Best-effort; lines can be truncated or dropped under load. |
| Job, operation, access-check, and failure events | KMES, then eventd | Independent of the log socket. KMES holds a finite in-memory record; eventd supplies stored history. |
Do not assume an event has survived a reboot merely because it was emitted. The kernel ring is not persistent, and it can overwrite older events if consumers fall behind. See KMES failure modes for those limits and Auditing for event storage and queries.
The pre-eventd buffer #
Before eventd is available, peinit holds output in memory, by default up to PreEventdBuffer=1048576 bytes. Overflow drops the oldest lines. The buffer is replayed when eventd is ready, but it is not persistent storage and cannot guarantee a complete boot log.
The buffer reference explains the early-boot path.
Flood protection #
A [truncated] marker means a line exceeded MaxLogLineLength (8192 bytes by default). Output delivered faster than peinit can read can block the service’s writes at its pipe; downstream delivery to eventd is loss-tolerant.
If a submitted job’s live output has gaps, look for peinit.job.output.dropped. That reports loss in the extra output copy, not necessarily loss in eventd; query eventd separately. Ordinary log delivery remains best-effort there too.
See the registry limits before changing buffer sizes and the TRM flood-control detail for the implementation.
The eventd handoff #
When eventd reaches Active, peinit replays buffered output and forwards new lines. If eventd exits, peinit resumes buffering and repeats the handoff when it returns. Missing log lines around that interval can reflect buffer overflow or lossy delivery; an empty query alone does not prove the service wrote nothing.
Access policy can also hide records from a query. Check who may read what if the expected origin is absent. The handoff reference describes transport behaviour.
Console output #
peinit writes boot progress, shutdown progress, recovery entry, and critical failures to /dev/console. Service output is not echoed there by default. A terminal-attached service is the exception because it writes directly to its terminal.
peios.quiet can suppress peinit’s console messages, and suppressed messages are dropped rather than saved for later. See console noise settings when investigating a boot problem.
Where to start #
For symptom-based next steps, use Troubleshooting. For finding older executions or actions, use Jobs and operations. For query tools, use Event Viewer or evctl.
Troubleshoot a service
Peios / Using Peios / Services & jobs
Collect the current state and cause before changing anything:
$ svctl status <service>
$ svctl list
$ svctl boot
Keep the job or operation ID and any warnings. Open Logs… for the service in Services Manager to inspect its main process, hooks, and health checks. For older attempts, query logs and events; live status is not a permanent history.
Use the symptom below to choose the next check. Avoid repeating a restart or changing permissions until you know which layer failed.
A service won't start #
status shows it Failed or Skipped. The cause says why:
| Cause | What happened | What to do |
|---|---|---|
ValidationError | The definition is malformed — a bad field, an unresolvable conflict, an illegal health-check timing. | Check the logs for the specific field. Fix the definition; the message names what is wrong. |
DependencyFailure | A Requires/BindsTo target failed or does not exist. | Fix the target first — status it. The dependent recovers once the target can start. |
ConditionSkipped (→ Skipped) | A start-time condition was not met. Not a failure — the service decided it does not apply here. | If it should run, the condition's premise is false (a missing path/file/key). This is often correct behaviour. |
TtyUnavailable (→ Skipped) | Another service is holding the terminal this one names in TTYPath. Not a failure — it is waiting its turn. | Check who has it: the holder is the other service with that TTYPath in a running state. Give this one a higher TTYPrecedence to win next boot, or a tty:released trigger so it starts when the terminal frees. |
AssertionError | A start-time assert failed — a required precondition is missing. | Create the missing precondition (path, file, directory, registry key), then start again. |
PreHookFailure | An ExecStartPre hook exited non-zero. | Inspect the hook’s captured output and its identity. Before rerunning it manually, check for side effects: setup and migration hooks may change data. |
ParentSetupFailure | peinit could not even fork — fd/PID exhaustion, a cgroup error. No process was created. | A system-resource problem, not the service's fault. Check for fd/PID limits and cgroup health. |
PreExecFailure | Setup after fork failed — token install, rlimits, environment. | Usually identity: check Identity, that authd is up, and that RequiredPrivileges names real privileges. |
ReadinessTimeout | The process started but never became ready within StartTimeout. | Either the service is genuinely slow (raise StartTimeout, or have it send EXTEND_TIMEOUT_USEC) or it never sends READY=1 (wrong Readiness, or a bug). |
A service keeps restarting #
The service flaps — up, down, up, down. This is restart policy doing its job, but it points at an unstable service.
- It eventually settles in
FailedwithRestartBudgetExhausted. It crashedRestartMaxRetriestimes faster than it could stay healthy forRestartWindow. The fix is the service, not the policy — read its logs for the crash. Raising the budget only delays the inevitable. - It flaps forever and never exhausts the budget. Check whether it stays Active for the full
RestartWindowbetween failures. Reaching Active briefly does not reset the budget. Inspect the health-check output too; a faulty check can restart a working service. - It restarts on a clean exit you did not expect.
RestartPolicy=Alwaysrestarts even successful exits (causeCleanExitRestart). If the service is meant to exit, useOnFailureinstead.
The machine keeps rebooting #
Preserve the last console error and any service records before attributing repeated reboots to a service. If peinit reports a Critical service exhausting its restart budget, its ErrorControl=Critical policy makes peinit sync and reboot. Repeating that failure can produce a reboot loop; the symptom alone does not establish this cause.
The boot-attempt counter can break a sequence of boots that reach peinit's counter increment but never become confirmed: after N attempts (default 3), peinit enters Recovery mode. If each boot reaches the success grace before the Critical failure, its counter is reset and the reboot loop can continue without automatic Recovery. Check svctl boot while the normal control interface is available; it is unavailable in Recovery. Do not assume every reboot loop will stop after three attempts, or that peinit can deliver Recovery if it cannot start.
In Recovery, start with the failure reason on the console. Phase 2 services are skipped, so stored eventd history is not automatically queryable from that shell. Where a supported running eventd query service is available, use Find service logs and Find missing records to investigate earlier attempts. evctl queries that service; it does not open stored databases offline. Otherwise, retain the console evidence for the administrator or provider's recovery procedure.
Once the evidence identifies a Critical service failure, repair its cause or restore known-good configuration through a supported recovery path before rebooting. Disabling a required platform service can leave its dependents unable to start. If you need to intervene before the counter trips, boot with peios.recovery=1 on the kernel command line.
A service is Abandoned #
Abandoned means peinit SIGKILLed the process but it survived — stuck in uninterruptible kernel sleep (D-state), the signature of hung I/O (a dead NFS mount, a failing disk). Nothing in userspace can kill a D-state process.
- The underlying I/O fault is the real problem — investigate the storage or mount, not peinit.
- The service's cgroup is leaked and shows in
warnings. A later start uses a fresh generational cgroup, so the new instance is unaffected. svctl reset <service>clears the Abandoned state and re-checks the cgroup: if the stuck process finally died, peinit cleans up; if not, it stays leaked and warns you. Leaked cgroups clear fully only on reboot.
I changed the config and nothing happened #
peinit works from an in-memory snapshot, and each field has a mutability class that decides when a change applies:
- Immutable at runtime (
ImagePath,Type,Identity,RequiredPrivileges,ErrorControl) → takes effect only onrestart. - Apply on next start (dependencies, conditions,
OnFailure) → next start or graph reload. - Reloadable at runtime → next relevant operation. Timeouts and health-check policy can affect later checks; environment, arguments, working directory and process limits apply to a new process, not one already running.
- Hot-reloaded (
ServiceSecurity) → next control request.
If a change is not landing, check its class first. For a wholesale re-read of every definition, run svctl reload-config — it rebuilds and validates the whole graph atomically and swaps it in only if validation passes (running services are untouched). And remember peinit pulls changes from registry notifications rather than having them pushed, so there can be a brief lag.
ACCESS_DENIED #
A control command returns ACCESS_DENIED. peinit ran AccessCheck on your token against the target's descriptor and the right was not granted:
- A service command needs the matching right in the service's ServiceSecurity descriptor (
startneedsSERVICE_START,restartneeds both stop and start, …). shutdownandreload-configneedSYSTEM_SHUTDOWN/SYSTEM_RELOAD_CONFIGin peinit's control descriptor.- The check uses your effective identity at connect time — if you are impersonating, that is what is checked.
Under the default descriptors every denial is recorded by the kernel as a kacs.audit.access.checked event naming the caller, the target (its object kind is service, job or peinit-system) and the rights requested and granted. Walk it through Debugging a denial.
A job submission is refused #
A job submit — from svctl or from a service — comes back with an error rather than a job. The code says which door it hit:
| Code | What happened | What to do |
|---|---|---|
| connection refused, or an immediate close | You cannot reach /run/services/peinit/jobs.sock — its file descriptor does not grant you write, or peinit is at MaxJobsConnections. peinit itself performs no check on who may submit; the socket's descriptor is the whole policy. | Check the socket's descriptor (by default Authenticated Users may connect) and the connection count. |
QUOTA_EXCEEDED | Your SID already holds MaxJobsPerSubmitter live jobs (default 64). | job list --submitter <your SID> to find them; stop what should not be running, or raise the key. SYSTEM is exempt. |
BAD_TOKEN | The token you attached cannot become a job identity: more than one was attached, it is below Impersonation level, or peinit could not duplicate it. | Attach exactly one token obtained by impersonating the client; an Identification-level token is never enough. If the job needs Delegation, set the jobs connection to Delegation before attaching. |
INVALID_ARGUMENTS | The definition is malformed — a relative image_path, a descriptors list that does not match what you attached, a security_descriptor without an owner and DACL. | The message names the field. |
INVALID_STATE | The system is shutting down; submissions are refused. | — |
A job that was accepted but never ran is not an error response: it is an ok response whose view is failed with a cause — parent_setup_failure (peinit could not prepare the launch) or pre_exec_failure (the program could not be executed as that identity, which is where "no such file" and "permission denied" land, because peinit does not check the image before accepting a submission).
A job's output has gaps #
If a submitter attached an output sink and sees gaps, look for a peinit.job.output.dropped event for the job in eventd: the sink was not being drained fast enough, and peinit dropped lines for the sink only rather than let it slow the job. Query eventd under the job’s GUID for the independently forwarded record. Sink loss does not itself drop that record, but eventd’s log path is also best-effort, so completeness is not guaranteed.
UNKNOWN_JOB #
The GUID is unknown or its retention has elapsed. A submitted job is retained for 60 seconds after it ends, then dropped; check eventd for any stored record. A known job that you lack permission to query returns ACCESS_DENIED. job list instead omits jobs you cannot query.
A dependent never started #
You started (or booted) a service, but something that depends on it never came up:
- A
Requires/BindsTodependent stays blocked until its target reaches a satisfying state (Active, Reloading, Completed, or Skipped). If the target is stuck in Starting or Failed, so is the dependent — fix the target. - If a dependent got connection refused on boot, check the target's readiness setting and startup logs. With
Readiness=Alive, peinit waited only for the process to exist, not to be serving. UseReadiness=Notifyonly if the program supports sendingREADY=1from its tracked main process when ready; changing the setting does not add that support and can causeReadinessTimeout. See the notification contract. If the program cannot signal readiness, have its service or package owner resolve the startup race. peinit's boot validation warning can identify an Alive-readiness dependency; it does not prove the cause of a refused connection. - A
Wantsdependent waits for the optional target’s startup attempt to resolve, but starts even if the target failed. UseRequireswhen successful readiness is necessary.
Booted into Safe mode #
Safe mode selects Critical and SafeMode=1 services within the boot-triggered set. Those flags do not give a demand-only service a boot trigger. You land here when graph validation finds a cycle or unresolvable conflict involving a Critical service, or when peios.safemode=1 is on the kernel command line. The TCB is healthy; the configuration is broken.
Fix the graph: the logs name the cycle path (A → B → C → A) or the conflicting pair. Once the cycle is broken or the conflict resolved, a normal boot returns. You can start any service by hand from Safe mode in the meantime — it is only auto-start that is restricted.
Booted into Recovery mode #
Recovery mode gives you a SYSTEM shell on the console and skips Phase 2 services. It starts registryd only if the boot has not already attempted that start. You reach it when the boot counter hits N, when peios.recovery=1 is set, or when registryd failed in Phase 1 (Recovery is immediate; this boot attempt has already incremented the counter).
If the registry itself is failing:
- Preserve peinit's console failure reason and the source's startup diagnostics. A failed registry operation does not prove the source process has exited.
- Identify the installed
registrydprovider and version, its hive declarations and actual database paths. LCS and sources explains the defaultloregdprovider and its diagnostics. - Establish whether the source is already active before any storage work. Recovery may retain a registryd started or attempted earlier in the boot. Do not launch a second source against the same hive files.
- Obtain the installed provider's supported recovery procedure before attempting offline inspection or repair. The source-checked loregd implementation accepts hive declarations and has no offline repair switches. It does not create an automatic startup backup; its orphan cleanup is not corruption repair or backup restoration. Those references identify the checked revision. Verify what recovery copy actually exists and what it contains before planning a replacement.
When the source is serving the required registry operations, Backup and restore describes the supported privileged subtree workflow. That workflow depends on a working source; it does not repair an unavailable source. Restore replaces the target contents and descendants, including security descriptors, so review the target and backup before using it. Role definitions can re-supply service configuration but do not restore every registry value.
Recovery offers no TCB guarantee. Treat the unrestricted SYSTEM shell accordingly, and remember it needs console access (physical, IPMI, or serial); there is no remote recovery yet.
Where to start #
To read states and causes fluently, keep The service lifecycle handy.
For restart, backoff, and Critical-reboot behaviour, see Keeping services running.
For the commands and their outputs, see Controlling services.
Read service states
Peios / Using Peios / Services & jobs
Run svctl status <service> and read state and cause together. State tells you where the service is now; cause tells you why it got there. Failed with ValidationError calls for a definition fix, while Failed with RestartBudgetExhausted calls for the logs from repeated attempts.
Use the state table below for a quick interpretation, then the troubleshooting guide for the next action. You do not need the full state machine before running a status check.
The states #
| State | Process? | Satisfies dependents? | What it means when you see it |
|---|---|---|---|
| Inactive | No | No | Not started, or stopped cleanly and not set to restart. The neutral resting state. |
| Starting | Maybe | No | Activation in progress — conditions, hooks, fork, or readiness wait. Dependents are blocked. |
| Active | Yes | Yes | Running and ready. The normal state of a healthy Simple service. |
| Reloading | Yes | Yes | Re-reading configuration. Still counts as running. |
| Stopping | Yes (briefly) | No | SIGTERM sent; waiting for exit or SIGKILL escalation. |
| Completed | No | Yes | Oneshot finished successfully. Stays here only with RemainAfterExit=1. |
| Backoff | No | No | A restart is pending; the service is waiting out its backoff delay before the next attempt. |
| Failed | No | No | Exited abnormally and restart policy is exhausted or not configured. |
| Abandoned | Yes (unkillable) | No | SIGKILL was sent but the process survived in uninterruptible sleep (D-state). peinit has given up supervising it; its cgroup is leaked. |
| Skipped | No | Yes | A start-time condition was not met, or its terminal was unavailable. Read the cause. |
Four of these — Active, Reloading, Completed, Skipped — satisfy dependents. Everything else blocks them. That single column is the rule the whole dependency system turns on: a service waiting on a Requires target does not move until that target reaches one of those four.
Choose the next check #
- Starting: inspect the current operation, readiness configuration, and pre-hook output. The main process may not exist yet.
- Backoff: an automatic retry is already pending. Another
starthonours the existing delay;stopcancels the pending restart. - Failed: read the cause and the failed run’s logs before starting again. Definition and dependency faults are not fixed by increasing retries.
- Skipped: inspect the condition or terminal owner. Skipped satisfies dependents and is not itself a failure.
- Abandoned: investigate the underlying I/O fault.
resetclears the state but cannot kill a process stuck in D-state.
Controlling services lists which actions are valid in each state.
Transition causes #
Every transition carries a cause. peinit keeps the cause of the most recent transition on the service, and emits all of them to eventd. The full taxonomy, grouped by what kind of thing happened:
Why a service started
| Cause | Meaning |
|---|---|
ExplicitStart | An administrator or a trigger started it. |
DependencyStart | Started to satisfy another service's dependency. |
RestartPolicy | An automatic restart, after a backoff delay. |
BindsToRecovery | A bound target returned to Active; the dependent is auto-restarted. Does not consume the restart budget. |
Why a service stopped
| Cause | Meaning |
|---|---|
ExplicitStop | An administrator requested stop. |
ConflictEviction | A conflicting service started and won. |
BindsToPropagation | A bound target stopped, so this one stops too. |
ShutdownWave | The system is shutting down. |
Why a service failed or restarted — the diagnostic causes
| Cause | Meaning |
|---|---|
ProcessCrash | The main process exited non-zero or died on a signal. |
CleanExit | A Simple process exited successfully, and policy is not Always — not a crash; goes straight to Inactive. |
CleanExitRestart | A Simple process exited successfully but RestartPolicy=Always, so it is restarted. Logged clearly as not a crash. |
ReadinessTimeout | StartTimeout expired before the service became ready. |
WatchdogTimeout | A WATCHDOG=1 keepalive did not arrive in time. |
HealthCheckFailure | HealthCheckRetries consecutive health checks failed. |
PreHookFailure | An ExecStartPre hook exited non-zero. |
ParentSetupFailure | peinit could not even fork — resource exhaustion, cgroup error. No child was created. |
PreExecFailure | Setup after fork but before exec failed (token install, rlimits, environment). |
RestartBudgetExhausted | RestartMaxRetries reached within the window; no more retries. |
Why a service was rejected — definition and graph problems
| Cause | Meaning |
|---|---|
ValidationError | The definition failed validation (bad field, unresolvable conflict, illegal health-check timing…). |
CycleDetected | The service is part of a dependency cycle. |
DependencyFailure | A Requires dependency entered Failed or does not exist. |
AssertionError | A start-time Assert failed. |
ConditionSkipped | A start-time Condition was not met → Skipped (this is not a failure). |
TtyUnavailable | Another service was holding the TTYPath this one names → Skipped (also not a failure). See Triggers and timers. |
ProcessUnkillable | The process survived SIGKILL (D-state) → Abandoned. |
ExplicitReset | An administrator cleared Failed/Abandoned/Skipped without starting. |
The grouping matters for what peinit does next: the diagnostic causes are mostly restart-eligible (peinit consults restart policy), whereas the definition/graph causes are never restarted — retrying a broken definition cannot help. The distinction is spelled out in Keeping services running.
The Abandoned state #
Abandoned is the one state that reflects a kernel-level problem rather than a service-level one. peinit reaches it when it has sent SIGKILL to a service's process group but the processes are still there after a grace period — the post-kill timeout, 5 seconds by default. If the service's cgroup has not emptied within it, the processes are wedged in uninterruptible kernel sleep (D-state), typically behind a hung mount or a broken storage controller, and the service is marked Abandoned (its cgroup leaked).
peinit cannot kill a D-state process; nothing in userspace can. So it stops trying: it marks the service Abandoned, leaks the cgroup (it cannot be removed while populated), and moves on rather than hanging. The leak is never silent — it shows up in the service's warnings and, on a later start, peinit creates a fresh "generational" cgroup so the new instance is unaffected by the stuck old one.
An Abandoned service is cleared with reset, which re-checks the cgroup: if it finally emptied, peinit cleans up; if it is still populated, peinit leaves it leaked and warns you. An Abandoned service is a sign of an underlying I/O fault that needs investigating, not something to paper over with a restart.
The common path #
Most of a service's life is a small loop. The diagram below shows the states a healthy Simple service moves through, plus the two ways it leaves Active.
stateDiagram-v2
[*] --> Inactive
Inactive --> Starting: start / trigger / dependency
Starting --> Active: ready (READY=1 or alive)
Active --> Stopping: stop / shutdown / conflict
Stopping --> Inactive: exited (clean stop / shutdown)
Stopping --> Failed: exited (conflict / BindsTo eviction)
Active --> Backoff: crash + restart allowed
Backoff --> Starting: backoff delay elapsed
Active --> Failed: crash + no restart
Starting --> Failed: timeout / hook / setup failure
Failed --> Starting: explicit start
A few things this picture makes concrete:
- An automatic restart always routes through Backoff, never through Failed.
Backoff → Startingis the retry;Failedis reached only when restarts are exhausted or disabled. This is whyOnFailurefires once at the end, not on every retry. Startingcan fail before any process exists — a parent-side setup error, a failed pre-hook, a condition or assert. The cause records which.Failed → Startingis how a manualstart(or a recovery path) revives a dead service; automatic restarts never originate fromFailed.Stoppinghas two exits. A clean stop or a shutdown lands inInactive. But a service stopped because it lost aConflictsrace (ConflictEviction) or because itsBindsTotarget went away (BindsToPropagation) comes to rest inFailed, carrying that cause — so an evicted or bound-out service shows as Failed instatusand needs areset(or, for a bound service, its target returning) before it starts again. It was not shut down on purpose, so peinit does not treat it as cleanly Inactive.
Oneshot services follow a parallel path through Completed instead of Active: Starting → Completed, and then either staying there (RemainAfterExit=1) or passing through to Inactive. See Simple and Oneshot services.
Readiness gating during boot #
The dependent-satisfaction rule is what makes boot orderly. A service's dependents do not start until it satisfies them:
- A Simple service satisfies dependents when it signals
READY=1(Notify) or when its process exists (Alive). - A Oneshot service satisfies dependents when it exits successfully and reaches Completed.
- A Skipped service satisfies dependents immediately — it succeeded by not needing to run.
If a service does not reach readiness within its StartTimeout, its dependents diverge by relationship: Requires dependents transition to Failed (DependencyFailure); Wants dependents start anyway. That difference is the whole point of the two relationship types — see Dependencies and ordering.
Invariants #
A handful of rules hold without exception:
- A service is in exactly one state at any moment.
- Only the transitions peinit defines are valid — there are no others.
- Only peinit changes a service's runtime state. No external process can reach in and set it.
- A service's securable identity (its ServiceSecurity descriptor — who can manage it) is independent of its process identity (its token — what it can access).
- Readiness is per start generation — a
READY=1from a previous incarnation is never honoured for the current start.
Where to start #
To understand what happens after a service crashes — restart policy, backoff, health checks, watchdogs — read Keeping services running.
To understand how dependents block on and are released by these states, read Dependencies and ordering.
To see which commands are valid in which state, read the command × state matrix in Controlling services.
Jobs and operations
Peios / Using Peios / Services & jobs
A job is one execution; an operation is one action requested on a service. Use the IDs from svctl status to follow what is happening. A service restart creates a new job, so its job ID also distinguishes its output from the previous run.
$ svctl status jellyfin
$ svctl operation-status <operation-id>
$ svctl job list --state running
$ svctl job status <job-id>
job list and job status here are for submitted jobs. For service-run history, follow the job ID into logs and events. Live status is short-lived; eventd is where to look after an ID expires.
Run a one-off job #
Use job submit when you want one supervised execution under your own identity. For example, if /usr/bin/backup is installed:
$ svctl job submit --description "nightly backup" --timeout 3600 -- /usr/bin/backup --full
Keep the returned ID, then query or wait for it:
$ svctl job status <job-id>
$ svctl job wait <job-id>
To end the whole job, use svctl job stop <job-id>. job signal sends a signal only to its main process and is not a substitute for stopping its cgroup. Check the final state and cause: a job that handles a requested stop and exits successfully is Completed with cause explicit_stop.
A submitted job remains queryable for 60 seconds after it ends. After that, use its GUID in eventd. See job command options and permissions before passing descriptors, changing the job descriptor, or attaching live output.
How they surface #
You meet jobs and operations in four places:
status—current_jobandcurrent_operationgive the GUIDs of the service's active execution and active action (ornull).operation-status <id>— polls one operation to a terminal state; a--waitlifecycle command does this for you under the hood.job list/job status <id>— the submitted jobs you may query, and one job's view.- eventd — the durable history. Every job and operation lifecycle transition lands there as a structured event, which is how you reconstruct what happened after the objects themselves are gone from peinit's memory.
Jobs: what actually ran #
Every time peinit forks a process — a service's main binary, a pre- or post-hook, a health check, or a submitted job — that single execution is a job, with its own GUID and exit result. If a service is the what, a job is what actually ran. A restart does not reuse a job; it creates a new one. The service always tracks its current job GUID, and a status query reports it.
Jobs come in five types:
| Type | Created for |
|---|---|
ServiceMain | A service's main process. |
PreExecHook | One ExecStartPre command. |
PostExecHook | One ExecStartPost command. |
HealthCheck | One health-check run. |
Submitted | A program submitted over the jobs socket. |
Their lifecycle is simpler than a service's, because a job only tracks a process — not policy:
| State | Meaning |
|---|---|
Created | The job object exists but the process is not forked yet (pre-hooks may be running). |
Running | The process is alive. |
Completed | The process exited successfully. |
Failed | The process failed — or peinit classified the job failed before fork (a parent-side setup error). |
Abandoned | The process survived SIGKILL (D-state). |
A job record carries the things you would want for forensics: the resolved identity and a token summary, the image path and arguments, created/started/ended timestamps, the exit code or signal, the cgroup, the failure cause, and the operation_id that created it (null for submitted jobs). The clean division of ownership is: the service owns policy (restart, dependencies, health schedule, current state); the job owns the execution facts (PID, exit, timestamps, identity, cgroup, log correlation).
Retention and log correlation #
For service executions, peinit keeps only active job records in memory. When a job reaches a terminal state it emits a structured event (through KMES) carrying the full record, then drops the job. Submitted jobs retain a terminal view for 60 seconds, as described below; peinit keeps no durable job history — eventd is the historian, consuming those events from the kernel ring buffer. The lifecycle events are peinit.job.created, peinit.job.started, and peinit.job.ended; a submitted job also emits peinit.job.status.reported as its progress changes. peinit.job.created and peinit.job.status.reported are verbose events, off unless the event emission policy (Machine\Generic\Events, PGSS §6.9) turns them on.
Separately, captured job stdout/stderr is forwarded to eventd tagged with the job's GUID. A service with TTYPath writes to its terminal instead; see the terminal-attached service exception. The GUID lets a query like "show me the logs for job X" distinguish that execution's records from the run before or after it.
Operations: what was requested #
An operation is a first-class object representing a requested state-machine action on a service. Rather than letting control commands mutate state directly, peinit turns each one into an operation that is validated, queued, and executed by its event loop. This is what gives concurrent callers — admin tools, automated triggers, dependency propagation — explicit, observable conflict resolution instead of races.
There are five operation types — Start, Stop, Restart, Reload, Reset — and each carries a source recording why peinit created it:
| Source | Created by |
|---|---|
Admin | A control client. |
Boot | The Phase 2 boot lifecycle. |
Shutdown | The shutdown lifecycle. |
DependencyPropagation | A start pulling in a dependency. |
RestartPolicy | An automatic restart. |
Timer | A timer firing. |
BindsToRecovery | A bound target returning to Active. |
BindsToPropagation | A bound target stopping. |
ConflictResolution | A Conflicts eviction. |
OnFailure | A failed service's fallback handler. |
The source is why a status showing current_operation.source: "restart_policy" tells you the service is being auto-restarted, while "admin" tells you a person asked.
Operation lifecycle #
flowchart LR
P["Pending"] --> R["Running"]
R --> C["Completed"]
R --> F["Failed"]
R --> A["Aborted"]
P --> M["Merged"]
P --> X["Cancelled"]
P --> F
| State | Meaning |
|---|---|
Pending | Validated and queued, waiting on a precondition (e.g. a prior stop to finish). |
Running | Executing — the service is transitioning. |
Completed | Achieved its goal (start reached Active/Completed; stop reached Inactive/Failed). |
Failed | Did not achieve its goal — including timing out while still Pending. |
Merged | Folded into an identical operation already in flight (records the survivor's GUID). |
Cancelled | Terminated while still Pending — never ran. |
Aborted | Terminated while Running — interrupted in progress. |
Cancelled and Aborted are the same idea at two points: cancelled never ran, aborted was running. The reason (admin action, supersession) is a property of the event, not the state.
Conflict resolution and merging #
When a command arrives for a service that already has an operation in flight, peinit resolves the collision deterministically — the same logic the command × state matrix summarises:
- Same type (start over start, stop over stop, reload over reload) → merge. The new caller transparently receives the existing operation's GUID; from their side, their request is in progress.
- Stop wins over start. An explicit stop cancels a pending start or aborts a running one. The admin said stop.
- Later supersedes earlier, and a start while a stop is in flight is queued to run after the stop.
Timeout and retention #
An operation inherits its target's timeout as its maximum lifetime — StartTimeout for start/reload/reset, StopTimeout for stop, and the sum of both legs for restart. Crucially, the clock starts at operation creation, including queue time — from the caller's perspective they have been waiting since they sent the command, so a long queue can time an operation out before it even runs.
Operations are emitted as events (peinit.operation.requested, .started, .ended and .merged; one .ended covers completed, failed, cancelled and aborted, and says which) and terminal ones are dropped from memory after a short grace (default 60 s — long enough for a polling client to read the result). As with jobs, peinit keeps no operation history; eventd does.
Submitted jobs #
A submitted job is a program peinit runs once, under supervision, because a process asked it to — not because a definition in the registry said so. Two situations motivate it:
- A task on a client's behalf.
backupdtakes a request from a user, and wants the backup to run as that user, in its own cgroup, with a timeout, reporting progress, visible injob list— but supervised by peinit rather than bybackupditself. - A session. A logon service (say, a web front-end) has authenticated a user and wants a long-lived per-logon process running as them, with a socket it can talk to, that it can stop when the session ends.
Both are the same mechanism: a jobs socket at /run/services/peinit/jobs.sock, separate from the control socket. A submitter connects, sends a job definition, and gets back the job's view and — while the job runs — a handle to its process.
Who may submit #
Being able to connect is the permission. peinit performs no access check of its own on a submission; the check is the one the kernel performs against the socket file's security descriptor when a process connects. The default descriptor gives SYSTEM and Administrators full control and Authenticated Users the write right that connecting requires — so out of the box, any logged-on principal may submit, and an administrator narrows or widens that by changing the socket's descriptor, not by configuring peinit.
What limits an over-eager submitter is a quota: at most MaxJobsPerSubmitter live jobs per submitting SID (default 64; SYSTEM is exempt). A submission that would exceed it is refused with QUOTA_EXCEEDED and creates nothing. A job stops counting the moment it ends.
What a job runs as #
A job submitted by svctl runs as your process’s own primary token. There is no CLI identity switch. A service can submit with a token it is authorised to convey; the kernel checks that authority, and a token below Impersonation level is refused with BAD_TOKEN. A job needing Delegation must be submitted through a connection already at that level; it cannot be raised later.
The view records both the submitter and the identity the job runs as, plus its logon session. The TRM identity-conveyance reference describes attached tokens.
The definition #
Choose an absolute image path, its arguments and environment, a working directory, and any timeout or readiness settings. A submitted job has no restart policy, dependencies, health check, or trigger; use a service definition if you need those.
Submission returns once the job is running or has failed to start, not when it has finished or signalled readiness. Use --wait to wait for completion or job wait --for ready for a job configured with notify readiness. An accepted submission can still fail at exec, for example because its identity cannot execute the image.
See svctl job options for the CLI and the submission reference for complete fields and descriptor passing.
Who may manage a job #
Every job carries its own security descriptor from the moment it exists. By default it is owned by the submitter and grants JOB_ALL_ACCESS to the submitter, SYSTEM, and Administrators — and to nobody else. In particular the job identity gets nothing: a process running as U cannot see or stop a job that happens to run as U, unless the submitter's descriptor says so. A submitter may supply its own descriptor (in SDDL) and peinit uses it exactly as given — including a descriptor that locks the submitter out of its own job.
| Right | Grants |
|---|---|
JOB_QUERY | Read the job view; wait on it; see it in job list. |
JOB_STOP | Stop the job. |
JOB_SIGNAL | Send a signal to the job's main process. |
These rights govern both the jobs socket's own commands (status, wait, stop, signal) and the control socket's job-status, job-list, job-stop. As with services, job list filters rather than denies: you see the jobs you hold JOB_QUERY on, while a command naming an existing job you cannot query returns ACCESS_DENIED. UNKNOWN_JOB means the ID is unknown or no longer retained.
The job view #
svctl job status <id> reports the job’s state and cause, submitter and execution identity, PID, readiness, exit code or signal, and timestamps. Its status text and progress are reported by the program itself; an absent value does not establish that it is stuck.
The TRM job-view reference describes the progress message forms and update limits.
Stopping, timeouts, and the end of a job #
A job ends when its process exits, when its timeout elapses, when its readiness_timeout elapses without READY=1, when someone stops it, or at shutdown. A stop — whichever of those triggers it — is a service stop in miniature: SIGTERM to the main process (skipped if the job already said STOPPING=1), then after stop_timeout a SIGKILL of everything in the job's cgroup, and if that survives, the job is Abandoned after the post-kill grace with cause process_unkillable.
cause records why peinit ended a job, and is null when the process ended of its own accord:
| Cause | Meaning |
|---|---|
parent_setup_failure | peinit could not prepare the launch — no token, no cgroup, no fork. No process existed. |
pre_exec_failure | Setup between fork and exec failed, or execve did. |
readiness_timeout | A notify job never sent READY=1 in time. |
timeout | The job ran longer than timeout. |
explicit_stop | A stop or job stop was issued. |
shutdown | The system was shutting down. |
process_unkillable | The process survived SIGKILL. |
State follows the exit, not the cause: a stopped job whose process handled SIGTERM and exited 0 is Completed with cause explicit_stop — peinit asked, the process agreed, and both facts are recorded. One that died to the signal is Failed with exit_signal set. A signal command is the raw mechanism, by contrast: SIGKILL sent that way produces a Failed job with a null cause, because peinit did not decide to end it.
A terminal job is retained for 60 seconds — long enough for a submitter polling once a second to read the result — then dropped; after that its GUID answers UNKNOWN_JOB, and the durable record is in eventd.
Shutdown #
At shutdown, every live submitted job is stopped at once, in no particular order — jobs have no dependencies, so there is nothing to order — and a job still queued to launch is cancelled with cause shutdown. Shutdown does not complete while a live job remains, bounded by the global ShutdownTimeout like everything else. Submissions during shutdown are refused with INVALID_STATE.
Output #
A job's stdout and stderr are captured and forwarded to eventd tagged with the job's GUID, unconditionally — a submitter cannot turn that off. A submitter that wants a live copy attaches one extra descriptor and sets output: true: peinit then writes each line it reads to that output sink as well. The copy is best-effort — a sink that is not being drained never slows the job or peinit; lines that would block are dropped for the sink only while the independent forwarding to eventd continues, and the drop is reported once per job as a peinit.job.output.dropped event.
Submitted jobs bypass the operation model entirely — there is no service to "start," so the submission creates a job directly. The job is the whole lifecycle.
Where to start #
To create and poll operations from the command line, read Controlling services.
To understand the service states an operation drives a service through, read The service lifecycle.
To query the durable job and operation history, read Auditing.
Defining a service
Peios / Using Peios / Services & jobs
Use svctl definition or Services Manager to change a service’s configuration. Both check field values before writing them. You need permission to change the service’s registry key; permission to start or stop the service is separate.
Start by reading the current definition and runtime state:
$ svctl definition show sshd
$ svctl status sshd
A definition lives at Machine\System\Services\<name>. Its typed registry values describe the program, identity, dependencies, triggers, and supervision policy. The name of the key is the name you use in svctl.
Change and verify #
- Read the current definition. Decide which field needs to change and when it will apply.
- Use
svctl definition editor Edit definition… in Services Manager. Check the reported changes before restarting a running service. - Validate the saved definition with
svctl definition validate <service>. This checks fields; identity, privilege names, and dependency targets also need their runtime checks. - If launch settings require it, restart the service during an appropriate interruption window.
svctl reload-configre-reads definitions but does not replace a running process. - Run
svctl status <service>again. If it fails, inspect the cause and logs.
Creating and changing a definition #
A definition is a registry key, so reg can write one. Two tools also know what each field means, and check a definition before writing it.
From a terminal: svctl definition #
$ svctl definition show sshd
$ svctl definition validate sshd
$ svctl definition create web --set ImagePath=/usr/bin/web --set Requires=lpsd --set Requires=netd:routed
$ svctl definition edit web --set StopTimeout=20 --unset Wants
def is short for definition.
- Fields go by their registry names, in any case.
- Values are given as text:
- a number as digits;
yesorno;- a choice by its name (
Type=Oneshot); - a list with one
--setfor each item, in order.
- A change is checked first, as peinit checks it. It goes through peinit's own decoder, and every
timer:schedule is parsed. If peinit would reject it, nothing is written and you are told why. - Only the values that change are written, in one registry transaction. If one of them has changed since svctl read it, nothing is written.
- Values that are not fields are kept as they are.
editnames any changed fields that wait for a restart on a running service. See the mutability classes.
Some things can't be checked until later:
- privilege names, when the token is made;
- whether the services a definition names exist, when the graph is built;
- the identity, by authd.
svctl status after the change is the final word.
From the desktop: Services Manager #
In Services Manager, Edit definition… opens the selected service's definition in a window of its own. It's in the details pane and on each row's right-click menu. It reads Definition… when you may only read the definition. New service… on the bar defines a new one. A service's definition opens in one window at a time: while it is open, Edit definition… is unavailable for that service.
-
Every field is shown, by group. Each has its name in words with the registry name beside it, its default, and when a change takes effect.
-
What you type is checked when you leave the field, as peinit checks it. Anything wrong is said beside the field, and Save stays unavailable until it is put right.
-
Each timer's schedule says when it next comes round, under Triggers, in local time. A schedule that never comes round, such as
*-02-30, is marked as one that never starts the service. peinit takes such a definition and arms its other timers, so it does not stop you saving. These times are the schedule's, before any jitter. When the timer will really fire is in the details pane, once it is saved (see Controlling services). -
Save writes as svctl does, from the button or with Ctrl+S. Only the changes are written, in one transaction, and it is refused if someone else changed a value meanwhile. Revert reads the definition again. If the service is running, Save says which changes wait for a restart.
-
Delete… asks first. A running service carries on until it stops.
-
What you may do is asked of the registry:
- changing needs the right to set values on the service's key;
- defining a service needs the right to create keys under
Machine\System\Services; - deleting needs
DELETEon the key.
Without those rights, the definition is shown with every field fixed and the reason said.
-
Who may control the service (
ServiceSecurity) is changed with Who may control it…, not here. See Who can manage a service.
Field mutability: when a change takes effect #
Every field falls into one of four mutability classes. This table is the one to keep handy.
| Class | When a change takes effect | Fields |
|---|---|---|
| Immutable at runtime | Next restart only. | ImagePath, Type, Identity, RequiredPrivileges, ErrorControl, RemainAfterExit, and, while it is running, Triggers and Disabled |
| Apply on next start | Next start or explicit graph reload — not while running. | Requires, Wants, BindsTo, Conflicts, OnFailure, Conditions, Asserts |
| Hot-reloaded | Next relevant event, no restart. | ServiceSecurity (next control request) |
| Reloadable at runtime | Next relevant operation (start, health-check cycle, …). Launch settings apply to the next process, not the current one. | Arguments, SuccessExitCodes, all timeout/retry values, the health-check fields, RestartPolicy, Environment, WorkingDirectory, the hooks and HookIdentity, Readiness, NotifyAccess, LimitNOFILE/LimitCORE, FdStoreMax, TTYPath/TTYPrecedence, RuntimeDirectories, Provides, TimerPersistent/TimerJitter, SafeMode, DisplayName, Description |
On a service that is not running, Triggers and Disabled take effect as soon as peinit has read the change: that is what arms a timer added to an inactive service.
The practical reading: changing what a process is or runs as (ImagePath, Identity, Type, privileges, ErrorControl) is fundamental enough that it only applies when a fresh process starts — you must restart. Changing policy that peinit consults each time it acts (timeouts, restart behaviour, health checks) is picked up the next time peinit acts. And ServiceSecurity is special: it is re-read on every control request, so an access-control change takes effect on the very next command without touching the running service.
Environment, Arguments, WorkingDirectory, and process limits do not alter an already-running process. Even though peinit can reload those fields, the process receives them only on its next start.
For inactive services the rules are simpler, because there is no running process to protect: a new service entry becomes available once the change notification is processed; a changed timer arms on the next evaluation; a changed dependency takes effect on the next start.
Removing a service definition #
Use svctl definition delete web only when you intend to remove the definition of web. To stop its current work, use svctl stop web instead.
Deleting a definition from the registry does not kill a running instance. peinit learns of the removal through the same notification path and behaves according to whether the service is running:
- Not running (Inactive, Failed, Completed, Skipped, Abandoned, Backoff): peinit discards the in-memory entry immediately. There is nothing to supervise.
- Running (Active, Starting, Reloading, Stopping): the running process is a job, and a job outlives its definition. peinit marks the entry definition-removed, keeps the cached definition only to finish supervising the existing instance, and does not restart it when it exits. Once it exits, the entry — and any stored fds — are discarded.
A removal that leaves a dangling hard dependency does not apply: peinit refuses the configuration read with MissingHardDependency. Remove or update the dependent definitions first. A definition-removed entry therefore has no dependents.
For a definition-removed entry, stop still works so you can drain it cleanly, but start, restart, and reload are rejected with UNKNOWN_SERVICE — there is no definition to start from. A status query reports it with a definition_removed: true flag so the draining instance is never invisible.
Service names #
A service name must be made of characters from [A-Za-z0-9._-] and be 1–128 bytes long. Anything else is a validation error. Two characters are pointedly excluded:
/— names map directly onto cgroup ids and registry key names, and a slash would be ambiguous in both.:— reserved for peinit-internal synthetic names (for example, the way a hook job is labelled).
The name is how you refer to the service everywhere: in svctl commands, in another service's dependency list, in a ServiceSecurity descriptor, and in the per-service SID derived from it.
The definition schema #
A definition is a set of typed registry values. Rather than list all of them in one wall of rows, the tables below group the fields by what they are for, with a pointer to the page that explains each group in depth. Every field is optional unless noted; the only hard requirement is ImagePath. The full type-and-default catalog lives in the Registry key reference.
What to run — the binary and its execution context.
| Field | Default | Purpose |
|---|---|---|
ImagePath (required) | — | Absolute path to the service binary. |
Arguments | — | Argument list passed to the binary. |
WorkingDirectory | / | Working directory for the process. |
Environment | — | KEY=VALUE pairs added to the environment. |
RuntimeDirectories | — | Private directories created under /run just before the process starts. See The execution environment. |
LimitNOFILE, LimitCORE | — | RLIMIT_NOFILE / RLIMIT_CORE. |
What kind of service — type and readiness. See Simple and Oneshot services.
| Field | Default | Purpose |
|---|---|---|
Type | Simple | Simple (long-running) or Oneshot (run-to-completion). |
Readiness | Notify | Notify (READY=1 via sd_notify) or Alive (ready when the process exists). Ignored for Oneshot. |
RemainAfterExit | 0 | Oneshot only — stay in Completed after a successful exit. |
SuccessExitCodes | — | Non-zero exit codes to treat as success. |
When to start — triggers and conditions. See Triggers and timers.
| Field | Default | Purpose |
|---|---|---|
Triggers | — | boot, boot:settled, timer:<schedule>, tty:released. Absent = demand-only. |
Disabled | 0 | If 1, triggers must not activate the service (manual start still allowed). |
SafeMode | 0 | Eligible within the boot-triggered set in Safe mode; does not add a boot trigger. |
Conditions, Asserts | — | Start-time checks. A failed condition skips; a failed assert fails. |
Who it runs as — identity and privileges. See Service identity and privileges.
| Field | Default | Purpose |
|---|---|---|
Identity | LocalService | Principal name, SID, or Service for a distinct virtual service account. |
RequiredPrivileges | — | Privilege allow-list; everything else is stripped from the token. |
HookIdentity | service's Identity | Identity for ExecStartPre/ExecStartPost hooks. |
How it relates to other services — dependencies. See Dependencies and ordering.
| Field | Purpose |
|---|---|
Requires | Hard dependencies — must be satisfied first; their failure fails this service. |
Wants | Soft dependencies — attempted first if present and enabled, but their failure does not block this service. |
BindsTo | Runtime coupling — if the target stops, this stops too. |
Conflicts | Mutual exclusion — starting this stops the named services. |
OnFailure | Service to start when this one enters Failed. |
How it is kept alive — supervision and health. See Keeping services running.
| Field | Default | Purpose |
|---|---|---|
ErrorControl | Normal | Normal (stay Failed) or Critical (sync + reboot on irrecoverable failure). |
RestartPolicy | OnFailure | Never / OnFailure / Always. |
RestartMaxRetries, RestartWindow, RestartDelay | 5 / 120 / 1 | Restart budget, the window of health that resets it, and the backoff base. |
HealthCheck, HealthCheckInterval, HealthCheckTimeout, HealthCheckRetries | — / 30 / 5 / 3 | Active health-check command and its timing. |
WatchdogTimeout | 0 | Expected interval between WATCHDOG=1 pings; 0 disables. |
The transition phases — hooks and timeouts. See The execution environment and The service lifecycle.
| Field | Default | Purpose |
|---|---|---|
ExecStartPre, ExecStartPost | — | Commands run before the binary / after readiness. |
ExecReload | (SIGHUP) | Reload command or signal:<NAME>. |
StartTimeout, StopTimeout | 30 / 10 | Seconds for the whole start sequence / between SIGTERM and SIGKILL. |
The remaining knobs — scheduling, notify, fds, metadata.
| Field | Default | Purpose |
|---|---|---|
TimerPersistent, TimerJitter | 1 / 0 | Catch up missed timer runs after reboot / random delay per firing. |
NotifyAccess | Main | Who may send sd_notify messages (only Main is supported). |
FdStoreMax | 0 | Size of the per-service fd store; 0 disables it. |
ServiceSecurity | inherit | Security descriptor controlling who may manage the service. |
DisplayName, Description | — | Human-readable labels for status output. |
Forward compatibility #
The schema version lives at Machine\System\Services\SchemaVersion (currently 1). peinit is deliberately forward-compatible:
- Unknown values are ignored. A definition written for a newer peinit does not break an older one.
- A newer schema version does not block boot. peinit logs a warning and continues.
- The schema only grows. New capability arrives as new optional fields, never as a breaking change to an existing one.
One defensive rule cuts the other way: a known field must not appear more than once in a collected definition. A duplicated known field is a validation error, even though the registry would ordinarily give you at most one value per name.
peinit works from a snapshot, not the live registry #
Here is the idea that explains most surprises. peinit does not re-read the registry every time it touches a service. It reads definitions at well-defined moments and operates on an in-memory model in between.
Two layers of snapshotting stack on top of each other:
- Boot generation. At the start of boot, peinit reads all definitions, builds the dependency graph, and validates it. The whole boot runs against that one snapshot. Changes made during boot — by an install script, a post-hook — do not perturb the boot in progress.
- Activation generation. When peinit starts a specific service, it snapshots that service's definition for the entire start. Pre-exec hooks, the token request, the readiness timeout, the first health checks — all use the values captured at activation. Edit a field while the service is
Starting, and the edit waits for the next start.
peinit learns about registry edits through change notifications: it subscribes to Machine\System\Services\ and Machine\System\Init\ at boot, and processes events in its event loop at a time of its choosing. If the notification queue overflows — a bulk admin operation, a flurry of scripted writes — peinit detects the overflow marker and does a full reload-config to resynchronise. You never have to think about the queue; you do have to know that a change is picked up, not pushed, and that when it takes effect depends on the field.
Where to start #
To understand the Type and Readiness fields and the Simple/Oneshot split, read Simple and Oneshot services.
To understand how a definition becomes a running, supervised process — and what each state in a status output means — read The service lifecycle.
For the complete type-and-default catalog of every registry key peinit reads, see the Registry key reference.
Simple and Oneshot services
Peios / Using Peios / Services & jobs
Choose Simple for a program that stays running and Oneshot for a task that should finish. Then check how it reports success: a Simple service becomes ready through Notify or Alive; a Oneshot succeeds by exiting successfully.
The default is Type=Simple with Readiness=Notify. Before starting a new definition, make sure the program can send READY=1 from its main process. A program that never sends it will time out even if its process is alive. Keep daemons in the foreground; peinit does not follow a double-forked replacement process.
Simple vs Oneshot at a glance #
| Simple | Oneshot | |
|---|---|---|
| Process lifetime | Long-running; is the service | Runs once, exits |
| Readiness | Notify (READY=1) or Alive | Successful exit — Readiness ignored |
| Satisfies dependents when | Active | Completed (with or without RemainAfterExit) |
| Successful end state | Active (until stopped) | Completed, then Inactive (unless RemainAfterExit=1) |
ExecStartPost fires | After readiness | After successful exit only |
StartTimeout covers | Startup until readiness | The whole execution |
| Watchdog & health checks | Supervised while Active | None — WatchdogTimeout/HealthCheck are inert |
| Restart on success | n/a | Never (use a timer to re-run) |
Simple services #
A Simple service is a long-running daemon. peinit forks, installs the token, and execs the binary. The process is the service: while it runs, the service is running; when it exits, the service has stopped. This is the default and covers the large majority of services — registryd, authd, sshd, application daemons.
The interesting question for a Simple service is when does it count as started — the moment its dependents are allowed to begin. That is the readiness model, set by the Readiness field.
Readiness: Notify vs Alive #
| Readiness | Ready when… | Use for |
|---|---|---|
| Notify (default) | The service sends READY=1 via sd_notify. | Any service that has meaningful startup work — opening a socket, loading state, connecting to a backend. The dependent genuinely should wait. |
| Alive | The process exists — readiness is immediate. | Services with no startup handshake, where "the process is up" is as good a signal as you will get. |
The distinction matters because it is a promise to dependents. With Notify, a service that declares itself a Requires target is telling peinit "do not start anything that needs me until I say READY=1." With Alive, peinit can only promise that the process was forked — not that it is functional.
Once ready, a Simple service transitions to Active and stays there until its process exits or it is stopped. What happens on exit — clean exit, crash, restart — is the subject of The service lifecycle and Keeping services running.
Oneshot services #
A Oneshot service is a run-to-completion task. peinit forks, installs the token, execs the binary, and waits for it to exit. It is the right type for setup work: creating directories, initialising a database, running a schema migration, applying a one-time fixup at boot.
For a Oneshot, readiness is exit, not a signal. The Readiness field is ignored entirely — READY=1 is meaningless for a process whose whole job is to finish. Success and failure are decided by the exit code:
- Exit 0 (or any code listed in
SuccessExitCodes) → the service succeeded. - Any other exit, or death by signal → the service failed, and the failure policy applies.
SuccessExitCodes is for binaries that signal a meaningful outcome with a non-zero code — for example, a tool that exits 2 to mean "nothing to do." Each entry is a decimal 0–255; code 0 is always success and need not be listed.
Completed, and RemainAfterExit #
A successful Oneshot transitions to Completed, and what happens next depends on RemainAfterExit:
RemainAfterExit | After a successful exit |
|---|---|
| 0 (default) | The service passes through Completed — releasing its dependents — then transitions to Inactive. |
| 1 | The service stays in Completed. |
Both forms satisfy dependents; the only difference is what a later status query shows. Use RemainAfterExit=1 when "this task is done" is a state worth seeing — a boot-time migration you want to confirm ran, say — rather than a service that quietly returns to Inactive.
Oneshot timing and hooks #
Two details distinguish a Oneshot's start sequence from a Simple one:
StartTimeoutcovers the entire run. For a Simple service the timeout covers startup until readiness; for a Oneshot it covers everything from the first pre-hook to process exit. A long-running Oneshot must raiseStartTimeoutaccordingly (or sendEXTEND_TIMEOUT_USECas it works — see Keeping services running).ExecStartPostruns only on success. Post-hooks fire after the successful exit. If the Oneshot fails,ExecStartPostdoes not run.
Restarting a Oneshot #
A Oneshot is not restarted on success, regardless of RestartPolicy — even Always. A successful exit is the goal, not a failure to retry. RestartPolicy governs only the response to failure (a non-zero exit). To re-run a Oneshot on a schedule, give it a timer trigger; that is the intended mechanism.
No forking daemons #
peinit does not support services that double-fork to background themselves. The whole reason a traditional daemon double-forks — to detach from the launcher — is moot when the launcher tracks the process it spawned. peinit obtains a pidfd for every process at fork time, which is a kernel handle to that exact process, immune to PID-reuse races. There is no MAINPID= mechanism for a service to redirect supervision onto a different process; peinit supervises what it forked.
If a legacy binary insists on daemonising, the fix lives at the packaging layer — wrap it so it stays in the foreground (commonly a --no-daemon/--foreground flag) — not at the init layer.
The sd_notify contract #
A Notify-readiness service signals readiness by sending READY=1 over the sd_notify protocol — a datagram to the socket named in the NOTIFY_SOCKET environment variable, which peinit sets for every service. READY=1 is only the most visible message; the same channel carries watchdog keepalives (WATCHDOG=1), graceful-stop notice (STOPPING=1), timeout extensions (EXTEND_TIMEOUT_USEC), status strings (STATUS=), and the fd store. These are covered where they belong — readiness here, the rest in Keeping services running and Controlling services.
Readiness is always per start generation. A READY=1 left over from a previous incarnation of the process is never valid for the current start — peinit ties every notify message to the specific process it is currently supervising, verified by pidfd.
Who is allowed to send these messages is governed by NotifyAccess, which currently has a single value: Main (0, the default). Under Main, only the tracked main PID — the process peinit forked and supervises — is authorised to send sd_notify readiness and status messages; a message arriving from any other PID (a child, a helper, an unrelated process) is ignored. So if a worker your service spawns sends READY=1, peinit will not act on it — the readiness signal must come from the main process itself.
Where to start #
To control when either type of service starts, read Triggers and timers.
To follow a service from definition to running process and read its states, read The service lifecycle.
To configure restart, health checks, and watchdogs for a Simple service, read Keeping services running.
Triggers and timers
Peios / Using Peios / Services & jobs
To see when a scheduled service will run, use svctl status <service> or the Next run column in Services Manager. To change automatic starts, edit the definition’s Triggers field. A saved schedule is not evidence that a timer is armed; check status after saving it.
A trigger decides when the service starts on its own. It is independent of service type: boot and timer triggers can start either Simple or Oneshot services. Each entry in Triggers is written type or type:argument.
| Trigger | Form | Meaning |
|---|---|---|
| Boot | boot | Start during Phase 2, in dependency order. |
| Settled boot | boot:settled | Start after the boot set settles, with a bounded wait; useful for console prompts. |
| Terminal handover | tty:released | Start when the terminal named in TTYPath is released. |
| Timer | timer:<schedule> | Start on a calendar schedule. |
Without triggers, the service is demand-only: it starts through a control command or because another service needs it. To prevent automatic activation while keeping manual start, use Disabled.
Seeing when a timer runs #
peinit reports each timer as it has it armed, in the service's status:
- the schedule's next occurrence;
- when the timer will actually fire, with its jitter added;
- when it last fired.
A schedule peinit refused, such as *-02-30, which never comes round, is reported with the reason, rather than left out.
$ svctl status logrotate
...
timers:
*-*-* 02:00:00
next: 2026-06-02T02:00:00Z, firing at 2026-06-02T02:07:12Z after jitter
last fired: 2026-06-01T02:03:40Z
svctl list has a NEXT TIMER column, with each service's soonest firing. On the desktop, Services Manager shows the same things in local time: a Next run column, and each timer in the details pane. See Controlling services for the fields.
These times are peinit's own. Jitter is drawn at random each time a timer is armed, so the time a timer fires can't be worked out from its definition.
The Disabled flag #
Disabled=1 suppresses automatic activation. A disabled service will not be started by any trigger — boot, timer, terminal handover, or anything added later — but it can still be started by hand through the control interface. It is the switch for "configured, but not running on its own right now."
Two related controls are easy to confuse with it:
| To… | Use |
|---|---|
| Stop a service from auto-starting, but keep manual start | Disabled=1 |
| Stop a service from being started at all, even manually | Deny SERVICE_START in its ServiceSecurity descriptor |
| Stop a running service | The stop command |
Enabling and disabling are registry writes performed by admin tooling, not peinit commands. peinit picks up a Disabled change through a registry notification or on reload-config. A disabled service's definition is still loaded into peinit's model, so it is ready for an on-demand start the instant you ask.
Calendar expressions #
Timer schedules are written in systemd's OnCalendar format. peinit's grammar is normative and self-contained — what follows is the whole language. The general shape is:
DayOfWeek Year-Month-Day Hour:Minute:Second Timezone
Every field is optional and has a default, so most real schedules are short.
| Field | Form | If omitted |
|---|---|---|
| DayOfWeek | weekday name(s) | any day (*) |
| Date | Year-Month-Day | *-*-* (any date) |
| Time | Hour:Minute:Second | 00:00:00 |
| Second | the :Second of Time | :00 |
| Timezone | IANA zone name | system-local time |
So 02:00:00 is a complete expression — date defaults to every day, and it means "every day at 02:00." Hour:Minute with no seconds defaults the seconds to 00.
Component syntax #
Each numeric component — year, month, day, hour, minute, second — and the weekday accept the same operators:
| Operator | Example | Matches |
|---|---|---|
| Wildcard | * | any value |
| List | 1,15 | any listed value |
| Range | Mon..Fri, 8..17 | the inclusive range |
| Repetition | 0/15 | 0, then every multiple of the step — 0, 15, 30, 45 |
| Range + step | 8..17/2 | 8, 10, 12, … up to and including 17 |
Weekdays are English names, case-insensitive, abbreviated (Mon) or full (Monday), and take lists and ranges.
Last day of the month #
A ~ in place of the - between Month and Day counts the day from the end of the month: ~01 is the last day, ~02 the second-to-last, and so on. So *-*~01 is the last day of every month. Repetition combines with it — Mon *-05~07/1 is "the last Monday in May."
Named shortcuts #
These stand in for the expression shown:
| Shortcut | Equivalent |
|---|---|
minutely | *-*-* *:*:00 |
hourly | *-*-* *:00:00 |
daily | *-*-* 00:00:00 |
weekly | Mon *-*-* 00:00:00 |
monthly | *-*-01 00:00:00 |
quarterly | *-01,04,07,10-01 00:00:00 |
semiannually | *-01,07-01 00:00:00 |
yearly / annually | *-01-01 00:00:00 |
Timezones and precision #
A timezone specifier follows the IANA database — Europe/London, US/Eastern, UTC. With none, the expression is interpreted in system-local time.
Precision is second-level. Unlike systemd, peinit does not accept sub-second fractions; a fractional second is a parse error. Service scheduling has no use for finer granularity.
Examples #
| Expression | Meaning |
|---|---|
*-*-* 02:00:00 | Every day at 2 am (system-local) |
Mon *-*-* 00:00:00 | Every Monday at midnight |
*-*-1,15 12:00:00 | 1st and 15th of each month at noon |
*-*~01 00:00:00 | Last day of each month at midnight |
Mon..Fri *-*-* 09:00:00 | Weekdays at 9 am |
*-*-* *:00/15:00 | Every 15 minutes |
*-*-* 02:00:00 Europe/London | Every day at 2 am London time |
What happens when a timer fires #
A timer firing does not blindly start the service — peinit considers the service's current state first, so a firing never collides with a run already in progress.
| Type | Current state | Action |
|---|---|---|
| Oneshot | Inactive / Completed / Failed | Start it (operation source Timer). |
| Oneshot | Active / Starting | Set a single pending flag — one catch-up run when the current one finishes. |
| Simple | Inactive / Failed | Start it. |
| Simple | Active / Starting | No-op; the firing is logged. |
The Oneshot "pending" flag is deliberately not a queue: many firings that land while a Oneshot is busy collapse into one catch-up run, taken when the current run ends. A service cannot stampede itself.
That single flag is shared across all of a Oneshot's timers, not one per timer. If a Oneshot has several timer triggers and two of them fire while it is busy, at most one run is still pending when the current one finishes. The timers are otherwise fully independent — each keeps its own next-firing time and its own last-run timestamp — but they can only ever queue up a single shared catch-up between them.
Each timer firing creates a normal start operation, so a timer-started service is observable exactly like an admin-started one — same GUIDs, same events.
Persistent timers and catch-up #
TimerPersistent (default 1) controls whether runs missed while the machine was off are caught up after a reboot.
peinit records the last-run timestamp of each timer in the registry. On boot, for each persistent timer it reads that timestamp, computes the next firing after it, and — if that time is already in the past — fires once, immediately, then resumes the normal schedule.
The catch-up is always a single run, never one-per-missed-occurrence. A daily timer that was off for five days fires once on the next boot, not five times. A non-persistent timer (TimerPersistent=0) ignores history entirely and simply computes its next future firing from now.
One subtlety worth knowing: the last-run timestamp is keyed by the schedule string. If you change a timer's schedule, the old timestamp is orphaned and the new schedule has no history, which causes exactly one spurious catch-up run. Schedule changes are rare and the cost is one extra run, so peinit accepts it rather than tracking trigger identity.
Jitter #
TimerJitter (default 0) adds a random delay of 0 to TimerJitter seconds to each firing, recomputed every time. A timer scheduled for midnight with TimerJitter=900 fires at a slightly different moment between 00:00 and 00:15 each day. Jitter only ever delays — a timer never fires before its scheduled time. It is the standard remedy for a fleet of machines all hammering the same backend at exactly midnight.
When the clock jumps #
Calendar timers are wall-clock schedules: *-*-* 02:00:00 means "02:00 on the wall," not "every 86,400 seconds." peinit arms them as absolute real-time timers that are cancelled and recomputed whenever the system clock is stepped, so the schedule stays anchored to the wall clock across corrections.
| Event | Behaviour |
|---|---|
| NTP step or manual clock set | Armed calendar timers are cancelled and recomputed against the new wall clock. A backward step pushes the next firing later; a forward step that crosses a missed time fires it once. |
| Suspend / resume | A scheduled time that elapsed while suspended fires once on resume — once, not once per missed occurrence. |
| A missed occurrence within one uptime | Fire once, then resume the normal schedule. peinit never replays every occurrence in the gap. |
| Daylight-saving transition (timezone-aware timer only) | A scheduled time in the skipped hour never fires; a scheduled time in the repeated hour fires once. See below. |
Daylight-saving is the case most likely to surprise you, and it only affects timers that name an IANA zone that observes DST (a bare 02:30:00 in system-local time follows whatever the system does). For a scheduled local time inside the affected hour:
- Spring forward — that time does not exist on the transition day. The timer does not fire for that occurrence; it resumes at the next valid occurrence.
- Fall back — that time occurs twice. The timer fires exactly once, on the first occurrence.
This is distinct from peinit's interval timers — the watchdog, health-check intervals, restart backoff, and the start/stop/reload phase timeouts. Those are genuine durations measured on the monotonic clock, so "wait 30 seconds" means 30 elapsed seconds and never lurches when the wall clock is set.
There is one boot-time case worth calling out on its own. When peinit reads a timer's stored last-run timestamp at boot and finds it is in the future relative to the current wall clock — meaning the clock has gone backwards since that run — it can't trust the timestamp, so it treats the timer as having an unknown last run and fires the persistent catch-up immediately. This only applies to the boot-time check; the runtime handling above is unaffected.
Waiting for the boot to settle #
boot:settled is boot with one difference: the service starts once the Phase 2 boot set has stopped moving — every service in it reached a state it will not leave on its own — rather than during it.
It exists for services that write to a terminal. peinit reports its progress to /dev/console, and a service holding that same terminal writes there too, so a prompt started mid-boot gets written over: login emits Username: with no newline and waits, peinit appends a service line, and the result is unreadable until you press Enter and login redraws.
The wait is capped (Machine\System\Boot\SettleTimeout, 5 seconds by default). A service stuck in Starting therefore costs a deferred service a short delay rather than preventing it — which matters most exactly when something is broken and a console prompt is what you need.
A boot:settled service is deliberately not part of the boot plan. It does not consume the parallel-start budget, is not counted towards boot success, and cannot block another service. Waiting for quiet is a preference; it must not change what the boot means.
Queueing for a terminal #
A terminal has one owner at a time. Two services writing to the same TTYPath do not fail — they interleave, which on a console means a login prompt with a progress line through the middle of it. So peinit gives the device to one of them and skips the rest, and tty:released is how the ones that were skipped get their turn.
The whole arrangement is three fields. A first-boot setup flow and a login prompt, both on the console:
| Service | TTYPath | TTYPrecedence | Triggers |
|---|---|---|---|
oobe | /dev/console | 100 | boot:settled |
login-console | /dev/console | (0) | boot:settled, tty:released |
On a first boot both are deferred, oobe outranks the login prompt on TTYPrecedence, so oobe takes the console and login-console is skipped with cause TtyUnavailable. When oobe finishes — or crashes, or is stopped; all three free the device — tty:released starts the login prompt. On every later boot oobe is gone, nothing is holding the console, and the login prompt starts straight away.
A few rules worth knowing:
- Higher
TTYPrecedencewins, and equal precedence breaks on service name, so the outcome is the same on every boot. The default is 0. - Nobody is preempted. Precedence decides between services starting at the same moment; a service already using a terminal keeps it.
- A service that stops is not offered its own terminal back. Relaunching it is
RestartPolicy's job, not this. - Both fields need a
TTYPath.tty:releasedorTTYPrecedencewithout one is rejected when the definition is read, rather than quietly doing nothing. tty:releasedon its own starts nothing until somebody takes the terminal and lets it go. Give a service that must come up either way abootorboot:settledtrigger as well, aslogin-consolehas above — otherwise a machine with nooobeinstalled never gets a login prompt.
Being skipped is not a failure: a skipped service satisfies anything that depends on it, exactly as a service skipped by a Conditions entry does.
You can list more than one trigger, including more than one of the same type. ["timer:*-*-* 02:00:00", "timer:*-*-* 14:00:00"] runs at 2 am and 2 pm. The model is built to grow — future trigger types (path, device, event) will slot into the same list without a schema change.
Where to start #
To see how a timer-started run appears as a job and an operation, read Jobs and operations.
To understand the states a timer-started service moves through, read The service lifecycle.
For the exact registry keys that store last-run timestamps, see the Registry key reference.
Keeping services running
Peios / Using Peios / Services & jobs
When a service keeps restarting, check its state, cause, and logs before changing the restart policy. Backoff means peinit is waiting to retry; Failed with RestartBudgetExhausted means the retry budget ran out.
Use the policy reference below to distinguish a program failure from a failing health check or watchdog. Preserve the underlying error while you investigate. Increasing a timeout or retry count can change when the failure appears without fixing it.
Restart policy #
When a service fails in a way that might be transient, peinit consults RestartPolicy:
| Policy | Value | Behaviour |
|---|---|---|
| Never | 0 | Never restart. The service goes straight to Failed. |
| OnFailure (default) | 1 | Restart on a crash or runtime failure. An exit code in SuccessExitCodes is not a failure and is not restarted. |
| Always | 2 | Restart on any failure, and — for a Simple service — restart even a successful clean exit (see clean exits). |
Not every failure is restart-eligible. peinit splits causes into three groups:
- Restart-eligible — peinit consults the policy and budget:
ProcessCrash,WatchdogTimeout,HealthCheckFailure,ReadinessTimeout,PreHookFailure,PreExecFailure,ParentSetupFailure. (The startup failures are eligible on purpose — a pre-hook that failed on a momentarily-missing mount deserves another try.) - Never restarted — retrying cannot help: an explicit stop or reset, shutdown, conflict eviction, a broken definition (
ValidationError,CycleDetected,DependencyFailure,AssertionError), an already-exhausted budget, or an unkillable process. - Budget-exempt —
BindsToRecovery, where a bound service came back and its dependents are revived. The dependent did not fail on its own, so this restart does not count against its budget.
The budget and backoff #
Restarting an instantly-crashing service in a tight loop is worse than useless, so every restart is throttled by two mechanisms working together.
Exponential backoff. Before each restart the service sits in the Backoff state for a delay that doubles on each consecutive failure, starting from RestartDelay (default 1 s) and capped at 60 s. So a crash-looping service backs off 1 s, 2 s, 4 s, 8 s, … up to a minute between attempts.
The budget. A consecutive-failure counter tracks how many times the service has failed in a row. Once it reaches RestartMaxRetries (default 5), the next failure is not restarted: the service transitions to Failed with cause RestartBudgetExhausted.
The reset. The counter resets to zero once the service stays Active for RestartWindow seconds (default 120) without failing. This is the crucial detail: the budget is not "N restarts ever" — it is "N restarts faster than the service can sustain RestartWindow of health." A service that crashes once a day and recovers cleanly each time never exhausts its budget, because every crash starts from a counter of zero. Only failures recurring faster than the service can stay healthy accumulate.
flowchart LR
A["Active"] -->|crash| B{"budget left?"}
B -->|yes| C["Backoff<br/>delay doubles"]
C -->|delay elapsed| D["Starting"]
D -->|ready, stays Active<br/>RestartWindow| A
B -->|no| E["Failed<br/>RestartBudgetExhausted"]
Clean exits #
For a Simple service, exiting with code 0 (or a SuccessExitCodes match) is success, not a crash — a daemon chose to quit. What peinit does next depends on the policy:
- Under
NeverorOnFailure, the cause isCleanExitand the service goes straight to Inactive, with no restart-policy consultation at all. - Under
Always, the cause isCleanExitRestartand the service is restarted — but through the same backoff and budget as a failure, so a daemon that exits cleanly in a tight loop cannot bypass throttling. Logs and status make clear it exited successfully and was restarted only because the policy is Always.
A terminal-attached Simple service is an exception: a successful exit with RestartPolicy=Always is treated as a normal logout. It resets the failure counter and restarts at the base delay, capped at 60 seconds, without using the crash budget. Failures still use the ordinary budget.
(A Oneshot clean exit is never restart-eligible regardless of policy — see service types.)
Health checks #
A live process is not always a working one — it can lose its database connection, wedge in a bad state, or return errors while still "up." An active health check closes that gap by running a command periodically and treating its exit code as a verdict: 0 is healthy, non-zero is a failure.
Health checks apply to Simple services only. peinit starts a service's health-check timer once the service becomes Active; a Oneshot has no long-running process to probe, so it never has one. HealthCheck and its companion fields are inert on a Oneshot — no timer is started — so don't set them there expecting supervision.
| Field | Default | Controls |
|---|---|---|
HealthCheck | — | The command to run (runs under the service's own token). |
HealthCheckInterval | 30 | Seconds between runs. |
HealthCheckTimeout | 5 | Seconds before a check is killed and counted as failed. |
HealthCheckRetries | 3 | Consecutive failures before the service is declared unhealthy. |
HealthCheckRetries consecutive failures mark the service unhealthy and restart it through the normal restart policy — backoff and budget included. A single success resets the failure count. If a check is still running when the next interval fires, the new one is skipped rather than stacked.
The health field in a status response reflects this: healthy, unhealthy (failing but not yet failed out), unknown (configured, no result yet), or null (no health check).
The flap guardrail. Health-check throttling only works if a failure cycle is shorter than the restart window — otherwise the counter resets between failures and the service restarts forever. peinit enforces this at validation time as a hard rule:
HealthCheckRetries × HealthCheckInterval < RestartWindow
A definition that violates it is a validation error, not a warning — the service is marked Failed with ValidationError rather than allowed to restart indefinitely.
A health check stuck in D-state is handled like any other stuck helper: its sub-cgroup is leaked and surfaced as a warning, but — unlike a stuck main process — it does not push the service to Abandoned. A health check holds no service resources; it is only a probe.
The watchdog #
The watchdog inverts the health check: instead of peinit asking the service if it is alive, the service must periodically tell peinit. Set WatchdogTimeout (seconds; 0 disables) and the service must send WATCHDOG=1 via sd_notify at least that often. Miss the deadline and peinit treats it as a failure (cause WatchdogTimeout) and applies the restart policy.
Like health checks, the watchdog is a Simple-only supervision mechanism. peinit arms the watchdog timer only once a Simple service becomes Active; on a Oneshot WatchdogTimeout is inert — no timer is started, and setting it changes nothing.
A service can adjust its own watchdog at runtime by sending WATCHDOG_USEC=<microseconds>: a positive value re-arms the timer with the new interval immediately; 0 disables the watchdog. This is for services whose phases have different latencies — a database might run a tight 5 s watchdog normally but widen it to 60 s during a compaction pass, because it knows its own phases better than the schema author did. The runtime value does not persist: a restart reverts to the schema's WatchdogTimeout.
Timeout extension #
A service that does genuinely slow work during a transition can ask for more time by sending EXTEND_TIMEOUT_USEC=<microseconds> while it is Starting, Stopping, or Reloading. Each message replaces the current phase deadline (it is not additive); a service that keeps sending them keeps proving it is making progress, and if it stops, the last deadline fires and peinit escalates normally.
The extension is capped to prevent a buggy service from extending forever:
| Phase | Base timeout | Maximum deadline |
|---|---|---|
| Starting | StartTimeout | StartTimeout × 4 |
| Stopping | StopTimeout | StopTimeout × 4 |
| Reloading | StartTimeout | StartTimeout × 4 |
During shutdown a second, global cap also applies — the deadline can never exceed the remaining time in the global shutdown timeout, and the stricter of the two caps wins. A message received outside a transition (the service is Active, Failed, …) is ignored — there is no timeout to extend.
OnFailure #
When a service enters Failed, an OnFailure field names another service to start in response — a fallback for graceful degradation (the main web UI failed; bring up a minimal emergency endpoint). It is not a monitoring or alerting hook; that is eventd's job.
OnFailure fires for runtime failures — ProcessCrash, WatchdogTimeout, HealthCheckFailure, the startup-failure causes, and non-Critical budget exhaustion. It deliberately does not fire for:
ShutdownWave— nothing new starts during shutdown.ValidationError,CycleDetected,DependencyFailure,AssertionError— these are definition or graph breakage, not a running service degrading. The fallback would likely sit in the same broken graph anyway.
Because a fallback can itself fail and carry its own OnFailure, peinit bounds the chain: it tracks which services it has already started for one originating failure, refuses to start one twice, and never follows the chain beyond a depth of 16. When the guard trips, it logs the loop and stops.
ErrorControl: when failure is not an option #
ErrorControl decides how serious an irrecoverable failure is:
| Level | Value | On irrecoverable failure |
|---|---|---|
| Normal (default) | 0 | The service stays in Failed. The rest of the system carries on. |
| Critical | 1 | peinit syncs filesystems and reboots immediately. |
A Critical service is one the system genuinely cannot run without — the platform daemons registryd, authd, lpsd, eventd are all Critical. "Irrecoverable" means the restart budget was exhausted: peinit tried, backed off, retried up to RestartMaxRetries, and the service still would not stay healthy. At that point the fastest path to a known-good system is a reboot.
A few consequences worth holding onto:
- Critical failure outranks
OnFailure. When a Critical service exhausts its budget, peinit reboots; it does not start theOnFailurehandler. ErrorControl=CriticalimpliesSafeMode. A Critical service is always eligible to start in Safe mode.- Critical services are OOM-protected. peinit sets their
oom_score_adjto-1000, so the kernel will not pick them as out-of-memory victims. - A runtime Critical crash does not enter Safe mode — it follows the reboot path. Safe mode is only for boot-time configuration errors (a cycle or conflict involving a Critical service). The escalation between reboots, the boot-attempt counter, and Recovery mode are covered in Boot and boot modes.
Where to start #
To see exactly which states these policies move a service through, read The service lifecycle.
To understand how a failed service propagates to the ones that depend on it, read Dependencies and ordering.
When a service will not stay up, work backward from the symptom in Troubleshooting peinit.
Dependencies and ordering
Peios / Using Peios / Services & jobs
Check a service’s dependencies before stopping a shared component or diagnosing a blocked start. Requires waits for successful startup but leaves an already-running dependent alone if the target later stops. BindsTo also stops the dependent when its target goes away.
Relationships are set on the service that needs something, naming the target it needs. Wants attempts an optional target first; Conflicts makes two services mutually exclusive and is symmetric, so one side declaring it is enough.
Before changing a dependency #
- Read the dependent’s definition with
svctl definition show <service>and inspect each target withsvctl status <target>. - Decide whether the requirement is successful startup (
Requires), optional startup ordering (Wants), runtime coupling (BindsTo), or mutual exclusion (Conflicts). - Use a readiness level when the target must have reached a specific condition, such as a routed network.
- After saving, validate the definition and check the next start. Dependency edits apply on a new start or graph reload; they do not rewrite a running process’s history.
A DependencyFailure usually points to the target to investigate first. An on-demand start can pull in other services and evict conflicting ones, so its effects can extend beyond the service you named.
The four relationships #
Requires — a hard dependency #
If A Requires B:
- Start. B must reach a satisfying state before A starts. If B is not running, peinit starts it first (cause
DependencyStart). If B fails, A is marked Failed withDependencyFailureand never even attempts to start. - Stop. Stopping B does not stop A. Requires is a start-ordering constraint, not a runtime leash — A keeps running.
- Runtime failure. If B crashes while A is already Active, A is unaffected. B's own restart policy handles B's recovery.
Requires is the workhorse: "I need this to have started, and if it can't, neither can I."
Wants — a soft dependency #
If A Wants B:
- Start. peinit starts B before A if B exists and is not Disabled — but if B fails to start, or does not exist, A starts anyway.
- Stop / failure. No effect either way. A and B are runtime-independent.
Wants is best-effort ordering: "start this first if you can, but I work without it."
BindsTo — a runtime coupling #
If A BindsTo B:
- Start. Identical to Requires — B must satisfy before A starts.
- Stop. If B stops for any reason — explicit stop, conflict, crash, shutdown — A is stopped too, transitioning to Stopping with cause
BindsToPropagation. When A finishes stopping it comes to rest in Failed (carryingBindsToPropagation), not Inactive — so astatusquery shows the bound service as Failed, making it clear it was taken down by its target rather than shut down cleanly. - Recovery. When B returns to Active, peinit automatically restarts A from that Failed state (cause
BindsToRecovery). This is reactive — peinit watches B's transitions — and it is budget-exempt: A did not fail on its own, so the restart does not count against its budget. If B never comes back, A stays Failed until youreset(or start) it.
BindsTo is Requires plus a runtime leash: "I need this to start, and I should not outlive it." It is the right choice for a sidecar that is meaningless without its principal. BindsTo implies Requires; listing both for the same target is harmless, and BindsTo semantics win.
Conflicts — mutual exclusion #
If A Conflicts with B:
- Start. Starting A while B is Active creates a stop operation for B (source
ConflictResolution), evicting it (causeConflictEviction) before A starts — and vice versa. If the loser will not stop within itsStopTimeout, SIGKILL escalation applies. The evicted loser does not land in Inactive: it comes to rest in Failed (carryingConflictEviction), so it shows as Failed instatusand needs aresetbefore it will start again. That is deliberate — an evicted service is not a clean stop, and leaving it Failed stops it from quietly restarting straight back into the conflict. - Symmetry. Conflicts is two-way. If A declares
Conflicts=["B"], starting either stops the other; B need not declare it back.
Conflicts is for true mutual exclusion — two services binding the same port, or two implementations of one role where exactly one must run — not for ordinary resource contention.
At a glance #
| Start order | Target failure stops dependent? | Target stop stops dependent? | |
|---|---|---|---|
| Requires | target first; dependent fails if target fails | At start time only | No |
| Wants | target first if present; dependent starts regardless | No | No |
| BindsTo | target first; dependent fails if target fails | Yes (and auto-recovers) | Yes (and auto-recovers) |
| Conflicts | starting one evicts the other | n/a | n/a |
Waiting for a condition, not just a service #
A target may carry a readiness level after a colon. Requires = ["network:routed"] waits not merely for the network manager to be active but for it to have published the level routed — a default route in place — and holds the start until it does. Publishers report a set of currently true levels, and peinit checks membership. A routed network publishes link, addressed and routed, so a service can ask for the condition it actually needs. peinit clears the set the moment its publisher leaves a satisfying state.
network there is a role, not a service: the service that fills it declares Provides = ["network"], and peinit rewrites the entry to that service before anything else looks at it. Write the role, not the daemon. The levels link, addressed and routed are defined by network policy, so the definition keeps meaning the same thing whichever executor an image ships. svctl status shows the resolved dependency, netd:routed on a standard image.
On-demand starts #
When you start a service explicitly rather than at boot, peinit does the same graph work on a smaller scope — the requested service's transitive closure:
- Collect all transitive
RequiresandBindsTodependencies (and best-effortWants). - Validate that sub-graph (cycles, missing targets).
- Resolve
Conflicts— stop anything that conflicts. - Start the sub-graph with the same parallel scheduler.
Anything already in a satisfying state (Active, Reloading, Completed, Skipped) is left alone — its dependency is already met, so there is no needless restart. Dependencies pulled in this way start with cause DependencyStart. If two on-demand starts need the same dependency at once, their start operations merge rather than racing.
Failure propagation #
When a service enters Failed during graph execution, the failure spreads along Requires and BindsTo edges — and only those:
- Every service that
Requiresthe failed one transitions to Failed withDependencyFailure. - Every service that merely
Wantsit is unaffected and starts normally. - Propagation is transitive: if A requires B and B requires C, and C fails, then B fails, then A fails — each with
DependencyFailure.
This is the payoff of the Requires/Wants distinction. A hard dependency failing takes its dependents down with it; a soft one failing is shrugged off. Choosing the right relationship is choosing how far a failure is allowed to travel.
Graph validation #
Before peinit starts anything, it builds the dependency graph and validates it. Validation runs once per graph build — at boot for the whole boot graph, and per request for an on-demand start's transitive closure. It is not incremental.
Cycles. peinit topologically sorts the graph; if the sort fails, there is a cycle. Every service in the cycle is marked Failed with CycleDetected, and peinit logs the full path (dependency cycle: A → B → C → A) so you can find and break it. A service may not depend on itself — a self-reference is rejected as a cycle. If any service in the cycle is ErrorControl=Critical, peinit downgrades to Safe mode rather than rebooting, because a reboot would just hit the same cycle.
Missing targets. What a missing target means depends on the relationship:
| Relationship | Missing or disabled target |
|---|---|
Requires | Dependent marked Failed (DependencyFailure). |
BindsTo | Treated as a missing Requires — Failed (DependencyFailure). |
Wants | Silently ignored — the entry is dropped. |
Conflicts | Silently ignored — nothing to conflict with. |
Unresolvable conflicts. If two boot-triggered services conflict with each other, there is no way to honour both, so both are marked Failed with ValidationError. As with cycles, if either is Critical, Safe mode applies.
Warnings. Some conditions are logged but do not block boot — most notably a Readiness=Alive service that others Requires (see service types). Warnings appear in the logs and do not change state.
Validation errors. Some conditions do fail the service — for example a health-check configuration that violates the flap constraint. These mark the service Failed with ValidationError.
When more than one finding applies to the same service, peinit records a single primary cause by precedence — CycleDetected > ValidationError > DependencyFailure — but it logs all findings, so the primary cause never hides the others.
Parallel start #
After validation, peinit starts services whose dependencies are all satisfied, and it does so in parallel up to a configurable limit:
| Registry key | Default | Meaning |
|---|---|---|
Machine\System\Boot\MaxParallelStarts | 10 | Maximum services starting concurrently. |
The scheduler is simple: every service with no unsatisfied dependencies is eligible; peinit starts up to MaxParallelStarts of them; as each one reaches a satisfying state its dependents become eligible and join the queue. The result is that independent subtrees of the graph come up at the same time, while ordering constraints are still honoured exactly.
Shutdown reverses the graph #
peinit does not need a separate stop-ordering configuration. Shutdown simply reverses the dependency graph: services with no dependents stop first, and services that others depend on stop last. A service is never stopped until everything that Requires or BindsTo it has already stopped.
Shared platform services stop after their dependents, with registryd last. The exact order comes from the installed graph; do not rely on a fixed list of daemon names. The full shutdown sequence is in Shutdown.
Where to start #
To see the states services move through as they start and stop in order, read The service lifecycle.
To understand how a target's failure interacts with the dependent's own restart policy, read Keeping services running.
To follow how dependency-driven starts appear as operations, read Jobs and operations.
Service identity and privileges
Peios / Using Peios / Services & jobs
A service’s Identity decides what its process can access. Its ServiceSecurity descriptor decides who may start, stop, or query it; the registry key descriptor decides who may edit the definition. Check the right boundary before changing permissions.
Read the definition with svctl definition show <service>. Prefer the identity and privileges the service actually needs. An absent or empty Identity means LocalService; SYSTEM must be explicit. peinit gives each service a separate token and never shares its own SYSTEM token.
A distinct virtual service account #
Set Identity to Service to run under the service's own S-1-5-80-… SID as
its user identity. authd derives it from the attested service name, using the same
case-insensitive derivation as the existing service group. Only SYSTEM PID 1 may
request the attestation. No local principal record or stored credential is
created, and this does not provide an interactive logon path.
Unlike shared LocalService, two differently named virtual services have distinct file and process owners. Grant storage access and policy privileges to the individual service SID. Renaming the service changes its identity and requires coordinated updates to those grants. The default remains LocalService for existing definitions; choosing Service is explicit.
The standard timed, resolvd and trustd deployments also use distinct Service identities, retaining their existing service-SID grants. Their SYSTEM preparation hooks, where present, remain separate from their main processes.
The standard eventd deployment uses this identity with SeChangeNotifyPrivilege, SeSecurityPrivilege (KMES consumption), and SeAuditPrivilege (event emission). Its System integrity level allows reception of higher-integrity client tokens; it does not grant SYSTEM account membership or SeTcbPrivilege. A short SYSTEM pre-start hook initializes registry authorization defaults, then exits before the long-running daemon starts. Private stores name eventd's own SID.
Services that need token minting/installation, raw network administration, device management or pre-authd bootstrap still require separate assessment. Granting SeTcbPrivilege solely to make an identity change work defeats the purpose of moving a service out of the machine-wide trust boundary.
The local principal source lpsd also runs as Service, with only
SeChangeNotifyPrivilege and System integrity for handling client tokens. Its
package provisions its private state scope; credential files are owned by lpsd
and admit only lpsd and SYSTEM. Its administrative API still admits SYSTEM and
Administrators. /run/psi.sock admits service connections, but authd registers a
source only when its enabled service SID matches the configured source allowlist.
Transport admission does not confer source authority.
Privilege restriction #
A token comes from its source (authd or the SYSTEM mint) with a default set of privileges. RequiredPrivileges lets a definition trim that set down to only what the service needs:
- peinit reads the
RequiredPrivilegesallow-list and removes every privilege not on it from the token before exec. - Restriction is purely subtractive. peinit removes privileges; it never adds them. A service cannot acquire a privilege its token's source did not grant.
- If
RequiredPrivilegesis absent, the token's default privilege set is used unchanged.
This is least-privilege made concrete: eudev, for instance, runs as SYSTEM (it needs device access during early boot) but with its privilege set stripped to the minimum it actually uses, so a compromise of eudev does not hand an attacker the full SYSTEM privilege set. Removal is permanent for the life of that token — it is not a disable that the service can re-enable. See Privileges for what the individual privileges grant.
Which identity runs what #
A service is not just its main process — it has hooks, health checks, and a reload command, and each runs under a defined identity:
| Context | Runs as |
|---|---|
| Main process | The service's Identity. |
ExecStartPre / ExecStartPost | HookIdentity if set, otherwise the service's Identity. |
| Health checks | The service's Identity, always. |
ExecReload (command form) | The service's Identity, always — HookIdentity does not apply. |
| Submitted jobs | The submitting process's own primary token — or, if the submitter attached one, the token the kernel verified it could convey (its client's identity, when impersonating). Never an identity named in the request. |
Token materialisation for hooks follows the same rules: a HookIdentity of SYSTEM is minted; any other principal is requested from authd, at the point the hook runs.
HookIdentity exists so hooks can run with different — usually higher — privileges than the service itself: creating directories in privileged locations, or running a database migration as an admin identity, before the long-running daemon drops to a lesser identity.
The per-service SID #
Every service token carries a SID derived from its service name. Granting access to that SID can distinguish two services even when they share LocalService. With Identity=Service, it is also the token’s user identity.
Renaming a service changes its SID. Update grants that name the old SID as part of the same planned change. The SID derivation reference gives the algorithm.
How a token is materialised #
Identity=SYSTEM uses a separate token minted by peinit. Other identities use authd; if authd cannot issue the required identity, the start fails rather than falling back to SYSTEM.
The SYSTEM path #
Use SYSTEM only when the service requires that authority, such as the services needed before authd is available. registryd and authd use this bootstrap path. Standard eventd and lpsd deployments now use their own Service identities and start through authd.
The authd path #
For a non-SYSTEM identity, check that authd is available and the account is permitted for service logon. The authd.service.attested event records the service, issued identity and privileges, or a refusal reason. peinit derives an authority dependency for a non-SYSTEM service or hook identity.
The token-materialisation reference retains the minting and authority flow. The current authd request is specified in the TRM authd path.
Security invariants #
The identity model rests on a few rules peinit never breaks:
- peinit never shares its SYSTEM token. Even
Identity=SYSTEMservices get a separately minted token. - peinit never drops its own SYSTEM identity. PID 1 runs as SYSTEM for the life of the system — its identity is axiomatic, granted by the kernel at boot.
- Privileges are subtractive only.
RequiredPrivilegesremoves; it can never add. - Identity is deterministic. Every service runs as a known principal.
SYSTEMmust be declared explicitly; an emptyIdentityis the minimalLocalService, never SYSTEM. - A submitted job never runs as an identity its submitter could not act as. peinit installs only the submitter's own primary token or a token the kernel gated on the way in; it never opens a token by a PID or descriptor a submitter names, and there is no identity field to name one.
Where to start #
Identity decides what a service can do; the ServiceSecurity descriptor decides who can manage it — two independent concerns. Read Who can manage a service for the other half.
To see how the token is installed alongside the rest of the process setup, read The execution environment.
For the identity primitives themselves — tokens, SIDs, privileges — start at Tokens.
The execution environment
Peios / Using Peios / Services & jobs
If a program works in your shell but fails as a service, compare its identity, environment, working directory, and standard streams. peinit does not inherit your login session. It launches the program with the context in its definition.
Start with svctl definition show <service>, then inspect svctl status <service> and the service’s logs. Check hook output as well as the main program. Environment and working-directory changes reach the next process start; they do not change a process already running.
The environment #
peinit constructs each service's environment in layers, lowest precedence first — it does not pass through its own near-empty startup environment.
- Compiled-in base. A fixed floor peinit always provides — currently just
PATH:/bin. - Global
EnvVars. Each value underMachine\System\Init\EnvVars\becomes a variable (value name → variable name).EnvVars\PATHoverrides the basePATH; other names add. Every service gets this layer except registryd, which serves the key itself — see the warning below for why that exemption exists. - Per-service
Environment. The definition's ownKEY=VALUEentries, which override layers 1–2. - Protocol variables.
NOTIFY_SOCKET(always) andLISTEN_FDS/LISTEN_FDNAMES(only when stored fds are injected). These have the highest precedence and cannot be overridden — a service that clobberedNOTIFY_SOCKETwould break sd_notify.
A change to EnvVars\ takes effect on a service's next start, like the per-service Environment field — it is not pushed into running services.
peinit deliberately does not set HOME, USER, LOGNAME, SHELL, or TERM. Peios identity is a token (a SID), not a passwd entry, so there is no canonical home directory or login shell to fill in. A service that needs any of these supplies it through EnvVars\ or its own Environment.
Working directory and limits #
| Field | Effect |
|---|---|
WorkingDirectory | The process's working directory. Must be a non-empty absolute path; defaults to /. |
LimitNOFILE | RLIMIT_NOFILE — the maximum number of open file descriptors. |
LimitCORE | RLIMIT_CORE — the maximum core-dump size, in bytes. |
peinit also sets oom_score_adj: -1000 (OOM-immune) for ErrorControl=Critical services, and the default 0 for everyone else — so the kernel never picks a Critical platform daemon as an out-of-memory victim.
Runtime directories #
RuntimeDirectories gives a service private scratch space under /run without an ExecStartPre that has to mkdir it by hand. Each name you list becomes a directory created directly under /run — RuntimeDirectories=myapp yields /run/myapp — set up immediately before the main process starts. Each one gets a security descriptor granting full access to SYSTEM, Administrators, and the service's own SID, so the service can write to its directory while other principals cannot. If a directory can't be created, or its descriptor can't be applied, the start fails with ParentSetupFailure.
peinit does not remove these directories when the service stops. /run is a boot-scoped tmpfs, so they simply disappear at the next boot rather than being torn down on each stop — a restarting service finds its runtime directory (though not necessarily its contents) already in place.
For the exact naming rules on each entry and the field's type and default, see the Registry key reference.
Standard streams #
peinit wires a service's standard streams before exec:
- stdin is redirected to
/dev/null. peinit never gives a service an interactive input channel; a service that needs input must obtain it explicitly (a socket, a stored fd). - stdout and stderr are redirected to pipes peinit holds, so it can capture and forward every line. This is the subject of Service output and logging.
Attaching a terminal #
A service that sets TTYPath to an absolute terminal path — /dev/console, /dev/tty1, a serial line — gets that terminal on all three standard streams instead of the wiring above, and becomes a session leader owning it as its controlling terminal. That last part is what makes job control work: Ctrl-C interrupts, fg/bg behave, and a hangup reaches the process group. It is what a boot shell or a login prompt needs.
It carries a path rather than a flag because which terminal is a per-service question: a maintenance shell on tty1 while the kernel console is a serial line is an ordinary thing to want.
Two services pointed at the same terminal do not fight over it. peinit gives the device to one of them and skips the rest — highest TTYPrecedence wins — and a skipped service can ask to be started when the terminal frees, with the tty:released trigger. See Queueing for a terminal.
Pre-exec and post-exec hooks #
Hooks let a service do work around its main process without baking it into the binary.
ExecStartPreruns before the main binary. Each command runs in sequence, in thehooks/sub-cgroup; any hook exiting non-zero aborts the start (the whole cgroup is killed and the service Fails withPreHookFailure). Use it for prerequisites that must hold before the service starts — creating a runtime directory, waiting on a precondition.ExecStartPostruns after readiness (Simple) or after a successful exit (Oneshot). A post-hook failure is logged but does not fail the service — by the time it runs, the service is already up. (A Oneshot that fails never runs its post-hooks.)
Hooks run under HookIdentity if set, otherwise the service's own identity. Their output is captured and tagged like the main process's, labelled with the hook (e.g. jellyfin/ExecStartPre[0]).
Command string parsing #
The command fields — ExecStartPre, ExecStartPost, ExecReload, HealthCheck — are parsed into an argument list with simple, predictable rules. peinit never invokes a shell.
- Whitespace splits the command into arguments; double quotes group text containing spaces into one argument and are not retained (
--name="hello world"→ one argument--name=hello world). - There is no shell expansion, variable substitution, or globbing. Backslash is literal; a single quote is an ordinary character.
- The first argument is the executable and must be an absolute path beginning with
/. Relative names and PATH search are validation errors. - An empty or whitespace-only command, or an unclosed quote, is a validation error.
If a command genuinely needs shell features, wrap it in a script and point the field at the script.
ExecReload can also be a signal, written signal:<NAME> — for example signal:SIGHUP. The name must be an exact, canonical Linux signal name (uppercase, no aliases, no numbers); SIGKILL and SIGSTOP are rejected because they cannot be handled as a reload. With no ExecReload at all, reload sends SIGHUP. The reload lifecycle is covered in Controlling services.
Conditions and asserts #
Before peinit runs any hook or the main process, it evaluates conditions and asserts — start-time checks in the same type:argument form. The difference is what failure means:
- A failed Condition skips the service — it transitions to Skipped, which satisfies its dependents (it succeeded by not needing to run). Conditions express "only run here if it makes sense."
- A failed Assert fails the service — it transitions to Failed with
AssertionError. Asserts express "this was expected to run, and a precondition is missing."
Conditions are checked first; only if all pass are asserts checked. All entries are AND-ed. The check types:
| Type | Form | Passes when |
|---|---|---|
path | path:<path> | the path exists (any type) |
file | file:<path> | a regular file exists |
directory | directory:<path> | a directory exists |
registry | registry:<key> | the registry key exists |
Two constraints follow from peinit being single-threaded PID 1, which must never block:
registry:checks are cache-only. Accepted paths are exactlyMachine\System\Services,Machine\System\Init, andMachine\System\Services\<name>with one nonempty service-name component. The roots resolve as present; the last form checks whether the service exists in the in-memory model. Init descendants, deeper Services paths, trailing separators, and other keys are load-time decode errors. No live registry read is made.- Filesystem checks run off the main loop.
path:/file:/directory:checks are performed by a short-lived forked helper, not astat()on the event loop, becausestat()can wedge on a hung mount.
That timeout is the PreStartCheckTimeout field, which defaults to 5 seconds. A filesystem check that does not finish within it — say a stat() wedged on a hung mount — is treated as not satisfied, fail-safe: peinit kills the helper and treats the check as unmet, so a Condition skips the service and an Assert fails it, rather than letting the event loop hang.
The fd store #
The fd store lets a service preserve file descriptors across restarts — most usefully a listening socket, so a stateful daemon can restart without dropping connections. It is opt-in: FdStoreMax (default 0) sets the maximum number of fds peinit will hold; 0 disables it.
The mechanics ride on sd_notify:
- A service stores an fd by sending
FDSTORE=1with the fd attached (and optionallyFDNAME=<name>; the default name isstored). peinit holds it. - It removes one with
FDSTOREREMOVE=1andFDNAME=<name>. - On the next start, peinit injects the stored fds starting at fd 3, sets
LISTEN_FDSto the count andLISTEN_FDNAMESto the colon-separated names, then clears the store. (This is the sameLISTEN_FDSconvention systemd uses, so software with existing fd-passing support works unmodified.)
The store survives an automatic restart (crash → restart policy → new start) — that is the whole point. It is cleared on an explicit stop or shutdown (the service is not coming back), and when a removed definition is finally discarded.
A clean context #
A service receives its standard streams and explicitly supplied descriptors. It does not inherit peinit’s privileged connections or signal handling. The launch-isolation reference describes the descriptor and signal setup.
The cgroup tree #
peinit tracks a service’s main process, hooks, and health checks in separate cgroups beneath /sys/fs/cgroup/peinit/. It uses those groups for tracking and clean termination, not resource limits.
A leaked cgroup warning means a process survived cleanup, often because I/O is stuck. A later start can use a fresh <service>.gen<N> tree; that does not repair or kill the stuck old process. Investigate the Abandoned state. The cgroup reference retains the layout and rationale.
Where to start #
For the identity half of the launch — how the token is chosen and installed — read Service identity and privileges.
For what happens to the stdout/stderr pipes peinit holds, read Service output and logging.
For how conditions, asserts, and hooks fit into the start sequence and its states, read The service lifecycle.
System-console aliases and logout #
Peinit pins TTYPath=/dev/console to the kernel-selected terminal when reading
service definitions. A display's moving tty0 alias becomes tty1, so switching
the foreground console does not move the session. Both arbitration and launch
use this fixed endpoint. When a terminal service's main process exits, peinit
kills its remaining session processes before handing the terminal on. A clean
exit under RestartPolicy=Always restarts the prompt at the base delay without
spending the crash budget.
Who can manage a service
Peios / Using Peios / Services & jobs
To let someone start or stop a service, change its ServiceSecurity permissions. Do not give them write access to the service definition just to grant control: definition writes can change the program and the identity it runs as.
In Services Manager, select the service and inspect the commands you may use. Who may control it… edits runtime control; Who may change its definition… edits the registry key’s permissions. If a command returns ACCESS_DENIED, check its required right and the caller’s identity before widening either descriptor.
Two descriptors, two questions #
A service is associated with two descriptors that are easy to conflate but answer different questions and are enforced by different components:
| Descriptor | Question | Stored | Enforced by |
|---|---|---|---|
| Registry key SD | Who can read or edit the definition? | On the Machine\System\Services\<name> key | LCS, at key-open time |
| ServiceSecurity SD | Who can manage the running service? | As the ServiceSecurity binary value on that key | peinit, on every control command |
The registry key SD is ordinary registry access control and not peinit's concern — peinit reads definitions as SYSTEM, which has full access. The ServiceSecurity SD is peinit's domain, and the rest of this page is about it.
They are genuinely independent. An administrator might be able to query a service's status (ServiceSecurity grants SERVICE_QUERY_STATUS) but not read its configuration (the registry key SD denies read) — or the reverse. Runtime control and configuration access are separate concerns, and both combinations are valid.
Changing them from the desktop #
On a GXWI desktop, select the service in Services Manager. Below what you may do with it, the details pane says where its ServiceSecurity comes from: set for this service, the default set for every service, or peinit's built-in default. Two buttons open the permissions editor, and both are also on each row's right-click menu.
- Who may control it… opens the ServiceSecurity descriptor. Its boxes are the service rights in words: Full control, Start, Stop, Reload (
SERVICE_INTERROGATE) and See its state (SERVICE_QUERY_STATUS). Restart needs Start and Stop together, and Reset needs Stop. When you apply, Services Manager writes the result as the service's ownServiceSecurityvalue. From then on, a change to the default no longer reaches that service. peinit applies the new descriptor on the next command, and the window's buttons change to match. - Use the default appears beside it once the service has its own value. After you confirm, it deletes that value, so the service takes the default again.
- Who may change its definition… opens the descriptor of the service's registry key. Its boxes are Full control, Read and Write on the key. Entries you add there apply to the key and to any keys under it.
Under Every service, the pane says what a service without its own value takes:
- Default permissions… opens the
ServiceSecurityonMachine\System\Services. If there isn't one, it starts from peinit's built-in default. Applying writes it to that key, and it reaches every service without its own value at the next command. - Use the built-in default appears once that key has a value. After you confirm, it deletes the value, so services without their own go back to peinit's built-in default.
Each of these is offered for changing only if you are allowed to change it:
- ServiceSecurity is a value on a registry key: the service's key, or
Machine\System\Servicesfor the default. Changing it, or deleting it, needs the right to set values on that key (KEY_SET_VALUE). - The service key's own descriptor needs
WRITE_DAC, andWRITE_OWNERfor its owner.
If you are not allowed, the editor still shows who may do what and says why it can't be changed, and the two "use the default" buttons are unavailable with the reason. Services Manager finds this out by asking the registry for those rights, not from your group memberships.
The same can be done from a terminal with reg. For example, this makes sshd take the default again:
$ reg del 'Machine\System\Services\sshd' ServiceSecurity
Service access rights #
The ServiceSecurity descriptor grants these rights:
| Right | Bit | Grants |
|---|---|---|
SERVICE_QUERY_STATUS | 0x0001 | Query state, PID, cause, health, warnings. |
SERVICE_START | 0x0002 | Start the service. |
SERVICE_STOP | 0x0004 | Stop the service. |
SERVICE_INTERROGATE | 0x0008 | Reload the service. |
SERVICE_ALL_ACCESS | 0x000F | All of the above — the "full access" granted to SYSTEM by default. |
restart requires both SERVICE_STOP and SERVICE_START, since it is a stop followed by a start. reset requires SERVICE_STOP.
When peinit evaluates the descriptor it maps the generic rights as follows, so a descriptor written with generic rights behaves sensibly:
| Generic | Maps to |
|---|---|
GENERIC_READ | SERVICE_QUERY_STATUS |
GENERIC_WRITE | SERVICE_START | SERVICE_STOP | SERVICE_INTERROGATE |
GENERIC_EXECUTE | SERVICE_START | SERVICE_STOP | SERVICE_INTERROGATE |
GENERIC_ALL | SERVICE_ALL_ACCESS |
The default descriptor #
If a service has no ServiceSecurity value, it inherits the one on Machine\System\Services itself. If that key has none either, peinit applies a built-in default:
- SYSTEM (
S-1-5-18) — full access. - Administrators (
S-1-5-32-544) — full access. - Authenticated Users (
S-1-5-11) — query only. - Everyone (
S-1-1-0) — every refusal audited, in its SACL.
So out of the box, administrators can do anything with a service, everyone who is signed in can see what it is doing, and every refused command is recorded. To let someone else start or stop a particular service, give that service a ServiceSecurity that grants it; anyone signed in can reach the control socket, so a grant to them takes effect.
ServiceSecurity is hot-reloaded: a change to the value in the registry takes effect on the next control request, with no service restart. peinit picks the change up through a registry notification. This is why ServiceSecurity is in its own mutability class — access policy should be able to change without disturbing a running service.
How a command is authorised #
Every control command runs the same gate:
- peinit captures the caller's token from the kernel (the
KACS_SO_PEER_TOKENsocket option) — the caller's effective identity at connection time, so if the caller is impersonating, the impersonated identity is what is checked. - peinit resolves the target service and its ServiceSecurity descriptor.
- peinit runs AccessCheck: the caller's token against the descriptor, for the right the command needs.
- Denied → return
ACCESS_DENIED. The kernel records the attempt — caller, target service, rights requested and granted — as akacs.audit.access.checkedevent, because the descriptor's SACL asks it to. - Granted → execute the command.
The system control descriptor #
Some operations are not about any one service — shutdown and reload-config act on the whole system, and boot reports on it. These are checked against peinit's own descriptor, stored at Machine\System\Init\ControlSecurity:
| Right | Bit | Grants |
|---|---|---|
SYSTEM_SHUTDOWN | 0x0001 | Initiate poweroff, reboot, or halt. |
SYSTEM_RELOAD_CONFIG | 0x0002 | Re-read all definitions and rebuild the graph. |
SYSTEM_QUERY_STATUS | 0x0004 | Ask how this boot went (svctl boot). |
| Generic | Maps to |
|---|---|
GENERIC_READ | SYSTEM_QUERY_STATUS |
GENERIC_WRITE | SYSTEM_RELOAD_CONFIG |
GENERIC_EXECUTE | SYSTEM_SHUTDOWN |
GENERIC_ALL | all three |
The default grants SYSTEM and Administrators all three rights, and Authenticated Users SYSTEM_QUERY_STATUS, as the default service descriptor lets everyone signed in query a service. peinit loads this descriptor at boot and hot-reloads it on registry change, exactly like ServiceSecurity. A descriptor written before SYSTEM_QUERY_STATUS existed grants it only through GENERIC_READ, GENERIC_ALL or the bit itself.
Jobs have descriptors too #
A submitted job is a securable object in the same way. It carries its own descriptor from the moment it exists — by default owned by the submitter and granting JOB_QUERY, JOB_STOP, and JOB_SIGNAL to the submitter, SYSTEM, and Administrators, and nothing to the identity the job runs as — and every job command, on either socket, is checked against it. The one thing no descriptor inside peinit governs is who may submit: that is the file descriptor on the jobs socket, checked by the kernel when a process connects.
The list command filters, it does not deny #
list is access-control-aware in a quieter way: it returns only the services the caller has SERVICE_QUERY_STATUS on, and simply omits the rest. A caller with no query rights gets an empty list, not a denial. What this protects is a service's state; that a service exists is not a secret. A command naming a service is answered UNKNOWN_SERVICE only if it does not exist, and the definitions under Machine\System\Services are readable.
The boundaries that hold #
A few invariants are worth stating outright, because they are what make this trustworthy:
- peinit never bypasses AccessCheck for a control operation. No backdoor, no override flag, no "trust localhost."
- The descriptors are the only policy inputs. peinit consults the ServiceSecurity and ControlSecurity descriptors and nothing else — not config files, not environment variables, not hardcoded lists.
- One service's state is never exposed to another without a check.
statusis per-service access-controlled;listfilters. The same holds for jobs:job statusis checked,job listfilters.
Where to start #
For the other half of the security model — what a service can reach once it is running — read Service identity and privileges.
For the commands these rights gate, read Controlling services.
For the descriptor and AccessCheck machinery itself, read Security descriptors and Access decisions.
Boot and boot modes
Peios / Using Peios / Services & jobs
On a running machine, use svctl boot or System Settings › Startup & Shutdown to see the mode, why it was chosen, and whether the boot has been confirmed successful.
- Full starts the normal boot-triggered set.
- Safe starts a reduced boot-triggered set. Use the reported graph error or requested-mode reason to decide what to repair.
- Recovery provides an unrestricted SYSTEM shell on the console and skips Phase 2 services.
svctl bootis not available there.
How this boot went #
svctl boot asks peinit, and so does System Settings, at the top of its Startup & Shutdown section:
$ svctl boot
boot: full
reason: normal
unconfirmed boots before this one: 0 (recovery at 3)
confirmed: yes
grace: 30s
- boot is the mode, and reason why:
normal,requested(peios.safemode=1), or a Safe mode peinit chose itself because a full boot couldn't be planned, with what stopped it. - unconfirmed boots before this one is the boot-attempt counter as this boot found it, and the threshold at which the machine starts in Recovery.
- confirmed says whether this boot has counted as a success yet: every Critical service holding for
BootSuccessGrace, and the counter put back to 0. Until then it names what it is waiting for.
Recovery mode never answers: it runs a recovery shell and no Phase 2 services or peinit control interface. Registryd may already be running, but there is no control socket to ask. Anyone signed in may ask; see who can manage a service for the descriptor.
The three boot modes #
The three modes form an escalation path from normal operation to last-resort maintenance.
flowchart TD
F["Full boot"] -->|success| OK["operational<br/>counter reset"]
F -->|cycle/conflict w/ Critical| S["Safe mode<br/>(no reboot)"]
F -->|Critical failure| RB["sync + reboot"]
S -->|success| OKS["operational (reduced)<br/>counter reset"]
S -->|Critical failure| RB
RB --> CNT["counter increments"]
CNT -->|counter ≥ N| REC["Recovery mode"]
F -.->|Phase 1 failure| REC
Full mode #
The default. All boot-triggered services start in dependency order. A boot is successful — and the boot-attempt counter resets to 0 — when every Critical service has reached and held a dependent-satisfying state (Active, Completed, or Skipped) for a grace period (BootSuccessGrace, default 30 s). The criterion is "satisfying," not "Active," so that a Critical Oneshot that reaches Completed can still mark the boot good.
Safe mode #
Safe mode starts a reduced set of boot-triggered services and rebuilds the graph from scratch using only those, dropping dependencies on excluded services. Two categories are eligible:
- Critical services (
ErrorControl=Critical) — must start; a Critical failure here still follows the normal reboot path. - SafeMode services (
SafeMode=1) — best-effort; if they fail, Safe mode carries on without them.
Everything else is skipped. Safe mode is entered when:
- graph validation finds a cycle involving a Critical service, or
- an unresolvable conflict involving a Critical service, or
- the kernel command line says
peios.safemode=1.
The first two are configuration errors — rebooting would just hit them again — so peinit downgrades without rebooting. Safe mode is not entered by a Critical service crashing at runtime; that follows the reboot path. And it is purely a boot-sequencing concern: once booted, you can start any service by hand exactly as in Full mode. It is the mode for "the configuration is broken but the TCB is healthy."
On a machine with the dynamic-boot feature, System Settings › Startup & Shutdown › Always Start in Safe Mode puts peios.safemode=1 on the command line the next boot image is made from. Its next-boot effect depends on a successful rebuild to the intended boot partition; verify the activation chain. Once present in the booted image it holds until changed there, and while the machine is in Safe mode because of it, System Settings says so.
Recovery mode #
Recovery mode is the last resort, and it offers no TCB guarantee — the administrator gets an unrestricted SYSTEM shell on the console and must treat it with corresponding care. It is a maintenance environment, not a degraded boot.
In Recovery, peinit completes the Phase 1 basics and starts registryd only if that boot has not already attempted it; a failed registryd start is not repeated. Registryd failure does not prevent the shell. peinit skips all Phase 2 services and execs a SYSTEM shell on /dev/console (/bin/recsh if present, else /bin/sh), respawning it if it exits. It is entered when:
- the boot-attempt counter reaches N (default 3),
- the kernel command line says
peios.recovery=1, or - registryd fails during Phase 1 — entered immediately, without rebooting, because there is no Phase 2 to attempt. The counter has already been incremented for this attempt.
Preserve the console failure reason and any source startup diagnostics first. Identify the installed registryd provider, its version, hive declarations and database paths. Recovery does not establish that the registry source is stopped: a source started or attempted earlier in the boot may still be active. Do not start a second source against the same hive files.
Offline inspection or repair needs a procedure supported by that provider and version. The source-checked loregd implementation has no offline repair switches and does not create an automatic startup backup. Its orphan cleanup is not corruption repair or backup restoration; the references identify the checked revision. Verify the existence and coverage of any recovery copy before planning to replace data.
Follow Recovery troubleshooting for the evidence to collect and the point to involve the administrator or provider. If the source can serve the required operations, the supported Backup and restore workflow can recover a subtree; it depends on that working source and is not offline source repair. Restore replaces data and permissions. Role definitions can re-supply service configuration but do not restore all registry data.
The boot-attempt counter #
The counter can escalate repeated unconfirmed boots to Recovery when peinit reaches the increment and can persist it. It is a plain integer in a file at /.peinit/boot-attempts — not in the registry, because the registry may be the very thing that is broken. A confirmed boot resets the count; the counter does not guarantee that every reboot loop ends in Recovery.
- peinit reads it at startup, before choosing a mode. The Recovery threshold (
counter ≥ N) is checked against this pre-increment value, so the default N of 3 admits exactly three attempts before Recovery. A missing file counts as 0; a corrupt or unreadable one → Recovery. Override N withpeios.bootattempts=Non the kernel command line, or set it to0to disable the check when the counter is itself the fault. - peinit increments it once per boot after the root, mount, seed, machine-ID and clock steps, before registryd starts. A registryd startup failure therefore still consumes an attempt. An earlier failure that enters Recovery before this point does not increment it.
peios.recovery=1forces Recovery but does not suppress the increment once that point is reached.- The counter is reset to 0 on a successful Full or Safe boot (after the grace period).
- If the counter file cannot be written (disk full), peinit treats it as 0 and continues — a write failure must not by itself trigger Recovery.
A Critical service exhausting its restart budget — at boot or at runtime — triggers a sync and reboot. Repeated boots that never complete the success grace accumulate attempts until the counter reaches N and peinit enters Recovery. A failure after a confirmed boot still reboots the machine, but that boot already reset the counter: repeated late failures can therefore continue without reaching the Recovery threshold. Use svctl boot to distinguish these cases. The counter deliberately does not try to catch a peinit too broken to reach its own increment; that is a binary-integrity problem, not a boot-loop problem. See the TRM counter cycle for the exact increment boundary.
Boot configuration #
| Key | Default | Controls |
|---|---|---|
Machine\System\Boot\MaxParallelStarts | 10 | Services starting concurrently (must be > 0; invalid → Recovery). |
Machine\System\Boot\BootSuccessGrace | 30 | Seconds a Critical service must hold a satisfying state before boot counts as successful. |
Machine\System\Boot\ShutdownTimeout | 90 | Maximum seconds for the whole shutdown sequence. |
Machine\System\Boot\PostKillTimeout | 5 | Seconds a service cgroup may take to drain after SIGKILL before it counts as stuck. |
Machine\System\Boot\SettleTimeout | 5 | Seconds peinit waits for the boot service set to settle before starting boot:settled services anyway. |
All but ShutdownTimeout are read at boot, so a change applies at the next one; ShutdownTimeout applies the next time peinit re-reads its configuration (svctl reload-config). System Settings shows and changes them in its Startup & Shutdown section, under Timeouts: Apply and Undo appear once one has been edited. Changing them needs write access to Machine\System\Boot — as shipped, Administrators.
The kernel command line #
peinit reads the following peios.* tokens. They are deliberately few: everything peinit can read after registryd is serving belongs in the registry instead, where it can be inspected, secured and changed without editing a boot entry. What is left is either a per-boot mode decision or a Phase 1 value — one peinit needs before there is a registry to ask.
| Token | Effect |
|---|---|
peios.safemode=1 | Force Safe mode. |
peios.recovery=1 | Force Recovery mode, whatever the counter says. |
peios.bootattempts=N | Override the boot-attempt threshold; 0 disables the check. |
peios.notifysocket=PATH | Move the sd_notify socket. Phase 1, because registryd is the first service to use it and it must be bound before registryd starts. |
peios.quiet=N | How much peinit may write to the console: 0 write everything, 1 (default) stay out of a terminal a service owns, 2 also drop ordinary progress. See below. |
Console noise: peios.quiet #
peinit reports its progress to /dev/console, and so does any service that claims that terminal with TTYPath. Two writers, one device: a login prompt started while peinit is still narrating gets written over, and the reader cannot tell input from log. peios.quiet decides who yields.
It sets two independent rules, which is why it is a level and not a flag:
| Terminal a service owns | Everywhere else | |
|---|---|---|
peios.quiet=0 | peinit writes anyway | everything |
peios.quiet=1 (default) | only messages that mean the machine is about to be lost | everything |
peios.quiet=2 | only messages that mean the machine is about to be lost | errors only |
The rules stack rather than scale: errors are never less visible at 2 than at 1. An error overrides a requested blackout — silence was a preference, and an error is news — but it does not override terminal ownership, which is not peinit's to override. Only losing the machine (dropping to Recovery, halting with no shell, a Critical service about to force a reboot) is worth one corrupted line of somebody else's session.
prelude honours peios.quiet=2 as well, since it writes to the same console. The other two levels mean nothing there — prelude exits before the first service starts, so no terminal has an owner yet.
A few lines escape all of this: whatever peinit and prelude print before they have read the command line. Neither can honour a preference it has not seen yet, and those lines are also the only evidence either started at all.
Seeing and changing it #
System Settings shows the command line this boot was started with, under Kernel Command Line in the Startup & Shutdown section, and the one the next boot image will be made from, /lcl/etc/boot/cmdline, where they differ.
On an installed machine the command line is part of the boot image, so editing that file changes nothing on its own. Dynamic Boot supplies mkuki-watch, but package delivery or feature enablement alone does not mean it is running or rebuilding successfully. Check its selected input, output filesystem and completed rebuild using the activation checklist. With the feature installed, System Settings offers peios.bootattempts and peios.quiet as choices — how many boots may fail before Recovery, and what peinit writes on the console — and writes them into the file with Apply at Next Boot, which appears once one has been changed. Nothing else on the line is offered: a mistake there can leave a machine that doesn't boot. Writing the file needs write access to it, which as shipped only Administrators have.
Unknown peios.* tokens are ignored, as is a malformed value on a valued token — this parser runs before anything exists to report a diagnostic to, and refusing to boot over a typo in a tuning knob is the worse outcome.
Where peinit takes over #
peinit is PID 1, but it is not the first thing that runs. The initramfs assembles and mounts the real root — decryption, RAID/LVM, the root filesystem itself — and then hands control to peinit. The contract peinit relies on is narrow:
- The real root is already mounted read-write, and
/proc,/sys,/devare mounted and moved into it. - peinit is exec'd as PID 1 from a fixed path on the real root.
peinit does not assemble, decrypt, repair, or even re-mount the root — those need tools and configuration that belong to the initramfs. It also does not fsck the root or mount non-root storage (a data partition is mounted by an ordinary Oneshot service, not by peinit). For the trust and identity side of this handoff — signatures, the SYSTEM token peinit inherits — see peinit at PID 1.
Phase 1: the hardcoded bootstrap #
Phase 1 is compiled into peinit. It cannot change at runtime and touches no registry. It does the minimum to make Phase 2 possible:
- Confirm the root is writable with a single probe write. registryd's storage needs a writable root even for reads, so a read-only root cannot support Phase 2 → Recovery.
- Mount the remaining virtual filesystems —
/dev/pts,/dev/shm,/run,/sys/fs/cgroup— mounting each only if absent. A failure here → Recovery. - Restore the persisted random seed from
/var/state/peinit/random-seed, mixing it into the kernel's entropy pool early so anything that needs randomness during boot gets it. A missing seed is normal — first boots and stateless live boots have none — so peinit just carries on; a seed problem is never fatal and never sends boot to Recovery. - Establish the machine-id from
/lcl/etc/machine-id— a stable, opaque identifier for this install (used for log correlation, instance identity, and software compatibility). It is not a security principal: it is not a SID, an account, or a credential, and no authorisation decision depends on it. If the file is missing, empty, or malformed, peinit generates a fresh 128-bit ID. If it cannot persist the ID, boot continues with a warning and an ID valid only for this boot; failure to obtain random bytes sends boot to Recovery. - Set the clock from the hardware RTC, so early timestamps and the boot counter are meaningful. A failure here → Recovery.
- Start registryd and wait for it to signal readiness, then probe-read the schema-version key to confirm it is actually serving reads. Any failure → Recovery — there is no Phase 2 without a registry.
- Provision boot-time paths. With registryd up and before Phase 2 starts, peinit applies the entries under
Machine\System\Init\ProvisionedPaths\— the registry-driven equivalent of tmpfiles.d, creating directories and files (with Peios security descriptors) that no single service owns. Best-effort entries that fail are logged and skipped, but an entry markedRequired=1that cannot be provisioned sends boot → Recovery. The individual keys are cataloged in the registry key reference. - Infrastructure setup — create the control socket and the jobs socket, and bring up the loopback interface. A control-socket or jobs-socket failure → Recovery; a loopback failure is logged as a warning and boot continues.
Most Phase 1 failures are fatal to a normal boot, because none of the later machinery can run without this foundation — the only outcome is Recovery mode. The exceptions are the fail-soft steps called out above: a missing or unusable random seed, a regenerated machine-id, and best-effort provisioned paths all let boot continue.
registryd and loregd #
registryd is an interface, not a specific program. It is the path peinit execs to get a registry source daemon — the component that implements the registry's persistent storage and answers peinit's reads. The implementation behind that interface can vary; by default it is loregd.
The distinction matters when diagnosing source failures: peinit's registryd role does not define a storage-repair interface. Check which provider and version the installation uses before choosing recovery instructions. The default provider's database paths come from its startup hive declarations, not registry settings. See LCS and sources for source availability checks and the Recovery-mode limits before direct storage work.
Phase 2: the registry-driven boot #
With registryd serving reads, peinit boots the rest of the system from the registry:
- Read all definitions under
Machine\System\Services\. (A registry read timing out here → Recovery.) - Build and validate the dependency graph from the boot-triggered services and their transitive dependency closure. Validation runs before anything starts.
- Start services in dependency order, in parallel up to
MaxParallelStarts, with readiness gating releasing each service's dependents as it becomes satisfied.
Only services with a boot trigger are start candidates; demand-only services are pulled in only if something boot-triggered depends on them, and Disabled services are excluded from the graph (but kept in the model for on-demand start). The whole boot runs against one snapshot — mid-boot registry edits do not perturb it.
The installed service graph determines the platform order. registryd and authd use the SYSTEM bootstrap path; standard eventd and lpsd deployments use Identity=Service and obtain tokens through authd. See Service identity. A non-SYSTEM service’s authority dependency is derived from its identity rather than left to an administrator to remember.
Do not infer readiness or shutdown order from a fixed list of daemon names. Check the installed definitions and the service status. The boot implementation describes graph construction and validation.
Where to start #
To understand the graph validation and parallel start that drive Phase 2, read Dependencies and ordering.
To understand the Critical-failure reboot path that feeds the boot counter, read Keeping services running.
For the trust and token side of early boot, read peinit at PID 1.
Shutdown
Peios / Using Peios / Services & jobs
Use svctl shutdown when you intend to stop the machine. Choose one action:
$ svctl shutdown poweroff
$ svctl shutdown reboot
$ svctl shutdown halt
These are alternatives, not a sequence. Each interrupts running services and jobs and requires SYSTEM_SHUTDOWN. Save work and account for other users before issuing one. halt stops the CPU but leaves power on.
A normal shutdown stops dependents before their dependencies, gives running services time to exit, then unmounts, syncs, and performs the selected action. Forced shutdown skips that opportunity to finish work.
How shutdown is triggered #
There are four paths into shutdown, and they are not equal — three are graceful, one is not.
A control command. An administrator with SYSTEM_SHUTDOWN runs svctl shutdown <type> (the familiar reboot, poweroff and halt commands, and shutdown -r now, are the same client under other names):
| Type | Result |
|---|---|
poweroff | Stop everything, unmount, power off. |
reboot | Stop everything, unmount, reboot. |
halt | Stop everything, unmount, halt (CPU stopped, power stays on). |
Signals. As PID 1, peinit treats a handful of signals as shutdown requests:
| Signal | Meaning |
|---|---|
| SIGINT | Reboot — the kernel sends this on Ctrl-Alt-Del. |
| SIGTERM | Poweroff. |
| SIGPWR | Poweroff — a compatibility path for environments that surface power failure or power-button policy as a signal. |
The power button. On Linux, a physical power-button press is a graceful poweroff — peinit watches the machine's input devices under /dev/input and treats a KEY_POWER press (the key going down, not its release or auto-repeat) as a shutdown request. This path is deliberately minimal and fail-soft: if /dev/input is missing, an input device cannot be opened, or a device stops responding, peinit just keeps running and drops that device — the button quietly stops working, but the machine stays up and you can still shut down over the control socket or with a signal. It is not a full power-management policy engine; a future acpid/logind-style daemon can layer richer policy on top by sending control-socket commands.
A Critical service failure — the ungraceful path. When an ErrorControl=Critical service exhausts its restart budget, peinit syncs and reboots immediately. There is no service-stop ordering: the system is in an undefined state and the fastest route to a known one is a reboot. This is what feeds the boot-attempt counter, and a Critical service that fails identically every boot becomes a reboot loop that the counter eventually breaks by escalating to Recovery.
The graceful sequence #
For a poweroff, reboot, or halt, peinit runs an ordered sequence:
- Enter the shutdown state. A flag is set, and while it holds: no new services may start, timer triggers are disarmed, and new lifecycle commands are rejected with
INVALID_STATE. Status, list, operation-status, boot, and job query/stop commands remain available so you can watch progress or stop a job sooner. - Suspend Critical-failure semantics. If a Critical service fails during shutdown, it is logged but does not trigger a reboot — the system is already going down, and rebooting would loop.
- Clear Completed services. Oneshot services sitting in Completed (with
RemainAfterExit) have no process; peinit moves them to Inactive so their dependents can be stopped cleanly. - Stop every live submitted job at once — SIGTERM, then SIGKILL of the job's cgroup after its own
stop_timeout— and cancel any job still queued to launch, both with causeshutdown. Jobs have no dependencies, so there is no order among them and nothing waits for them to finish before the service waves begin; but shutdown does not complete while a live job remains, within the same global timeout. - Stop services in reverse dependency order, in waves. Each graceful-stop-eligible service (those in Active or Reloading) gets SIGTERM and
StopTimeoutseconds to exit; if it does not, peinit SIGKILLs its whole cgroup. A service is never stopped until everything thatRequiresorBindsToit has already stopped. Services that are Starting are not stopped gracefully — their startup is cancelled and the cgroup SIGKILLed. - Enforce the global timeout. The whole sequence is bounded by
ShutdownTimeout(default 90 s). When it expires, all remaining services are SIGKILLed; any whose cgroups do not empty within the post-kill timeout (default 5 s) become Abandoned (leaked), and shutdown continues regardless. - Save the random seed. With every service stopped and before any filesystem is touched, peinit writes a fresh seed drawn from the kernel's CSPRNG to
/var/state/peinit/random-seed— this is the entropy the next boot starts from. The write is crash-conscious (written to a temporary file, flushed, then atomically swapped in), and it is best-effort: if it fails, peinit logs it and carries on. Shutdown never stalls on it, and the forced and Critical-reboot paths skip it entirely in favour of getting the machine down fast. - Unmount filesystems. peinit snapshots the mount table and tries to unmount every remaining non-root mount in its namespace — not just the ones it mounted itself — working from the deepest mount points outward. That includes the Phase 1 set (
/proc,/sys,/dev,/dev/pts,/dev/shm,/run,/sys/fs/cgroup) wherever those are still mounted. A mount that is already gone counts as done. If one won't unmount because it is busy, peinit falls back to remounting it read-only; anything it still can't clean up is logged and left for the finalsync()and the kernel, never blocking shutdown. The root filesystem (/) is never unmounted — once the rest are handled, peinit remounts root read-only. - Sync and finish.
sync()to flush pending writes, then the final action — power off, reboot, or halt.
The reverse ordering falls straight out of the dependency graph; there is no separate stop-order configuration. Shared platform services stop after their dependents, with registryd last. The exact order is derived from the installed graph.
STOPPING=1 and timeout extension #
A service that begins shutting down on its own — say, in response to an internal error — can tell peinit by sending STOPPING=1 via sd_notify. peinit acknowledges it and, importantly, stops sending SIGTERM to that service: it is already on its way down, and a redundant signal could interfere.
STOPPING=1 is a courtesy, not a timeout extension. The StopTimeout keeps running; if the process has not exited when it expires, peinit escalates to SIGKILL regardless. A service that genuinely needs longer must say so with EXTEND_TIMEOUT_USEC (see Keeping services running) — and during shutdown those extensions are capped not only by the per-service StopTimeout × 4 but also by the remaining global ShutdownTimeout, whichever is smaller.
Forced shutdown #
When the machine must be stopped despite a stuck graceful shutdown, repeated SIGINT forces it: three Ctrl-Alt-Del presses within five seconds skip the graceful sequence entirely — SIGKILL everything, sync, reboot. It is the escape hatch for a graceful shutdown that is itself stuck behind an unresponsive service.
Shutdown during boot #
A shutdown requested while peinit is still in Phase 2 boot takes effect immediately: services still Starting are SIGKILLed (they never reached Active, so there is nothing to stop gracefully), services that did reach Active are stopped per the normal sequence, and the boot is abandoned.
How peinit handles signals #
peinit handles all signals through a signalfd read from its event loop — every signal is blocked and read as data, so there are no async signal handlers and no async-safety hazards. PID 1 cannot be killed by any signal; the kernel protects it.
| Signal | Behaviour |
|---|---|
| SIGCHLD | Reap children; match exits to services and submitted jobs. Also reaps orphaned processes the system reparented to PID 1. |
| SIGINT | Reboot request. Repeated within the window → forced reboot. |
| SIGTERM | Poweroff request. |
| SIGPWR | Poweroff request — compatibility path for power-button/power-failure policy. |
| SIGHUP | Ignored — PID 1 has no controlling terminal. |
| SIGPIPE | Ignored — a broken control-socket pipe must never crash PID 1. |
All other signals are ignored.
Where to start #
To understand the reverse ordering and which relationships gate it, read Dependencies and ordering.
To understand the Critical-failure path that bypasses graceful shutdown, read Keeping services running.
For the boot side of the lifecycle and the reboot/recovery escalation, read Boot and boot modes.
Registry key reference
Peios / Using Peios / Services & jobs
Use this catalog when you already know which setting you need. For a guided change, start with Defining a service, then use svctl definition show and svctl definition validate to inspect and check it.
The tables retain the service-definition schema and every registry key peinit reads or writes. Follow each row’s link for validation rules, safety notes, and when a change takes effect. Saving a correctly typed value does not mean a running process has adopted it.
Registry value types #
Service definition fields map onto registry value types as follows:
| Schema type | Registry type | Is |
|---|---|---|
| string | REG_SZ | A UTF-8 string. |
| multi_string | REG_MULTI_SZ | An ordered list of strings. |
| dword | REG_DWORD | A 32-bit unsigned integer. |
| binary | REG_BINARY | A raw byte sequence. |
| (timestamp) | REG_QWORD | A 64-bit value — used for timer last-run timestamps. |
The service definition schema #
Every service is a key under Machine\System\Services\<name>; these are the values inside it. Only ImagePath is required. Fields are grouped by purpose; for full semantics see Defining a service and the linked pages.
Execution — see The execution environment
| Field | Type | Default | Meaning |
|---|---|---|---|
ImagePath | string | (required) | Absolute path to the service binary. |
Arguments | multi_string | — | Command-line arguments. |
WorkingDirectory | string | / | Working directory (non-empty absolute path). |
TTYPath | string | — | Absolute path to a terminal to attach as the service's stdio and controlling terminal, e.g. /dev/console or /dev/tty1. Absent or empty means the daemon default (/dev/null stdin, captured output). Attaching a terminal suppresses log capture. |
TTYPrecedence | dword | 0 | Who wins that terminal when several services want it at the same moment. Higher wins; ties break on service name. Rejected without a TTYPath. See Triggers and timers. |
Environment | multi_string | — | KEY=VALUE pairs added to the environment. |
RuntimeDirectories | multi_string | — | Directories created directly under /run just before the main process starts, each secured for SYSTEM, Administrators, and the service's own SID. |
LimitNOFILE | dword | — | RLIMIT_NOFILE. |
LimitCORE | dword | — | RLIMIT_CORE, in bytes. |
Type and readiness — see Simple and Oneshot services
| Field | Type | Default | Meaning |
|---|---|---|---|
Type | dword | 0 (Simple) | 0 = Simple, 1 = Oneshot. |
Readiness | dword | 0 (Notify) | 0 = Notify (READY=1), 1 = Alive. Ignored for Oneshot. |
RemainAfterExit | dword | 0 | Oneshot only — stay Completed after a successful exit. |
SuccessExitCodes | multi_string | — | Non-zero exit codes treated as success (each 0–255). |
Activation — see Triggers and timers
| Field | Type | Default | Meaning |
|---|---|---|---|
Triggers | multi_string | — | Any of boot, boot:settled, tty:released (needs a TTYPath) and timer:<schedule>. Absent = demand-only. Bare tty and bare timer are errors, and an unrecognised sub-trigger is rejected rather than ignored. |
Disabled | dword | 0 | If 1, triggers must not activate the service. |
SafeMode | dword | 0 | Eligible within the boot-triggered set in Safe mode. Critical implies SafeMode; neither adds a boot trigger. |
Conditions | multi_string | — | Start-time checks; a failure skips the service. |
Asserts | multi_string | — | Start-time checks; a failure fails the service. |
PreStartCheckTimeout | dword | 5 | Seconds allowed for the helper that evaluates filesystem Conditions/Asserts; if it overruns, it is killed and the check counts as not satisfied (a Condition skips, an Assert fails). |
TimerPersistent | dword | 1 | Catch up a missed timer run after a reboot. |
TimerJitter | dword | 0 | Maximum random delay (seconds) added to each firing. |
Identity — see Service identity and privileges
| Field | Type | Default | Meaning |
|---|---|---|---|
Identity | string | LocalService | Principal name, SID, or Service for the service’s distinct virtual account. |
RequiredPrivileges | multi_string | — | Privilege allow-list; all others are removed from the token. |
HookIdentity | string | (service's Identity) | Identity for ExecStartPre/ExecStartPost. |
Dependencies — see Dependencies and ordering
| Field | Type | Default | Meaning |
|---|---|---|---|
Requires | multi_string | — | Hard dependencies. Each entry may carry a readiness level (netd:routed). |
Wants | multi_string | — | Soft dependencies. |
BindsTo | multi_string | — | Runtime coupling. |
Conflicts | multi_string | — | Mutual exclusion. A level parses here but is inert. |
Provides | multi_string | — | Roles this service fills, so others can depend on the role rather than the implementation. Bare names only — a level is rejected. |
OnFailure | string | — | Service to start when this one enters Failed. |
Supervision and health — see Keeping services running
| Field | Type | Default | Meaning |
|---|---|---|---|
ErrorControl | dword | 0 (Normal) | 0 = Normal, 1 = Critical (sync + reboot on irrecoverable failure). |
RestartPolicy | dword | 1 (OnFailure) | 0 = Never, 1 = OnFailure, 2 = Always. |
RestartMaxRetries | dword | 5 | Consecutive restarts before Failed. |
RestartWindow | dword | 120 | Seconds of Active health that resets the restart counter. |
RestartDelay | dword | 1 | Backoff base (doubles per failure, capped at 60). |
HealthCheck | string | — | Periodic health-check command. |
HealthCheckInterval | dword | 30 | Seconds between health checks. |
HealthCheckTimeout | dword | 5 | Seconds before a check is killed and failed. |
HealthCheckRetries | dword | 3 | Consecutive failures before unhealthy. |
WatchdogTimeout | dword | 0 | Seconds between expected WATCHDOG=1 pings; 0 disables. |
Transition phases — see The service lifecycle and Controlling services
| Field | Type | Default | Meaning |
|---|---|---|---|
ExecStartPre | multi_string | — | Commands run before the binary (sequential; any failure aborts start). |
ExecStartPost | multi_string | — | Commands run after readiness / successful exit (failure logged, not fatal). |
ExecReload | string | — (SIGHUP) | Reload command, or signal:<NAME>. |
StartTimeout | dword | 30 | Seconds for the whole start sequence. |
StopTimeout | dword | 10 | Seconds between SIGTERM and SIGKILL. |
Notify, fds, security, metadata
| Field | Type | Default | Meaning |
|---|---|---|---|
NotifyAccess | dword | 0 (Main) | Who may send sd_notify (only Main is supported). |
FdStoreMax | dword | 0 | Max fds in the fd store; 0 disables it. |
ServiceSecurity | binary | inherit parent | Descriptor controlling who may manage the service. |
DisplayName | string | — | Human-readable name for status display. |
Description | string | — | Description of the service. |
Service definition keys #
| Key | Type | Purpose | See |
|---|---|---|---|
Machine\System\Services\ | (parent key) | Parent of all service definitions; each child key is a service. | Defining a service |
Machine\System\Services\SchemaVersion | dword | Schema version guard (currently 1). | Defining a service |
Machine\System\Services\<name>\LastTimerRun | REG_QWORD | Last-run timestamp for a single-trigger persistent timer. Written by peinit. | Triggers and timers |
Machine\System\Services\<name>\TimerState\ | (subkey) | Per-trigger last-run timestamps for multi-trigger services. Each value is named by the percent-encoded schedule string and holds a REG_QWORD. | Triggers and timers |
Boot configuration #
Under Machine\System\Boot\:
| Key | Type | Default | Purpose | See |
|---|---|---|---|---|
MaxParallelStarts | dword (> 0) | 10 | Maximum services starting concurrently. Missing → default; zero/invalid → Recovery. | Boot and boot modes |
BootSuccessGrace | dword | 30 | Seconds a Critical service must hold a satisfying state before boot is successful. | Boot and boot modes |
ShutdownTimeout | dword | 90 | Maximum seconds for the whole graceful shutdown. | Shutdown |
SettleTimeout | dword | 5 | Seconds to wait for the boot set to settle before starting boot:settled services anyway. | Triggers and timers |
PostKillTimeout | dword | 5 | Seconds peinit waits for a service cgroup to drain after SIGKILL before treating it as stuck. ShutdownTimeout bounds the shutdown as a whole; this bounds one service's last stage of it. | Shutdown |
Operational parameters #
Under Machine\System\Init\:
| Key | Type | Default | Purpose | See |
|---|---|---|---|---|
ControlSecurity | binary | SYSTEM and Administrators all; Authenticated Users query | Descriptor for system-level control operations. | Who can manage a service |
MaxControlConnectionsPerUser | dword | 16 | Maximum concurrent control-socket connections one user may hold; SYSTEM is exempt. | Controlling services |
MaxControlConnections | dword | 256 | Maximum concurrent control-socket connections in all. | Controlling services |
MaxRequestSize | dword | 65536 | Maximum control-socket request size (bytes). | Controlling services |
ConnectionTimeout | dword | 30 | Seconds before an idle control connection is closed. | Controlling services |
MaxJobsConnections | dword | 64 | Maximum concurrent jobs-socket connections. | Controlling services |
MaxJobMessageSize | dword | 32768 | Maximum jobs-socket message size (bytes); bounds a submission's whole definition. | Controlling services |
JobsConnectionTimeout | dword | 30 | Seconds before an idle jobs connection is closed. | Controlling services |
MaxJobsPerSubmitter | dword | 64 | Live submitted jobs one submitting SID may hold; SYSTEM is exempt. | Jobs and operations |
MaxLogLineLength | dword | 8192 | Maximum bytes per service output line before truncation. | Service output and logging |
MaxLogBufferPerService | dword | 65536 | Maximum bytes buffered per service pipe before back-pressure. | Service output and logging |
LogReadBytesPerEvent | dword | 16384 | Bytes drained from a service's output pipe per readable event. Larger favours throughput on chatty services, smaller favours fairness between them. | Service output and logging |
PreEventdBuffer | dword | 1048576 | Total bytes of service output held in memory before eventd is up to receive it. Overflow drops the oldest output; it never blocks a service. | Service output and logging |
EnvVars\ | (parent key) | empty | Default environment variables injected into Phase 2 services (value name = variable, REG_SZ data = value). The descriptor here is security-critical. | The execution environment |
ProvisionedPaths\ | (parent key) | empty | Boot-time path provisioning: each child key names one directory or file that peinit creates and secures after registryd starts and before Phase 2 — the registry-backed equivalent of tmpfiles.d. | Boot and boot modes |
ProvisionedPaths\<name>\Kind | string | (required) | directory or file — what to create at Path. | Boot and boot modes |
ProvisionedPaths\<name>\Path | string | (required) | Absolute path to create or verify. | Boot and boot modes |
ProvisionedPaths\<name>\Security | binary | SYSTEM + Administrators full, users read/traverse | Peios file security descriptor to apply to the object. | Boot and boot modes |
ProvisionedPaths\<name>\Required | dword | 0 | If 1, a failure to provision this entry sends boot to Recovery mode before Phase 2 starts. | Boot and boot modes |
Keys peinit reads from other subsystems #
LogSocketPath is read by peinit. MaxLogDatagramBytes is included for related eventd configuration; it is not a peinit input.
| Key | Type | Purpose | See |
|---|---|---|---|
Machine\System\eventd\LogSocketPath | string | Path of eventd's log datagram socket, where peinit forwards service output. | Service output and logging |
Machine\System\eventd\MaxLogDatagramBytes | dword | eventd’s receive ceiling; default 262144, valid range 4096–1048576. peinit does not read this setting; it uses the portable 262144-byte ceiling or its socket limit, whichever is smaller. | Service output and logging |
Where to start #
For how these fields combine into a working definition and when changes take effect, read Defining a service.
To look up the meaning, valid range, and effect-timing of any key on a live system, use regman.
Networking
Peios / Using Peios / Networking
Start with net status to see whether an interface has joined a network,
which profile it uses, and whether it has an address and a default route.
On the desktop, open Network Manager from the launcher by typing
network.
net reports interface configuration; resolv reports the name servers
actually in use. For a firewall change, an administrator also checks
net policy. A route, a DNS answer and permission to reach a service are
separate checks.
Choose the next check #
| What you see | Next step |
|---|---|
No carrier, or readiness absent | Check the cable or link and the interface's verdict in net status. |
IGNORE, DOWN, or no profile | Inspect net rules and choose a profile safely. |
Carrier, but no address or only 169.254… | Check the profile and DHCP warning; follow connectivity troubleshooting. |
| An address, but no default route | Check Route.Offered or the pinned Route.Gateway, then read back the result. |
| An address works but a name does not | Use name-resolution checks. |
| A particular service is unreachable | Check its listener, port reservation and firewall decision. |
policy REFUSED | The last good interface configuration still applies. Fix the named rule or profile and check again. |
The net command reference explains its output and access rights. The diagnostic tools guide covers probes available in Experimental.
The registry is the truth #
Network settings live under Machine\System\Network. Change them through
Network Manager or the
profile and rule keys. netd reads
those settings and applies them live; there is no interface configuration
file or reload command.
Do not add an address by hand to a joined interface: netd removes addresses that its profile does not request. Routes added by another program are left alone unless they carry netd's own routing-protocol tag. The kernel's IPv6 link-local address is left alone too. The exact ownership and apply order are in Reconciliation.
The netd loop and startup and restart chapters describe how registry, kernel, lease and timer changes are reconciled.
Rules say which, profiles say how #
A rule under Rules\Interface selects an interface and gives it one of
three outcomes:
JOIN(profile)uses a profile underProfiles\.IGNOREleaves the interface as it is, including existing settings.DOWNkeeps the interface administratively down.
A profile supplies settings such as Address.Static, Route.Gateway and
Dns.Servers. Each Offered value decides whether to accept that part
of the network's offer. An offer is a claim from the network, not proof
that it is trustworthy. A child profile inherits its parents' settings
and replaces only the values it names.
A profile can serve several interfaces at once; there is no separate
activation step. Rules can match stable interface identity or the network's
name and trust label. A shipped wired rule at priority 10 assigns all
wired interfaces to default, which accepts offered addresses, routes
and DNS and enables link-local fallback. A bare profile accepts none of
those offers. No matching rule means IGNORE.
See Configuring profiles for examples and Network policy for exceptions, priorities and conflicts.
The inventory and the networks #
Use net status for the current link, address, lease and network. The
interface id remains stable for the same card in the same slot even if its
kernel name changes across boots.
The registry also keeps records under Interfaces\<id> and
Networks\<id>. netd's findings are in their protected Status subkeys;
hand edits there are refused. On a network record, you can set Name and
Trust. Rules for both profiles and the firewall use those labels, so
changing one can change connectivity.
Network identification currently uses the DHCP server and subnet, or an advertising router and prefix. It does not prove a network's identity. The inventory and network records chapters list the persisted fields; lease timers and current offered configuration are live daemon state, not a lease to restore from the registry.
Addresses #
Choose these in a profile, then check the result in net status:
| Need | Setting and effect |
|---|---|
| Accept the network's address | Address.Offered: DHCPv4 and IPv6 router discovery, limited by Address.Families. IPv6 uses stable-privacy addresses; DHCPv6 supplies DNS information, not addresses. |
| Set an address yourself | Address.Static: CIDR addresses of either family. Set the gateway separately with Route.Gateway, or accept it with Route.Offered. |
| Reach another machine on a cable without DHCP | Address.LinkLocal: a 169.254/16 fallback while DHCP discovery continues; removed when a lease arrives. |
| Add rotating IPv6 addresses | Address.Temporary: daily-rotating addresses alongside the stable address. |
An expired DHCP lease is dropped by default. Address.OnExpiry = Keep
trades address-conflict risk for continuity. A network's RequestedAddress
is only the address to ask for first next time; the server can refuse it.
A deprecated IPv6 address remains for existing connections but is not chosen for new ones; it is removed when its valid lifetime ends. Stable address derivation, temporary-address rotation and the protective two-hour lifetime rule are documented in IPv6 addresses. See DHCPv4 for acquisition, renewal and fallback details.
Readiness is a level, not a boolean #
| Level | What it tells you |
|---|---|
absent | The joined interface is down or has no carrier. |
link | It is up with carrier, but has no usable address. |
addressed | It has a usable address, but no default route. A 169.254… address counts. |
routed | It has a usable address and a default route. This does not test Internet or DNS reachability. |
The machine reports the highest level among joined interfaces. Readiness
is also published on Machine\System\Network and each joined interface's
Status for scripts and services.
net wait routed waits for a default route. A service may declare
Requires = ["network:routed"]. These waits differ: net wait accepts
at least the requested level, while peinit matches the published service
level exactly. See Readiness
before depending on network:addressed.
What netd does not do #
- Answer DNS. resolvd receives netd's live server and domain information;
use name resolution to configure and
diagnose it. netd writes neither
/etc/resolv.confnor/etc/hosts. - Filter traffic. The kernel enforces PNP's packet layers. DHCP and router-discovery traffic need the baseline's visible, deletable rules.
- Grant port ownership. Port reservations decide who may bind a port; the firewall decides who may reach it.
- Associate Wi-Fi. A supplicant must bring up a wireless link before netd can configure it. Wi-Fi is unfinished: wireless is absent from the baseline, Network Manager shows it as Not managed, and wireless networks are not yet identified by name.
Where to start #
- Configure a profile, apply it through a matching rule, and read back the accepted settings.
- Use Network Manager for desktop configuration, change previews and its documented rollback flow.
- Troubleshoot a symptom.
- Look up exact settings in
regman 'Machine\System\Network'or the network policy reference. - Use the netd technical reference manual for daemon algorithms, protocol exchanges, timers and limits.
Configuring profiles
Peios / Using Peios / Networking
Network configuration is two kinds of key under Machine\System\Network. A profile, a subkey of Profiles, says how an interface stands on a network: what to take from the network's offer and what to dictate. A rule, a subkey of Rules\Interface, says which interfaces stand in which profile. netd watches the subtree, so a change takes effect within a moment of the write — no restart, no reload command.
Follow this order: inspect the active interface, prepare the profile, select it with a rule, then read back the accepted state. For desktop changes, use Network Manager, whose profile editor shows inherited values and the interfaces a change affects.
Every value is documented on the machine, regman 'Machine\System\Network',
and in the reference.
Before you change anything #
- You need write access to the particular registry keys. Reading status
does not grant permission to change configuration; protected
Statuskeys are netd's output, not settings to edit. - Record the current rule, profile and values so you can restore them. Keep local console access or another recovery path if this network carries your remote session.
- Profile and rule writes take effect live. Changing a profile's resolved values restarts its interfaces' clients, even for a DNS-only profile edit. No reboot or daemon reload is required.
- Use a derived profile for one interface. Editing
defaultaffects every interface using it and descendants that inherit the edited value. - The commands below are examples. Replace addresses, names, ids and bus
paths with your own. The
vpn,office,laptop,untrustedandhomeprofile examples assume those profiles already exist.
See what you have #
net status
net rules
net profiles
Keep this output for comparison after the change. net status shows each interface with its verdict: the rule that spoke and the profile it stands in. An image's baseline is a profile default that takes the network's address, way out and name servers, and a rule wired that puts every wired interface in it at priority 10.
A static address #
Create the profile before a rule names it. This example derives default/db1
from default, so values not listed here remain inherited, including
Dns.Offered and Address.LinkLocal. Lists replace inherited lists; they
do not append. An absent value inherits, while a present empty value means
none.
Confirm that 10.0.0.5/24 is appropriate and available on your network and
that 10.0.0.1 is its gateway before adapting these example writes:
reg new Machine/System/Network/Profiles/default/db1
reg set Machine/System/Network/Profiles/default/db1 Address.Offered dword:0
reg set Machine/System/Network/Profiles/default/db1 Address.Static multi:10.0.0.5/24
reg set Machine/System/Network/Profiles/default/db1 Route.Offered dword:0
reg set Machine/System/Network/Profiles/default/db1 Route.Gateway 10.0.0.1
Then a rule naming the interface by its bus path (from net status, stable across boots), as an exception under the baseline's wired rule so it is judged only for wired interfaces:
reg new Machine/System/Network/Rules/Interface/wired/db1
reg set Machine/System/Network/Rules/Interface/wired/db1 Interface.Path.Equal pci-0000:00:03.0
reg set Machine/System/Network/Rules/Interface/wired/db1 Actions 'multi:JOIN(default/db1)'
The exception is more specific than wired, so it selects default/db1
for that interface and stops offered address acquisition. Check for
JOIN(default/db1) by wired/db1 and the intended address and gateway in
net status; readiness routed appears once both a usable address and
the default route are in place.
Inherited Dns.Offered is a setting, not evidence that a server is
available after address acquisition stops. Check resolv status. To pin
DNS as well, set Dns.Offered = 0 and Dns.Servers on this derived profile.
The inherited link-local setting does not add a fallback beside a static
address. See desired state.
Verify the accepted settings #
- Confirm the rule names the intended profile and matches the intended
interface.
net profileslists each profile's own values, not a flattened result; account for inherited values. - Check that
net statushas nopolicy REFUSEDline. Read the active verdict, rule and profile, then the address, gateway, lease and warnings. - Check the servers and domains in
resolv status, and test a relevant name withresolv query <name>. - Test the service you actually need. Readiness
routedproves a default route exists, not that a remote service or the Internet answers.
If you also changed firewall rules or a network's Name or Trust, an
administrator should run net policy wait and check net policy.
That verifies the kernel's packet policy; it does not substitute for
checking netd's accepted profile in net status.
If the result is wrong, restore the values you recorded and repeat the
checks. net reconcile requests another pass; it does not undo a write.
net renew <interface> asks to renew an existing DHCP lease without
releasing it first. Both need NETWORK_CONTROL, granted to SYSTEM and Administrators by default.
Facts a rule can name #
Every <Fact>.<Operator> value present must hold. Comparison is exact; Equal takes a list for any of.
| Fact | Matches | Example |
|---|---|---|
Interface | the kernel's interface name | enp0s3 |
Interface.Kind | wired, wireless, loopback, other | wired |
Interface.Id | the stable id from net status | 3f2a1b8e-… |
Interface.Mac | the hardware address | 52:54:00:12:34:56 |
Interface.Path | the bus path | pci-0000:00:03.0 |
Interface.Driver | the kernel driver | virtio_net |
Network.Name | the Name you gave the network's record | palfrey-home |
Network.Trust | the Trust you gave it | corporate |
Prefer Interface.Path or Interface.Id for a specific card and Interface.Kind or Interface.Driver for a class. Interface (the name) is the least stable: the kernel may call the same card something else after a hardware change. A fact the interface lacks — a tunnel has no Path — makes the condition false, never an error.
Taking the offer, with adjustments #
default believes the network about the address, the way out and the name servers. To keep the address but pin DNS, override two values in a derived profile and point a rule at it — or, if the whole machine should behave so, edit default itself:
reg set Machine/System/Network/Profiles/default Dns.Offered dword:0
reg set Machine/System/Network/Profiles/default Dns.Servers multi:1.1.1.1,9.9.9.9
Three more Dns values decide which names an interface's servers are asked for, which matters the moment a second interface appears — typically a VPN:
reg set Machine/System/Network/Profiles/vpn Dns.Domains multi:corp.example
reg set Machine/System/Network/Profiles/vpn Dns.Default dword:1
reg set Machine/System/Network/Profiles/vpn Dns.Exclusive dword:1
Dns.Domains sends names under corp.example to this interface's servers. Dns.Default sends every other name there too (unset, an interface claims that only when it carries the default route). Dns.Exclusive takes all queries only while the interface is at least addressed and supplies a DNS server. Without those conditions it does not prevent another interface from being chosen. Verify the VPN's addresses and server lines before relying on it to keep names off another network. See name resolution.
To hold an offered address across a server outage:
reg set Machine/System/Network/Profiles/default Address.OnExpiry Keep
Keep trades the risk of an address conflict for continuity; the default Drop is the safe choice.
IPv6 #
Address.Offered covers both families: on a network with router advertisements the interface gets a stable-privacy address per advertised prefix, and with Route.Offered and Dns.Offered the advertised default route and DNS from RDNSS or stateless DHCPv6. Three values change that:
reg set Machine/System/Network/Profiles/office Address.Families multi:ipv4
reg set Machine/System/Network/Profiles/laptop Address.Temporary dword:1
reg set Machine/System/Network/Profiles/office Route.Gateway multi:10.0.0.1,fe80::1
Address.Families = ipv4 stops router discovery on interfaces in the profile and drops IPv6 entries from Address.Static. Address.Temporary adds daily-rotating temporary addresses beside the stable one, for machines whose traffic should not correlate over days; the stable address remains for inbound. An IPv6 entry in Route.Gateway — usually a link-local address — pins the default router instead of believing advertisements.
The stable address survives reboots and reinstalls that keep /var/state/netd; it is not derived directly from the MAC, and a different network sees an unrelated address. The IPv6 address chapter documents derivation, lifetimes and rotation.
Prefer a cable over WiFi #
When both interfaces have default routes, the metric decides which is used. Wired defaults to 100 and wireless to 600, so no configuration is needed. To invert it, set Route.Metric on the profiles. This describes route preference once links exist; Wi-Fi association is not implemented by netd, wireless is not in the baseline, and Network Manager currently shows wireless as Not managed.
Name a network, and behave differently on it #
Once an interface has stood on a network, net status shows the record's id. Give it a name and your word on it:
reg set Machine/System/Network/Networks/<id> Name palfrey-home
reg set Machine/System/Network/Networks/<id> Trust home
Changing either label can change both the selected profile and firewall access. Check the active interface and packet policy afterwards.
The following illustrates choosing a profile by network. It is not a
Wi-Fi setup procedure: a supplicant must supply the link, and wireless
network-name identification is unfinished. Create the untrusted and
home profiles before a rule names them:
reg new Machine/System/Network/Rules/Interface/radio
reg set Machine/System/Network/Rules/Interface/radio Interface.Kind.Equal wireless
reg set Machine/System/Network/Rules/Interface/radio Actions 'multi:JOIN(untrusted)'
reg new Machine/System/Network/Rules/Interface/radio/home
reg set Machine/System/Network/Rules/Interface/radio/home Network.Name.Equal palfrey-home
reg set Machine/System/Network/Rules/Interface/radio/home Actions 'multi:JOIN(home)'
Until a network has been identified on the link, the Network.* facts are absent, so the radio stands in untrusted first and moves to home once the network is known. Trust is your label, not verified evidence of network identity: a rule that conditions on it is stating that you accept the identification. The same word reaches the firewall: a Flow rule with Network.Trust.Equal = home sees the network the radio is on, so what you open at home stays shut at the cafe (see the network context).
Hand an interface to something else #
An IGNORE verdict leaves a matched interface alone: no new addresses, routes, link changes or DHCP. If it was previously joined, existing addresses and routes remain; IGNORE is not a cleanup action:
reg new Machine/System/Network/Rules/Interface/leave-alone
reg set Machine/System/Network/Rules/Interface/leave-alone Interface.Equal tap0
reg set Machine/System/Network/Rules/Interface/leave-alone Actions multi:IGNORE
Take an interface down #
A DOWN verdict keeps a matched interface administratively down. DOWN wins equal-priority ties, but a higher-priority rule still wins. Check existing priorities before using the example priority 100:
reg new Machine/System/Network/Rules/Interface/dead-card
reg set Machine/System/Network/Rules/Interface/dead-card Interface.Id.Equal <id>
reg set Machine/System/Network/Rules/Interface/dead-card Priority dword:100
reg set Machine/System/Network/Rules/Interface/dead-card Actions multi:DOWN
The interface id is in net status. Delete the rule, or set Enabled = 0 on it, to let the remaining rules decide again. Verify that they select the intended profile; removing DOWN alone does not guarantee a JOIN.
Ask for the same address #
The address a network last leased is on its record, and netd asks for it first. Write it yourself for a soft reservation — a request the server may refuse, unlike a static address:
reg set Machine/System/Network/Networks/<id> RequestedAddress 10.0.2.50
Set the hostname #
reg set Machine/System/Network Hostname workshop
netd applies it on the next pass. A profile with Hostname.Offered on adopts a network-supplied name when this value is unset; Hostname.Announce tells DHCP servers the name so their DNS can register it. Both are off in a bare profile.
Each change netd makes to the machine's name is recorded in the event log as netd.hostname.changed, with the name before and after (config.text-previous and config.text). The netd manual's hostname chapter says exactly when.
netd sets whatever is written, but a network carries the name as one label: letters, digits and hyphens, at most 63 of them, not beginning or ending with a hyphen, and not localhost. System Settings holds it to that as it is typed: in its About section, Machine Name says what is wrong with a name before it can be applied, and shows the name the machine has now if netd hasn't applied the new one yet; Apply appears once the name has been changed. The About section also shows what the machine is and runs: the system and its edition, the kernel, how long it has been up, the hardware, and the disks.
When netd refuses #
An unknown fact, a JOIN naming a missing profile, or an unknown profile
value refuses the whole new interface generation. netd keeps the last good
one and net status reports policy REFUSED with the reason. Fix the named
entry, then verify the next write was accepted; the registry listing alone
does not tell you what is active.
Two equal-priority rules selecting different profiles also refuse a runtime reload when the conflict affects an existing interface. At startup, or for an interface that appears later, the tied interface is instead ignored and its warning names both rules. Make one rule unambiguously win. The generation and interface judgment chapters describe that distinction.
A JOIN naming an existing but disabled profile abstains; its parent,
another rule or the backstop answers. Disabling a profile does not exempt
malformed values in it from validation.
Where to go next #
- The net command — read back what netd did.
- Networking — the model behind these keys.
- Network policy reference — every fact, verdict and profile value.
- The interface layer as netd runs it, in the netd technical reference manual — how netd reads these keys, builds a generation from them and judges each interface.
The net command
Peios / Using Peios / Networking
Use net status first. It shows what netd accepted and what is present on
each interface. Use net rules and net profiles to compare that state
with the registry configuration. On the desktop, the same starting point
is Network Manager's Overview.
net does not write profile or rule configuration. Follow
Configuring profiles to change it,
then return here to verify the result.
After a change #
- Profile or interface rule: check
net statusfor the intended verdict, rule and profile, nopolicy REFUSEDline, and the expected addresses and gateway.net profilesshows each profile's own values, so inherited settings still matter. - DNS: check
resolv statusand a relevantresolv query <name>. - Firewall rule or network label: as an administrator, run
net policy wait, thennet policy. A returned registry write is not yet proof that the kernel accepted it. - Connectivity: test the required service.
routedmeans there is a usable address and a default route, not a successful Internet probe.
The interface and packet policy are accepted independently. A successful
net policy wait does not prove that netd accepted a profile; a good
net status does not prove that the firewall accepted a rule.
Reading net status #
hostname workshop
readiness routed
enp0s3 [3f2a1b8e-…]
verdict JOIN(default) by wired
state up, carrier
readiness routed
hardware 52:54:00:12:34:56 pci-0000:00:03.0 virtio_net
network palfrey-home [9c0d4e21-…] trust home
address 10.0.2.15/24
gateway 10.0.2.2
dns 10.0.2.3
lease bound from 10.0.2.2, 86395s left
verdict is what the interface layer said and who said it: the verdict, the profile (for JOIN), and the rule path that spoke — backstop when no rule did. readiness is absent (down or no carrier), link, addressed or routed, shown for joined interfaces; the machine's is the highest among them. network is the record under Machine\System\Network\Networks identified on the link, with the Name and Trust you gave it.
The bracketed value after the name is the interface id — the inventory key under Machine\System\Network\Interfaces.
Read the interface block, not just the machine's readiness: another
interface can make the machine routed while this one is unusable. An
IPv4 link-local address counts as addressed; IPv6 link-local alone does
not. Only joined interfaces contribute to machine readiness.
A warning line can mean DHCP discovery went unanswered or two rules
tied and this interface is ignored. policy REFUSED: means the newest
interface generation was rejected and the last good one stands. Correct
the named rule or profile and read status again. A listed registry write
is not proof it took effect.
Commands #
| Command | Does |
|---|---|
net status | Hostname, the machine's readiness, whether the newest generation was refused, and every interface: verdict with the rule and profile behind it, link state, readiness, hardware identity, the network identified on it, addresses, gateway, DNS, lease, warnings. |
net wait <level> [seconds] | Block until the machine reaches link, addressed or routed, or the timeout (default 60) passes. For scripts that need the network. |
net renew <interface> | Renew the DHCP lease now. Accepts a kernel name or an interface id. |
net reconcile | Re-run the reconciler immediately rather than waiting for an event. |
net policy | The kernel's side of the packet policy: whether the filtering engine (NTFE) is enforcing, its generation, whether every change written so far is in force, whether the last re-read was refused, and how many interfaces have a network context. |
net policy wait [seconds] | Block until every change made to Machine\System\Network before the command started is in force in the kernel, or the timeout (default 10) passes. Fails if the kernel refused the new generation. For scripts that write a rule and then rely on it. |
net rules | The interface layer, one rule per line: path, priority, conditions, actions. |
net profiles | The profile tree, one profile per line with the values it sets itself. |
Choose renew or reconcile carefully #
net renew <interface> asks a DHCPv4 client holding a lease to renew it
without releasing its current address first. A client with no lease is
left as it is; a successful request alone does not prove a new lease
arrived. Check the lease afterwards. The argument may be a kernel name or
the stable interface id shown by net status.
net reconcile asks netd to apply the configured desired state now.
It does not reload a different configuration, undo a write, or make an
invalid gateway reachable. Neither operation requires a reboot.
Do not substitute a daemon restart for these commands: the restart chapter describes leased addresses and routes disappearing until DHCP responds, and IPv6 autoconfiguration waiting for advertisements again.
Who may run it #
status, wait, renew and reconcile reach netd through /run/netd/control.sock. The control object is a security descriptor: Machine\System\Network\ControlSecurity, or a compiled default when that is unset. status and wait need NETWORK_QUERY, which the default grants to everyone. renew and reconcile need NETWORK_CONTROL, which the default grants to SYSTEM and Administrators. rules and profiles read the registry directly and need only read access to it. policy and policy wait ask the kernel through /dev/peios-ntfe, which ordinary users may not open. A denied request reports access denied.
Exit status #
| Code | Meaning |
|---|---|
| 0 | Done; for wait, the level was reached; for policy wait, the changes are in force. |
| 1 | netd refused or could not be reached; for wait, the timeout passed; for policy wait, the timeout passed or the kernel refused the generation. |
| 2 | Usage error. |
See also #
- Networking
- Configuring profiles
regman 'Machine\System\Network'on the machine, for every key netd reads and writes.- netd's control socket, in the netd technical reference manual: the requests behind every
netcommand, and their replies.
Network diagnostic tools
Peios / Using Peios / Networking
Start with the symptom below. Run read-only checks before changing profiles, firewall rules or service state, and keep the output that explains the failure. The tools retain your native identity and access restrictions; installing a tool does not grant network or capture privileges.
Start with the symptom #
| Symptom | Check | What to do with the result |
|---|---|---|
| No connectivity | net status | Find the affected interface's carrier, verdict, profile and readiness. IGNORE or DOWN is a profile-rule question; no carrier is a link question. |
| Carrier, but no DHCP address | net status, then net profiles and net rules | Check Address.Offered, Address.Families and the selected rule. An unanswered-discovery warning can mean no server or blocked DHCP traffic. |
Only a 169.254… address | net status | This is link-local fallback, not a routed lease. DHCP discovery continues. Check server reachability and the baseline DHCP rules. |
| Address present, no gateway | net status | Check Route.Offered or your pinned Route.Gateway. An unreachable pinned gateway is logged as a failed route addition. |
| IP connection works, names fail | resolv status, resolv query <name> | Check servers, domains, netd connection, and notfound versus unavailable; use DNS troubleshooting. |
| One TCP service fails | ss -lnt on the receiving machine; nc -vz 192.0.2.1 443 from the caller | Confirm the listener and intended address/port, then check native port authority and PNP. Replace the example address and port. |
| Settings were written but behavior did not change | net status; as administrator, net policy wait and net policy | Check interface and firewall acceptance separately. A refused generation leaves the previous one active. |
| TLS certificate errors | Strict OpenSSL check | Check both trust and the expected peer identity. SNI alone is not hostname verification. |
For daemon errors, use the service logs:
If a profile or rule is wrong, follow the safe change checklist and verify it afterwards. Restarting netd can remove offered addresses and routes while it acquires them again; it is not the first connectivity test. The netd failure modes and resolvd failure modes give the detailed failure paths.
Choose a tool #
Experimental includes these tools alongside net, resolv and trust:
| Command | Purpose | Example |
|---|---|---|
rsync | Synchronize file contents locally or over SSH | rsync -rlt source/ destination/ |
openssl | Inspect TLS, certificates and cryptographic material | openssl x509 -in certificate.pem -noout -text |
curl | Transfer data using URLs | curl https://example.org/ |
wcurl | Download files using curl | wcurl https://example.org/file.txt |
ping | Check ICMP echo replies and round-trip time | ping -c 3 192.0.2.1 |
tracepath | Investigate a route and its path MTU | tracepath 192.0.2.1 |
dig | Send DNS queries to a chosen DNS server | dig @127.0.0.53 example.org A |
ss | Inspect sockets | ss -lnt |
nc | Send or receive TCP and UDP streams | nc -vz 192.0.2.1 443 |
whois | Query domain and IP address registration records | whois example.org |
iperf3 | Measure TCP or UDP throughput between cooperating hosts | iperf3 -c 192.0.2.1 |
socat | Relay data between sockets, files, terminals and programs | socat - TCP:192.0.2.1:443 |
tcpdump | Capture packets or decode saved captures using libpcap | tcpdump -nr capture.pcap |
mtr | Repeatedly probe a route and report latency and loss | mtr -r -c 10 192.0.2.1 |
traceroute | Probe each hop towards a destination | traceroute -n 192.0.2.1 |
arping | Send ARP probes on a local IPv4 link | arping -I eth0 -c 3 192.0.2.1 |
nmap | Discover hosts and inspect listening services | nmap -sT -Pn -n -p 443 192.0.2.1 |
These are command examples, not an instruction to scan or capture an unrelated network. Test hosts and traffic you are authorized to inspect. The documentation addresses above are examples; replace them with the host you want to test. net remains the interface to netd for network configuration and status. The iproute2 package supplies ss; it does not add ip or tc as alternative configuration tools.
Echo and network policy #
Ordinary IPv4 and IPv6 echo datagram sockets are available to every principal. ping needs neither a privileged executable nor a raw socket for its ordinary echo operation. Peios does not use Linux's POSIX group-range setting to decide who may open these echo sockets.
PNP still evaluates the resulting packets. Rules can block requests and replies, inbound or outbound, including loopback traffic. A missing reply can mean filtering, routing trouble or a host that does not answer echo; it does not by itself prove that the host is down. Ordinary echo never receives the bypass reserved for refusal packets generated by PNP itself.
Raw sockets remain subject to the existing kernel authority checks. Options that require raw sockets can therefore fail for an ordinary caller. The package grants no raw-packet privilege and does not change the caller's token.
Routes, capture and scanning #
Ordinary UDP traceroute and echo-datagram probes do not require raw-socket authority. MTR uses the ordinary echo-datagram fallback when raw sockets are unavailable. Other probe modes can require enabled SeTcbPrivilege, the existing Peios authority for raw and packet sockets. This applies to live tcpdump capture, ARP probes and Nmap's raw scanning modes. arping is part of iputils and operates on the local IPv4 link; it does not trace routed paths.
Nmap selects its default probing behaviour from the caller's enabled native privilege, rather than its projected Unix UID. Use -sT -Pn for ordinary TCP connect scans that skip raw host discovery. --privileged and NMAP_PRIVILEGED only select behaviour; they grant no authority. Nmap includes its NSE scripts and data, but this package does not install Ncat or Nping. NSE scripts run with the invoking principal's access. User data lookup uses an absolute $HOME/.nmap, with normal FACS checks.
All these tools retain the invoking token. There are no set-ID helpers, file capabilities, automatic privilege grants or new PNP exceptions. tcpdump's Unix -Z privilege-drop option is rejected because changing a projected UID does not reduce native authority. Launch capture with the intended native authority from the outset. Probe traffic remains subject to PNP; a capture filter only selects what is recorded and is not a firewall rule. Seeing a packet in a capture does not prove that a later policy check allowed delivery.
Named libpcap savefiles, including tcpdump -w output, and Nmap's named scan logs are created with an atomic, protected owner-only FACS descriptor. An existing output must be a regular file with one hard link, owned by the effective principal and protected by an owner-only descriptor. Shared destinations, symlinks and special files are refused before truncation or append. Capture rotation creates each new file under the same rule. Choose a directory you control: private file contents do not stop another principal with directory authority from replacing entries.
Reading captures requires ordinary FACS read access, without raw-socket privilege. Output to -, shell redirection, caller-supplied libpcap streams and NSE scripts' own file operations follow the destination's existing policy. They are not converted into private named logs by these patches.
Socket and terminal relays #
socat supports TCP, UDP, Unix sockets, files, PTYs, program execution and OpenSSL TLS. It starts listeners or programs only when explicitly requested. They retain the caller's native identity, port authority and PNP restrictions. Unix ownership, mode, umask, identity-switch, chroot and network-namespace address options are rejected during option parsing; those options cannot establish native isolation. Configure FACS and native launch authority separately.
Regular relay files use the destination's normal FACS policy. Place sensitive output in a private directory. TLS uses OpenSSL trust handling; supply the intended peer name and trust anchors when using an explicit private service. TUN, POSIX message queues, readline and libwrap support are not included in this build.
DNS and socket inspection #
resolv asks resolvd about Peios name resolution. dig sends a DNS query directly, which is useful for inspecting the local stub or comparing an explicitly named server. Use @127.0.0.53 to query resolvd's local stub. Direct DNS requests still traverse PNP and do not change the system's DNS configuration.
ss reports the socket information the kernel exposes to the caller. Any numeric Unix UID shown is a compatibility projection, not a Peios SID or proof of the effective principal. Use PNP's identity information when investigating native ownership or policy decisions.
Transfers and saved state #
curl uses OpenSSL's system trust paths, maintained by trustd. Its cookie jar, HSTS cache and Alt-Svc cache are created atomically with a protected FACS descriptor belonging to the effective principal. A second principal does not gain access merely because it has the same projected Unix UID. Failure to create that descriptor does not fall back to Unix mode bits.
Normal downloaded documents use the destination's usual FACS policy. Shared writable directories can still allow another principal to remove or replace directory entries; save sensitive state in a directory you control. curl's saved-state files must be regular writable file destinations rather than special devices.
nc is the OpenBSD implementation with Debian's portability patches. This package does not supply its optional TLS mode. It listens only when explicitly invoked in a listening mode; installing it starts no service. Listeners and connections remain subject to native port reservations and PNP.
Registration queries and throughput #
whois queries remote registration servers. The package includes the query client, without the separate mkpasswd program. Domain names must be ASCII or already converted to their punycode form; this build does not include an IDN conversion library. The optional /etc/whois.conf compatibility file can override server selection. Queries and any referrals remain subject to PNP.
Start iperf3 -s on the receiving host, then run iperf3 -c HOST on the sending host. Add -u for UDP, -R to reverse the data direction, or -P 2 for two parallel streams. The default server port is 5201. The invoking principal must have the necessary port authority, and PNP must allow both the control connection and the test traffic. Installing the package starts no service and grants no port reservation, privilege or policy exception.
iperf3 uses anonymous in-memory files for its stream buffers. Received -F files and PID files are created with a protected, owner-only FACS descriptor for the effective principal. An existing destination must be a regular file with one hard link, owned by that principal and already protected by an owner-only descriptor; shared files, symlinks and special files are refused before truncation. Send-side -F files and authentication inputs use ordinary FACS access checks. Log files and redirected output follow their destination's usual policy, so choose a private directory when results are sensitive.
Optional RSA authentication restricts who may run a measurement against that server; it does not create a Peios logon or encrypt the measured traffic. Authentication debug output omits decoded credentials. Protect the server's private key and authorized-user file with FACS and use a directory you control for saved state.
rsync and OpenSSL
Peios / Using Peios / Networking
Use rsync -rlt for content synchronization and an explicit verification
mode for TLS diagnostics. Before either, choose a destination you control
and confirm the identity and access the command will use.
| Task | Start here | Check afterwards |
|---|---|---|
| Copy directory contents locally or over SSH | Synchronizing contents | Check the destination contents and native access policy. rsync is not a security-descriptor backup. |
| Create private key or other secret output | Private outputs | Use the tool's named-output option and a private directory; shell redirection has different protection. |
| Diagnose a TLS server | TLS diagnostics | Require successful certificate verification for the intended DNS name or IP identity. |
Synchronizing contents with rsync #
The Peios rsync port synchronizes contents between local directories or over SSH. Replace the example paths, account and server; an existing destination file may be replaced:
-rlt copies directories recursively, carries symbolic links and preserves modification times. Add -X to copy ordinary user.* extended attributes. Security descriptors, signatures, capabilities and other security attributes are excluded from ordinary xattr synchronization, including when explicit xattr filters are supplied.
A new regular file receives the destination directory's native inheritance. An updated regular file retains the destination's complete security descriptor. Source Unix modes, UID and GID do not define the destination's Peios access policy. Archive/security-preservation requests such as -a, -p, -o, -g, ACL preservation, --chmod and --fake-super are rejected. Native cp -a requests supported preservation attributes, with component and error-reporting limits; it does not establish a complete security backup either. rsync is not yet a native security backup/restore tool.
Ordinary replacement stages a private inode in the destination filesystem. Publishing a replacement requires permission to read and restore the destination's full security, including audit metadata. If those rights are unavailable, the transfer fails without publishing a replacement. rsync does not silently drop inaccessible descriptor components or change to in-place writing. It checks for destination inode and descriptor changes before publication, but that check and the final rename are not a single security transaction. Coordinate concurrent permission changes separately.
For an existing file you are authorized to write, you can explicitly choose:
An in-place update retains the original inode and its security descriptor, and also affects its other hardlinks. It is not atomic: interruption can leave partially updated contents. Use it only when that tradeoff is appropriate.
This initial port rejects alternate staging/partial/backup modes, delayed updates, hardlink preservation, basis-directory optimization, special-file preservation, batch mode and insecure symlink traversal. It also refuses replacements that would delete an existing object to change its type or symbolic-link target, since that would discard its destination security. Explicit deletion remains a separate operation. Filesystems must support unnamed temporary files for ordinary staged transfers; there is no insecure staging fallback.
Rsync daemon serving is disabled, including daemon mode invoked over a remote shell. Ordinary SSH --server transfers remain supported. The receiving process uses the identity supplied by SSH; installing rsync creates no service and grants no extra authority. Security-sensitive symlink ownership checks use native SIDs rather than cosmetic Unix UID zero. A client password file must have a protected owner-only native DACL.
After a transfer, check that the intended contents arrived and that the
destination's native security is still the policy you intended. If staged
replacement is refused for insufficient security rights, read that failure
before considering --inplace; it is a different interruption and hardlink
tradeoff, not an equivalent retry.
OpenSSL and private outputs #
OpenSSL uses trustd's generated /etc/ssl/cert.pem and hashed /etc/ssl/certs by default. Use trust to manage machine trust. Explicit CA-file, CA-directory and environment overrides select trust for that invocation; they do not modify the machine's trust store. openssl rehash is for directories you manage yourself, not trustd's generated directory.
Named secret outputs are private to the effective Peios principal from creation. This includes private keys, secret-bearing PKCS12 exports, session files, TLS key logs, derived/random key material, RNG seed files and decrypted plaintext. Public certificate and public-key outputs use ordinary destination policy, except mixed exports, which are treated as potentially secret.
An existing secret destination must be a regular file owned by the effective principal, with a protected owner-only DACL. Shared files, symbolic links, multiply linked files and policies the tool cannot prove private are refused before data is overwritten. This intentionally does not reinterpret Unix 0600 as a native access policy. Configure intentional service-key sharing separately through Peios security tools after generation. Ordinary administrative privileges and mandatory controls still apply.
Output to stdout or a pipe remains supported. Shell redirection creates its own file: openssl ... > key.pem does not receive the tool's named-output protection. For private named files use the command's -out, -keyout, -secret, -sess_out, -keylogfile or other named-output option as appropriate. A shared directory may still permit someone to remove or replace directory entries; use a directory you control for durable keys.
TLS diagnostics and network policy #
openssl s_client retains its diagnostic behavior: certificate errors are reported but do not necessarily abort the connection. For a strict server check, request both fatal verification errors and the expected identity:
Use -verify_ip for an IP identity. SNI (-servername) alone is not hostname verification. Explicitly selected TLS diagnostic behavior never bypasses PNP: connections and listeners remain subject to native network policy and port reservations.
A successful TLS check establishes the requested certificate checks for that peer. It does not change system trust or grant access through PNP. If verification fails, inspect the reported certificate, peer name and trust anchors rather than disabling the check to declare success.
Name resolution
Peios / Using Peios / Networking
Use resolv to check which DNS settings Peios is using and where an answer
came from. On the desktop, use DNS in
Network Manager.
Network-supplied DNS belongs to an interface's profile. Machine-wide
fallback servers, extra search domains and static names live under
Machine\System\Network\Dns. Do not edit /etc/hosts,
/etc/resolv.conf or nsswitch.conf to configure Peios name resolution.
Check a name first #
Replace example.org with the name that fails. query shows the outcome,
source, server and interface; lookup shows addresses as getaddrinfo
sees them. For a fresh network answer, use:
This skips cache lookup but still stores the new answer. If only an IP connection works, use the result to choose the next check under Not found versus unavailable. If the IP connection also fails, start with connectivity.
Where the servers come from #
resolv status shows netd's connection and the scopes currently in use.
A scope is an interface's servers, domains and addresses:
hostname workshop
netd connected
cache 12 entries
eth0 metric 100 [default-route]
server 10.0.2.3
domain lan
subnet 10.0.2.15/24
netd sends that live information to resolvd; offered server lists are not
written to a resolver configuration file. Match the scope against the
interface and profile in net status.
To change an interface's DNS, use its profile's Dns.Offered,
Dns.Servers, Dns.Domains, Dns.Default and Dns.Exclusive settings in
Configuring profiles.
A profile edit can restart address clients, so use that guide's recovery
checklist even for a DNS change.
For machine-wide settings, use FallbackServers and ExtraSearchDomains
on Machine\System\Network\Dns, or Network Manager's DNS Settings:
FallbackServersis used only when no up interface supplies a server. It is not failover for an interface whose servers time out.ExtraSearchDomainsadds domains after the applicable interface domains. It is not applied while an exclusive scope takes all queries.- These registry changes apply live to later questions. Check the new
fallback-server list in
resolv statusand repeat the lookup. Extra search domains are not listed in that status output: verify them with a lookup of the intended bare name. No restart is normally needed. A failed registry watch or a malformed value can prevent the expected change; the registry chapter describes the log messages.
Verify which interface gets a name #
A query goes to one interface's servers, never to all interfaces at once. The operator-relevant order is:
- An exclusive scope, if it has a server and is at least
addressed. - The scope with the most-specific matching search domain.
- For a reverse lookup, a scope whose subnet contains the address.
- A scope that claims the default route, otherwise the lowest-metric eligible scope.
- Fallback servers only when no up scope has a server.
A single-label name such as printer is asked only after a search domain
is appended. With no applicable domain it is not found locally and no
query leaves the machine. A name with a dot between labels is not
expanded. Use a fully qualified name to check whether the problem is the
search list. The scope-routing
and single-label expansion
chapters contain the full selection and tie-breaking algorithms.
Static names #
For a name that this machine should answer without DNS, add a value under
Hosts. You need registry write access. Replace this example name and
address with the intended host:
Then verify both directions:
An exact static name wins over DNS at every resolver entry point, including
programs using the DNS stub. Static-name changes are live and need no cache
flush. localhost, the machine's own hostname and their reverses are
answered without configuration. The synthetic-name chapter
documents precedence, record types and reverse-name collisions.
.local names are not sent upstream. mDNS is not implemented and LLMNR
is never used. An explicitly configured static .local name can still
answer locally.
The resolv command #
| Command | What to check |
|---|---|
resolv status | Scopes, servers (including those demoted after failures), fallback servers, cache size and counters. |
resolv query <name> [type] [--no-cache] | One question, default type A; outcome, source (synthetic, hosts, cache, dns or local), server, interface and records. |
resolv lookup <name> | The addresses and canonical name as getaddrinfo sees them. |
resolv reverse <address> | The names for an address. |
resolv flush | Clear the cache; requires RESOLVER_CONTROL. |
Queries and status need RESOLVER_QUERY, granted to everyone by the
default control descriptor. Flush is granted to SYSTEM and Administrators
by default. Machine\System\Network\Dns ControlSecurity can change those
grants. Registry edits require separate write access.
Exit 0 means found or a successful status/flush, 2 means notfound, and 3
means unavailable. Exit 1 includes an unreachable daemon, access denial,
protocol error or bad type/address; unrecognized usage exits 64. See the
command reference
for accepted record types and exact output.
Unlike dig, resolv query reports cache use and the chosen interface.
Every network answer currently says unvalidated; this version performs
no DNSSEC validation.
Not found versus unavailable #
| Result or symptom | Meaning and next check |
|---|---|
notfound with source dns or cache | The server said the name does not exist. Negative answers may be cached for a few minutes. Check spelling and use --no-cache if the record just changed. |
notfound with source local for a bare name | Check search-domain lines in resolv status. A scope without servers contributes no search domains. |
unavailable | No usable DNS path, or attempts failed. It is never cached as a missing host; programs get a retryable failure. Check scopes, server reachability and policy. |
netd not connected, no scopes | resolvd has not received a snapshot. Check net status and netd's service; without fallbacks only local/static names can answer. |
netd not connected, scopes still listed | These are the last received scopes and remain in use. Do not assume they describe the current links. |
Server marked (demoted) | It recently failed. It is tried after healthy servers, not permanently disabled. Check the server and network path. |
| Wrong or stale answer | Compare a --no-cache query. A static name can override DNS; administrators can flush the cache if needed. |
resolv: access denied | Check the native control object's query right. It is separate from reachability of the DNS stub. |
| Daemon unreachable or repeated failures | Read evctl 'LOGS FROM resolvd SINCE 1h ago TAKE 40' and the failure-mode guide. |
On restart, resolvd removes its stale socket and binds a new one. Its directory and socket descriptors retain full access for its own service identity so this replacement can succeed. See the socket chapter. If startup reports a permission error, inspect the actual descriptors and logs; do not delete the runtime directory merely because a restart occurred.
For comparison with the local DNS stub, Experimental includes
dig @127.0.0.53 example.org A. A direct query to some other server tests
that server, not Peios's normal scope selection. All DNS traffic still
meets PNP.
Three doors #
resolvd is Peios's stub resolver: a DNS client forwarding to upstream
servers, not a recursive or authoritative server. Its shared policy and
cache are reached through libc's getaddrinfo, DNS on 127.0.0.53, or the
native socket /run/resolvd/resolv.sock used by resolv.
The glibc hosts database is fixed to libnss_peios_net.so.2, without
files or dns fallbacks. The constant /etc/resolv.conf points other
DNS clients, including Go and musl programs, at the stub. A bare name gets
the same search-domain treatment through each route. Native replies also
carry TTLs, source and validation state. See the
resolver overview
and name-resolution interface
for the entry-point contracts.
What is not there yet #
DNSSEC validation, DNS over TLS, mDNS and per-program resolution policy are not implemented in this version. Native query access is checked, but allowed callers use the same resolution policy.
Going deeper #
- Name resolution interface: requests, outcomes and entry-point contracts.
- resolvd technical reference manual: cache behavior, routing, server demotion, retries, wire handling and limits.
- netd's resolver channel: which profile and offered DNS facts reach resolvd.
Network policy
Peios / Using Peios / Networking
Use Peios Network Policy (PNP) to decide which connections the machine
allows and which interfaces join a network. Settings are registry keys
under Machine\System\Network; there is no iptables configuration.
The kernel's Network Traffic Filtering Engine (NTFE) enforces packet
policy, while netd applies interface rules and profiles.
For desktop changes, open Network Manager and use Rules or Profile rules. The policy reference lists exact values and examples. Experimental also includes the PNP viewer, with important access risks.
Check and change policy safely #
- Identify the symptom. For a service that cannot be reached, confirm that it is listening and has permission to bind its port. A firewall allow does not create a listener or grant a port reservation.
- Check what is active. As an administrator, run
net policyto see enforcement, generation and refusals. Usenet statusseparately for interface rules. A refused update leaves the previous accepted policy active. - Preview the smallest change. In Network Manager, use Test a connection and What this changes. Its firewall exposure and activity displays currently include labeled example data; a preview is not evidence that a real connection worked.
- Keep a recovery path. Record the old rule values and retain local access before changing the path used by a remote session. A rule or network-label change can cut an existing connection on its next packet.
- Apply, then verify. After a registry edit, run
net policy wait, thennet policy, and test the intended connection. Checknet statustoo if interface rules, profiles or network labels changed.
net policy and net policy wait require access to the kernel device;
ordinary users cannot open it. Editing rules requires registry write
access. A successful write is not yet proof that the kernel ingested it;
net policy wait fails on a refused generation or timeout.
Rules are keys #
A rule's path identifies it in diagnostics, such as
no-inbound/ssh-from-lan. Its values specify:
- Conditions:
<Fact>.<Operator>values, such asDstPort.Equal = 22. Every condition must hold. A rule with no conditions matches everything. - Actions: a list such as
PASS,DROP,REJECT(Prohibited)orCOUNT(dns). - Priority: a number, inherited by children and defaulting to 0 at a
root.
Enabled = 0disables a rule and its entire subtree.
Create a child key for an exception. The child must satisfy its parent's
conditions as well as its own, so no-inbound can drop inbound traffic
while a child allows SSH from a particular subnet. Order in the editor or
registry does not decide which rule wins. Independent rule trees compete
by priority and strictness, allowing local and organization policy to
coexist without merging their trees.
Three layers, and a fourth #
Choose the layer for the question you need to answer:
| Layer | Use it for |
|---|---|
Rules\Flow | Who may connect to what. Most service and port policy belongs here. A flow is a TCP connection, UDP exchange or tracked ICMP echo exchange. |
Rules\Packet | Conditions that must be checked per packet. The baseline passes tracked traffic here and leaves connection decisions to Flow. Untracked traffic is decided here. |
Rules\RawPacket | Wire-side frame rules before connection tracking on ingress. Usually unnecessary for host access policy. |
Rules\Interface | Which profile an interface uses, or whether netd leaves it alone or keeps it down. |
Allowing a flow does not override a drop in another layer. A fact absent at a layer cannot make its condition match. Check the layer table before moving a rule between layers.
The kernel's exact inbound/outbound order, bridge and non-IP fallback checks, and untracked-packet handling are in Seats and dispatch.
Flows and sentences #
A sentence is the flow's cached verdict. Normally the first packet
is judged and later packets use it. The rule's direction is the
originator's: a connection this machine opened remains out, including
its replies.
Two changes can trigger a fresh judgment:
- Policy or network context changes. The next packet is judged against
the new generation. A newly forbidden existing connection stops then;
REJECTon an established TCP connection tears down both ends. - A consulted
Time.*condition changes.Time.Hour.Equal = 9-17can stop an existing connection at 18:00 UTC, on its next packet. UseStart.Hourif only new connections should be restricted; it is fixed for the flow's life.
A loopback flow has two local endpoints and two judgments. Both must
allow it. COUNT and REPORT effects at this layer run when a flow is
judged, including a re-judgment, rather than on every packet.
See The Flow layer for caching, time expiry and the cases where no sentence can be retained.
Who is speaking #
Use Flow identity facts for a local program or service. For example,
Local.Service.Equal = resolvd with DstPort.Equal = 53 matches the
resolver service's traffic to that port. Identity comes from the native
token stamped on the socket, not a program path or a claimed name.
Local.User.Present = 0 means no attributable program user is present.
The endpoint can instead be the kernel, a shared receiver or no listener.
On loopback, Remote.* also describes the other local endpoint. Check the
PNP viewer's Listeners and Flows when a service rule does not
match; an unresolved identity is reported rather than guessed.
The identity facts describes socket stamping and exactly when each identity is available.
Joining a network #
For addresses and routes, use Configuring profiles. An interface is the hardware or virtual link; the network is what is on the other side. The address, route, DNS and hostname it offers are claims, not verified trust.
An interface rule selects by Interface.Kind, Interface.Path,
Interface.Id, or an identified network's Name and Trust. Its verdict
is JOIN(profile), IGNORE or DOWN; no matching verdict leaves the
interface ignored. Each interface has one profile or one of those other
outcomes. Profiles contain settings, while rules contain the selection.
A child profile inherits all parent values and replaces what it names.
A bare profile accepts no offers; the shipped default profile supplies
the friendly wired defaults. Network-dependent profile examples describe
selection once the link and identity exist, not Wi-Fi association, which
remains unfinished.
The interface and firewall read the same network labels, so naming or
reclassifying a network can change both. Identification is not proof:
writing Trust = home is your decision to accept that identification.
netd's protected Status records report its findings; do not edit them.
netd and the kernel accept their own layers independently. A missing
JOIN target can leave netd on its last good configuration while firewall
changes still apply. DHCP and router discovery also need ordinary PNP
allows; the baseline's rules are visible and deletable.
The interface-layer chapters cover parsing, generation validation, judgment and reconciliation.
Verdicts and effects #
| Action | What the operator or peer sees |
|---|---|
PASS | This layer allows the traffic; other layers can still refuse it. |
DROP | Silent refusal, usually seen as a timeout. |
REJECT or REJECT(Refused) | An explicit refusal, like no listener: TCP reset or ICMP port-unreachable. |
REJECT(Prohibited) | An explicit policy refusal: ICMP admin-prohibited. |
NULL | No decision; an ancestor, another tree or the backstop decides. |
REJECT works inbound and outbound. An outbound refusal can fail the local
operation promptly instead of leaving it to time out. Where no refusal
can be sent, it degrades to a counted drop; see
Where a REJECT can speak.
Effects do not grant access. TAG marks a flow, COUNT contributes to a
counter stream, and REPORT writes an audit event at its configured level.
PROMPT currently has no handler transport and takes its fallback
immediately. A rule containing only effects abstains from the verdict.
The laws #
Use these checks when a rule behaves unexpectedly:
- Backstop: an ingested empty packet forest drops everything, attributed
to
backstop; the interface backstop isIGNORE. Before any policy is loaded, generation 0 is permissive and reported as such. - Absent facts: a condition on a missing fact is false, including
missing tags and counter cells.
Presentis the explicit existence test. - Exceptions: a matching child shadows its parent within that tree. Trees never shadow one another.
- Abstention: a triggered rule without a verdict hands the decision to the nearest ancestor with a direct verdict. That ancestor's full action list runs once; intervening abstainers do not run.
- Priority: highest wins. At equal priority,
DROPbeatsREJECTbeatsPASS, andRefusedbeatsProhibited. An interface's verdict order isDOWN,IGNORE,JOIN; conflicting profile selections need correction rather than relying on rule order. - Effects: triggered rules' effects run even when another rule wins. Matching uses the snapshot from before those writes. A count does not affect the same evaluation's threshold; a later packet can see it.
- Whole generations: malformed packet policy keeps the previous
generation active. A registry write returning is not acceptance; run
net policy waitbefore depending on the new rule. - Tags: reads can only go upward through
RawPacket<Packet<Flow. Downward reads are refused. Tags remain on a flow across policy changes. - Flow scope: a flow is judged per local endpoint, then cached until a policy change or consulted time condition makes it stale. Loopback has two endpoints.
- Refusals: PNP does not filter its own generated refusal packets. Ordinary traffic, including echo, has no such bypass.
The full evaluation algorithm, generation ingestion and stores belong in the kernel technical reference; the reference keeps the authoring vocabulary and limits.
Rate limiting without a throttle verb #
PNP expresses a rate limit using a count, a view of that count, and a verdict. This example observes DNS queries and rejects queries from a source whose preceding count exceeds the threshold:
Rules\Packet\dns-watch Protocol.Equal = udp, DstPort.Equal = 53
Actions = COUNT(dns)
Rules\Packet\dns-flood Protocol.Equal = udp, DstPort.Equal = 53
Counter.dns(10s, SrcAddr).GreaterThan = 20
Actions = REJECT(Prohibited)
dns-watch counts and abstains. dns-flood reads the ten-second view
before the current packet's count lands. Windows are approximations;
check the counter view
and its observed counts rather than treating this as an exact throttle.
As counts age out, the condition stops matching.
Where things are visible #
net status: interface verdict, selected rule/profile and refusal.net policyandnet policy wait: kernel acceptance and enforcement.- Network Manager: configuration and connection tests; read its Example data disclosures for firewall displays.
- PNP viewer: live verdict attribution, flows, listeners and counter tables on Experimental, with its safety warning observed.
- Event Viewer:
ntfe.verdict.reportedevents requested byREPORTeffects.
A packet using a cached sentence has no new evaluation event. Missing events are not proof that traffic passed; check the sentence and surfaced drop/refusal counters. The event-stream chapter describes the kernel records and loss accounting.
Network policy reference
Peios / Using Peios / Networking
Use this page while editing profiles or rules. It covers the kernel's packet layers and netd's interface layer. Start with the safe profile workflow or firewall workflow if you are making a change rather than looking up syntax.
| To find | Go to |
|---|---|
| Addresses, gateway, DNS, hostname or MTU | Profiles |
| Where a setting belongs | Registry layout |
| A match value or comparison | Conditions |
| Allow, drop, reject, join or observe | Actions |
| Which layer can see a fact | Layers |
| Default image rules | Development baseline |
| A policy pattern to adapt | Worked examples |
Writes take effect live and can interrupt remote access. Record the old
values before editing. Check net status for accepted interface settings;
for packet-policy changes, an administrator runs net policy wait and
net policy. These are separate acceptance checks.
Profiles #
Profiles\<path> is how an interface stands on a network, named by a
JOIN. A profile is a key with flat dotted values and no match block.
Inheritance. A subkey inherits every value of its ancestors and
overrides those it names — per value name, wholesale: a list replaces a
list, never appends. A present-but-empty value means none; an absent
value means inherit. This is the same law as in Rules\: a subkey
specialises its parent. A locally written profile may nest under one
pushed by policy. Enabled = 0 makes a key and its subtree invisible.
Vocabulary. Bundles are the claims a network can make; each has an
Offered that says whether to believe it. Every compiled default is
"believe nothing, do nothing": a bare profile brings the link up and
nothing else. Unknown value names refuse the generation.
| Bundle | Value | Type | Meaning | Default |
|---|---|---|---|---|
Address | Offered | 0/1 | take the address the network offers (DHCPv4, IPv6 autoconfiguration) | 0 |
Families | list | ipv4, ipv6: which families the bundle deals in at all; filters Static too | both | |
Static | list | CIDR addresses, either family | none | |
LinkLocal | 0/1 | self-assign 169.254/16 while nobody answers; dropped when a lease arrives | 0 | |
Temporary | 0/1 | add daily-rotating IPv6 privacy addresses beside the stable one | 0 | |
OnExpiry | Drop / Keep | an offered address the network stops renewing | Drop | |
Route | Offered | 0/1 | take the way out, and extra routes, the network offers | 0 |
Gateway | list | pinned way out, either family | none | |
Metric | number | rank of this interface's way out | 100 wired, 600 wireless | |
Dns | Offered | 0/1 | take the servers and search domains the network offers, after our own | 0 |
Servers | list | own servers, in order | none | |
Domains | list | domains these servers answer for | none | |
Default | 0/1/absent | take names no domain claims; absent follows the default route | absent | |
Exclusive | 0/1 | take all queries while at least addressed and supplying a DNS server | 0 | |
Hostname | Offered | 0/1 | adopt the network's name for us if Hostname is unset | 0 |
Announce | 0/1 | tell the network our name | 0 | |
Mtu | Offered | 0/1 | take the packet size limit the network offers | 0 |
Value | number, 68+ | pin it | leave alone |
The Dns bundle is provisional. Verify the effective scope with resolv status; see name resolution for exclusivity and fallback conditions.
The laws of JOIN, whatever the profile says: the executor owns
every IPv4 and non-link-local IPv6 address on a joined interface, and only
routes carrying its own protocol tag; the kernel's IPv6 link-local address
is left alone;
exactly one place decides what a router advertisement means (the
executor; the kernel's own handling is off); a link-local address is
dropped the moment a real lease arrives; IPv6 lifetimes follow the RFCs
with the two-hour floor; Route.Metric defaults by kind; the network's
RequestedAddress is asked for first; a manual change to a joined
interface lasts until the next reconcile; the executor re-derives state on restart.
Registry layout #
Machine\System\Network
Hostname REG_SZ operator
Readiness REG_SZ netd: absent | link | addressed | routed
Duid REG_SZ shared: DHCPv6 identifier, hex
Rules\
CurrentReportingLevel REG_DWORD 1..6 (optional; absent = 1)
RawPacket\ the wire-side layer's forest
<rule>\
Packet\ the per-packet layer's forest
<rule>\ one tree root
<exception>\ a subkey: narrower region, same laws
...
Flow\ the flow layer's forest
<rule>\
Interface\ the interface layer's forest (netd's)
<rule>\
Profiles\ how an interface stands on a network
<profile>\ flat dotted values, see Profiles
<derived>\ a subkey inherits and overrides
Interfaces\<id>\ the inventory, one key per interface seen
ClientId REG_SZ shared: DHCPv4 client identifier
Status\ netd-only: Name, Kind, Mac, Path, Driver,
Verdict, Rule, Profile, Readiness,
Network, LastNetwork
Networks\<id>\ one key per network stood on
Name, Trust REG_SZ operator
RequestedAddress REG_SZ shared: IPv4 to ask for next
Status\ netd-only: Kind, Server, Gateway, Router,
Prefixes, DnsServers, LastSeen, LastInterface
Dns\ name resolution's machine values, see name resolution
A rule key's name is its attribution handle and may not contain \ or
/. The kernel reads RawPacket, Packet and Flow; Interface is
read by netd, the interface layer's executor (see Layers); any
other layer key is ignored by both. The kernel also reads the inventory
— each interface's Status Network and the Name and Trust on the
record it names — as the network context of the
packet layers. Shared values are written by netd
when absent and may be set by the operator; Status keys carry a
descriptor that lets only netd write, so a hand edit is refused rather
than silently reverted.
Rule values #
| Value | Type | Meaning |
|---|---|---|
Actions | REG_MULTI_SZ | The action list, one expression per element. Absent or empty = NULL (abstain). |
Priority | REG_DWORD / REG_QWORD | Collation weight. Inherited by subkeys; a root defaults to 0. |
Enabled | REG_DWORD 0 or 1 | 0 disables the rule and its whole subtree. Default 1. |
<Fact>.<Operator> | see below | A condition. Any other value name is an error that refuses the generation. |
The Rules key's own value #
| Value | Type | Meaning |
|---|---|---|
CurrentReportingLevel | REG_DWORD 1..6 | REPORT(n) fires when n >= CurrentReportingLevel. Absent = 1 (everything fires); 6 silences all reports. Out of range refuses the generation. |
Conditions #
A condition is a value named <Fact>.<Operator>. A rule matches when
every condition holds; a condition over a fact the packet lacks is false
(the absent-fact law).
Operators #
| Operator | Families | Value |
|---|---|---|
Equal | all | Membership: a scalar, or a list (REG_MULTI_SZ) meaning any of. Integers accept a-b ranges; addresses accept CIDR prefixes and (IPv4) a-b ranges. |
GreaterThan | integer | A single integer; strictly greater. |
LessThan | integer | A single integer; strictly less. |
Has | flags | A flag name or list; all listed bits set. |
Hasnt | flags | A flag name or list; all listed bits clear. |
Present | all, and Tag.<name> / Counter.<name> | 1: the fact exists for this packet or flow; 0: it does not. The one operator that looks through the absent-fact law. On a fact its layer never has, it refuses the generation rather than becoming an always-true condition. |
Integer values may be written as REG_DWORD, REG_QWORD, or as decimal
strings; a list of integers is a REG_MULTI_SZ of decimal strings and
ranges. Applying an operator a family does not support (GreaterThan on
an address, Has on a port) refuses the generation.
Facts #
| Fact | Family | Values | Present when |
|---|---|---|---|
Direction | string | in, out | always. Flow: the originator's side, for the flow's whole life |
Interface | string | interface name, e.g. eth0 | always. Flow, outbound: the route's device |
EtherType | integer | number, or ipv4, ipv6, arp | always. Not a Flow fact (a flow's family is its addresses') |
SrcMac | MAC | aa:bb:cc:dd:ee:ff | frame has an Ethernet header; at the IP seats outbound, the machine's own device address |
DstMac | MAC | aa:bb:cc:dd:ee:ff | frame has an Ethernet header. Not a Flow fact |
Vlan | integer | VLAN id | the frame is VLAN-tagged (device seats) or the interface is a VLAN device (IP seats) |
SrcAddr, DstAddr | address | exact (10.0.0.5, fd00::1), CIDR (10.0.0.0/8, fd00::/64), IPv4 range (10.0.0.1-10.0.0.99) | IP packet |
Protocol | integer | number, or tcp, udp, icmp, icmpv6, sctp | IP packet |
Ttl | integer | TTL / hop limit | IP packet. Not a Flow fact |
Dscp | integer | 0..63 | IP packet. Not a Flow fact |
Fragment | integer | 0 or 1 | IP packet (only ever 1 before defragmentation, i.e. at the RawPacket seat). Not a Flow fact |
SrcPort, DstPort | integer | 0..65535 | TCP, UDP or SCTP |
TcpFlags | flags | FIN, SYN, RST, PSH, ACK, URG, ECE, CWR | TCP. Not a Flow fact |
IcmpType, IcmpCode | integer | ICMP / ICMPv6 type and code | ICMP or ICMPv6 |
Length | integer | packet length in bytes at the seat (stack view) | always. Not a Flow fact |
FlowState | string | new, established, related, invalid, untracked | Packet layer at its proper seat; never at the RawPacket or Flow layers |
Related | integer | 0 or 1: the flow was expected by another (an ICMP error for a live flow, FTP data) | Flow layer only |
Time.Year, Time.Month, Time.DayOfMonth, Time.DayOfWeek, Time.Hour, Time.Minute, Time.Second | integer | wall clock, UTC; DayOfWeek is ISO (1 = Monday .. 7 = Sunday) | always. In Flow, a consulted condition expires the sentence at its next flip |
Start.Year, Start.Month, Start.DayOfMonth, Start.DayOfWeek, Start.Hour, Start.Minute, Start.Second | integer | the wall clock when the flow began, UTC | Flow layer only; fixed for the flow's life, never expires a sentence |
Local | string | program, kernel, shared, none: what stands at this machine's end of the flow | Flow layer only; always present there |
Local.User | SID | S-1-5-19, or a well-known name (SYSTEM, LocalService, NetworkService, Everyone, Administrators, Users, …) | Flow, Local is program |
Local.Group | SID set | SIDs or well-known names; any of the token's enabled groups (deny-only groups are invisible; the logon SID is among them) | Flow, Local is program |
Local.Integrity | integer | a level, or untrusted, low, medium, high, system | Flow, Local is program |
Local.Confinement | SID | the confinement SID | Flow, the program is confined |
Local.Capability | SID set | any of the confinement's capability SIDs | Flow, the program is confined |
Local.Service | SID | a service name (resolvd), or its S-1-5-80-… SID | Flow, the program is a service |
Local.Process | string | the process GUID, 8-4-4-4-12 hex (case-insensitive) | Flow, Local is program |
Remote, Remote.* | as Local | the other end of the flow, when it is on this machine too | Flow, loopback flows only |
Interface.Kind | string | wired, wireless, loopback, tunnel, bridge, other | Interface layer only; always |
Interface.Id | string | the stable interface id (the inventory key name) | Interface layer only; always |
Interface.Mac | MAC | aa:bb:cc:dd:ee:ff | Interface layer only; the interface has a hardware address |
Interface.Path | string | bus position, e.g. pci-0000:00:03.0 | Interface layer only; it is hardware |
Interface.Driver | string | kernel driver | Interface layer only; it is hardware |
Network.Id | string | the network record's key name under Networks\ | every layer: a network has been identified on the interface the packet crossed (see the network context) |
Network.Name | string | the operator's Name on the record | every layer; the record has one |
Network.Trust | string | the operator's Trust on the record | every layer; the record has one |
Network.Kind | string | the Interface.Kind the network was seen on | Interface layer; a network has been identified |
Tag.<name> | integer | the flow tag's value | the flow carries the tag (Packet and Flow layers; never RawPacket) |
Counter.<name>[(...)] | integer | a counter view, see below | the packet has the view's key facts and a cell exists |
Address families are strict: an IPv4 pattern never matches an IPv6
address and vice versa, so 0.0.0.0/0 does not swallow IPv6.
A Flow fact is one that is identical for every packet of the flow. The
per-packet facts marked "not a Flow fact" are legal in a Flow rule
but never present there, so the condition never holds; the viewer flags
it. Related, Start.* and the identity facts are the reverse: never
present outside Flow. Interface.* facts are for the Interface layer.
Network.Id, Network.Name and Network.Trust are also available in the
packet layers, as the table above and network context
describe. A packet-only fact in an Interface rule cannot match.
For the kinds netd currently reports, see its
interface classification. Tag.* and Counter.* do not exist at the Interface
layer at all — there is no store behind them — and refuse the generation.
Who is at this end #
Local says what answers at this machine's end of a flow: a
program (a socket some process owns — the identity the kernel stamped
on it at the last act that committed it: creation, bind, listen,
connect, accept, or an explicit restamp), the kernel itself (resets,
ICMP errors, neighbour discovery, tunnel outers, kernel sockets),
shared (inbound multicast or broadcast, delivered to every socket
bound to the port — one flow, many receivers), or none (nothing
listens; the stack will refuse it). Only a program has Local.*
facts, so Local.User.Present = 0 is "no program stands here".
Identity is decided once, at the flow's first judgment, and kept for
the flow's life. A service thread acting for a client is the client
(the effective identity governs, as it does for files); a listener
handed to another program is governed by the program that last
committed it. SIDs are written as S-1-…; a user or group may instead
be a well-known name, a service its name — the name is turned into the
service's SID exactly as the service's token was minted, so the rule
matches the token, not a string. A misspelt name refuses the
generation rather than quietly matching nothing.
Counter views #
Counter.<name> reads the stream <name> that some rule's COUNT
writes. In parentheses, in any order, at most one of each:
| Argument | Form | Meaning |
|---|---|---|
| window | <n>s, <n>m, <n>h, <n>d — at most 1d | Sliding window; omitted = the cumulative total since the cell was created. |
| key | SrcAddr, DstAddr, Interface, or a +-compound (SrcAddr+DstAddr) | Which facts partition the count; omitted = one global cell. |
Examples: Counter.dns (total, global); Counter.dns(10s) (last ten
seconds, global); Counter.dns(SrcAddr) (total, per source);
Counter.dns(1h, SrcAddr+DstAddr) (last hour, per source–destination
pair). A view over a stream that no rule anywhere writes refuses the
generation; a stream nobody reads is fine. Windows are approximated by
eight buckets, so a value can be up to an eighth of a window stale.
Actions #
Actions is a list of expressions. Names are case-insensitive;
whitespace is ignored. A rule's actions may contain any mix; at most one
verdict results (the strictest listed).
| Expression | Species | Meaning |
|---|---|---|
NULL | abstention | Nothing. A rule whose actions yield no verdict abstains. |
PASS | verdict | This layer approves. |
DROP | verdict | Refuse silently. |
REJECT / REJECT(Refused) | verdict | Refuse; look like nothing is listening — TCP RST, else ICMP port-unreachable. |
REJECT(Prohibited) | verdict | Refuse; say policy did it — ICMP admin-prohibited for every protocol. |
PROMPT(Handler[, Fallback]) | deferral | Ask a userspace handler; on no answer apply Fallback (an action expression, nesting up to 4 deep). No handler transport exists in this release: the fallback applies immediately and the prompt is recorded. |
TAG(Name, Set[, n]) | effect | Set flow tag Name to n (default 1). |
TAG(Name, Add[, n]) | effect | Add n (default 1) to the tag; an absent tag counts as 0 first. |
TAG(Name, Clear) | effect | Remove the tag; it reads as absent afterwards. |
COUNT(Name[, n]) | effect | Emit n (default 1) into stream Name. |
COUNT(Name, Length) | effect | Emit the packet's byte length — the bandwidth primitive. |
REPORT(level) | effect | Emit an ntfe.verdict.reported audit event, level 1..5, if the level clears CurrentReportingLevel. One report per rule per evaluation, at the highest level listed. |
JOIN(profile) | verdict, Interface layer | Bring the interface up and stand in the named profile: a path under Profiles\, /- or \-separated. |
IGNORE | verdict, Interface layer | Never touch the interface; something else owns it. |
DOWN | verdict, Interface layer | Keep the interface administratively down. |
The three interface verdicts are legal only in the Interface layer,
and that layer speaks nothing but them, NULL and REPORT; either way
round refuses the generation. Their strictness order is DOWN > IGNORE
JOIN.JOINmust name a profile that exists (a path that names no key refuses the generation); naming a disabled profile makes the rule abstain, as if its action wereNULL.
Unknown action names, wrong argument counts, negative or non-integer
operands, unminted REJECT kinds, and a TAG operation other than the
three above all refuse the generation.
Where a REJECT can speak #
Everywhere, for IP traffic. Inbound, the refusal goes back to the peer.
Outbound, the answer the peer would have sent is delivered to the local
socket, so the program fails at once — a TCP connect, or the next call
on a connected UDP socket — with connection refused for Refused
(a reset for TCP, a port unreachable for UDP and the rest), and for
Prohibited with host unreachable over IPv4 or permission denied
over IPv6. Only a packet with no refusal vocabulary — ARP and other
non-IP frames, a broadcast or multicast destination, a fragment — is
applied as a DROP and counted as degraded; the verdict event still
says REJECT and names the kind. The refusals PNP sends pass its own
seats unjudged.
In the Flow layer a REJECT sentence answers every later packet of
the flow the same way, so a retransmitted SYN gets its reset too —
from the sentence, or, where refusing the first packet took the new
flow with it, from a fresh judgment of the retransmit as a new flow. When
the refused packet belongs to an established TCP connection (a flow
re-judged after a policy change or at a time edge), the other end is
torn down as well: the refused packet is turned into a reset and sent
where it was going, so both sockets fail at once. New flows and UDP have
no far end to tear down.
Layers #
RawPacket | Packet | Flow | |
|---|---|---|---|
| Judged | at the device seats, for all traffic, per packet | once per traversal, at the richest seat, per packet | once per flow (per local endpoint), at the IP seats, on the flow's first packet |
| Order | first inbound, last outbound | between | last inbound (after Packet), first outbound (before Packet) |
FlowState | never (the seat stands before conntrack) | yes | never (the layer is the judgment of a flow) |
Related, Start.* | never | never | yes |
Local, Local.*, Remote.* | never | never | yes (Remote on loopback only) |
Network.Id, Network.Name, Network.Trust | yes | yes | yes |
Per-packet facts (Length, TcpFlags, Fragment, Ttl, Dscp, EtherType, DstMac) | yes | yes | never |
Tag.<name> reads | never | tags RawPacket or Packet rules write | any tag |
TAG writes | yes | yes | yes |
Fragment | may be 1 | always 0 | — |
| Effects run | per packet | per packet | per evaluation of the flow |
| Verdict scope | the packet | the packet | the flow: cached as its sentence |
A rule conditioned on a fact its layer never has is legal but can never
match; the viewer flags it. A Packet or RawPacket rule that reads a
tag a Flow rule writes is a downward read and refuses the generation.
The interface layer #
Rules\Interface is the fourth layer, and the one the kernel does not
read. Its subject is an interface, not a packet: a rule is judged for each
interface (loopback excepted) whenever an interface appears or changes,
whenever a network is identified on it, and whenever a generation lands.
Its executor is netd, which builds and judges the forest with the same
engine the kernel uses, so a rule means the same thing in every layer.
Interface | |
|---|---|
| Judged | per interface, by netd, on interface and generation change |
| Facts | Interface, Interface.*, Network.* (Network.Id, Network.Name and Network.Trust are the packet layers' too; the rest is this layer's alone) |
| Verdicts | JOIN(profile), IGNORE, DOWN; strictness in that order, rising |
| Effects | REPORT only |
| Backstop | IGNORE: an interface no rule speaks for is left as the kernel left it |
| Result | an interface is in exactly one profile, or ignored, or down |
The collation laws are the packet layers': highest priority wins, ties
go to the strictest verdict, a subkey is an exception, an abstaining rule
hands up its parentage. One case has no packet-layer analogue: two rules
tied on priority naming different profiles for one interface. That is a
conflict, not a choice. On a runtime reload, a tie affecting an existing
interface refuses the generation; net status names both rules. At startup,
or when a new interface appears, that interface is ignored with a tie
warning instead. See netd's conflict handling.
Executor independence. A generation is one registry state read by
two executors. Each validates and refuses its own layers on its own: a
dangling JOIN keeps netd on its last good interface forest and never
stalls the firewall, and a bad Flow rule never stops an interface
joining.
The executor's own traffic. Joining needs netd to send and receive
DHCP and router discovery, and PNP grants that nothing implicitly: the
permission is the dhcp-client, icmpv6-housekeeping and
dhcpv6-client rules of the baseline below, ordinary and deletable. netd cannot yet read the verdict
stream to name a rule that refused it (the stream has one reader, the
viewer); net status reports an unanswered request as a warning.
Sentences #
A sentence caches the Flow verdict. Later packets use it without a new
evaluation event until the policy generation changes or a consulted
Time.* condition changes. A fresh judgment runs the effects again.
Start.* stays fixed and does not expire the sentence. Loopback has one
sentence per local endpoint and must pass both.
For exact expiry rules (including daily reevaluation of calendar fields), cache-allocation failure and event behavior, see The Flow layer and Evaluation. Those chapters retain the implementation detail; use this section to choose live-time versus connection-start conditions.
The network context #
Which network the machine is on is one fact, told once. netd
identifies the network on a link and writes the record's id to the
interface's Status Network; the kernel reads that, and the Name and
Trust on the record it names, into a table keyed by interface, and
every traversal carries its interface's entry as Network.Id,
Network.Name and Network.Trust. So the three facts the interface
layer conditions on to choose a profile are the same three a packet
rule conditions on to open or close a door, read from the same record:
Rules\Flow\no-ssh Direction.Equal = in
Protocol.Equal = tcp, DstPort.Equal = 22
Actions = DROP
Rules\Flow\no-ssh\home Network.Trust.Equal = home
Actions = PASS
is "SSH answers only on the network I called home", and it is true on every interface at once: a wired card at home and a radio at a cafe each carry their own context, and a flow sees the context of the interface it actually uses.
The facts are absent on an interface no network has been identified on
— before the first offer, after the carrier goes, on the loopback, on an
IGNOREd or DOWN interface — so a rule about a network is simply
false there and the rules that say nothing about networks apply
unchanged: an unrecognised network gets the baseline, never a door
opened for a network that is not there. Network.Id.Present = 0 is the
honest "not on any known network".
A change of network is a change of policy. When the context of any
interface changes — a network identified, the carrier lost, the operator
editing Trust — the kernel publishes a new generation, and every flow
is re-judged on its next packet exactly as after a rule change: a
connection the new context forbids is refused then, with the same
REJECT teardown, and one it now permits goes through. netd rewriting
an interface's Status without changing its network costs nothing; a
generation is published only when the table differs. The context is not
policy and cannot refuse a generation: a record the kernel could not
read is an interface without a context.
Trust is the operator's word, exactly as written, with no vocabulary
yet: home, corporate, untrusted are conventions, and a rule that
conditions on it is stating that the operator accepts the
identification. It is not evidence that the network has been authenticated.
Limits #
These kernel limits can cause a refused generation or a counted refusal. netd has its own registry-read depth limit; do not assume a packet-layer limit describes every executor.
| Limit | Value | When exceeded |
|---|---|---|
| Distinct tags per flow | 64 | the write is refused and counted |
| Keys per counter table | 4096 | idle keys are reaped; if none are idle the new key is refused and counted |
| Windows per (stream, key) | 8 | the generation is refused |
| Longest window | 1 day | the generation is refused |
| Rule nesting depth | 12 | the generation is refused |
| Rules per layer | 4096 | the generation is refused |
| Attribution path in events | 96 bytes | truncated |
| Tags reported per flow in the flows dump | 8 | the rest are counted, not listed |
The development baseline #
The rules below illustrate the baseline seed policy, not the complete
policy of every image. The compiled-in backstop is DROP in every layer;
each allow rule is a visible, deletable exception.
Service-specific seeds add their own rules. The Experimental edition's
gxwi-network seed admits GXWI on TCP 7780; see the
desktop quick start.
Inspect the configured rules and verify kernel acceptance with the
policy-checking workflow
before deciding which services a machine exposes. The development
PNP viewer uses port 8081 and needs its
own restricted access path.
Rules\RawPacket:
| Rule | Conditions | Actions |
|---|---|---|
all | — | PASS |
Rules\Packet — passes every tracked packet through to Flow, and the
untracked housekeeping a host needs:
| Rule | Conditions | Actions |
|---|---|---|
tracked | FlowState.Equal = new, established, related | PASS |
outbound-ok | Direction.Equal = out | PASS |
loopback | Interface.Equal = lo | PASS |
arp | EtherType.Equal = arp | PASS |
icmpv6-housekeeping | Protocol.Equal = icmpv6, IcmpType.Equal = 130-137, 143 | PASS |
dhcp-client | Protocol.Equal = udp, SrcPort.Equal = 67, DstPort.Equal = 68 | PASS |
Rules\Flow — the decisions:
| Rule | Conditions | Actions |
|---|---|---|
outbound-ok | Direction.Equal = out | PASS |
loopback | Interface.Equal = lo | PASS |
dhcpv6-client | Direction.Equal = in, Protocol.Equal = udp, SrcAddr.Equal = fe80::/10, SrcPort.Equal = 547, DstPort.Equal = 546 | PASS |
An inbound flow with no matching allow rule meets the Flow backstop,
once, on its first packet; untracked inbound packets with no matching
allow rule meet the Packet backstop.
Profiles and Rules\Interface — every wired interface joins the network
it is plugged into, taking what it offers:
| Key | Values |
|---|---|
Profiles\default | Address.Offered = 1, Address.LinkLocal = 1, Route.Offered = 1, Dns.Offered = 1 |
Rules\Interface\wired | Interface.Kind.Equal = wired, Priority = 10, Actions = JOIN(default) |
Wireless is not in the baseline until a supplicant exists. An interface
no rule speaks for meets the Interface backstop, IGNORE.
Worked examples #
These blocks describe registry layouts, not shell commands or complete
replacement policies. Adapt the addresses, ports, names, priorities and
profile paths to your machine. A JOIN target must already exist; examples
are evaluated alongside every existing rule, so inspect the baseline and
competing priorities before applying them. Preserve a recovery path, then
verify accepted policy and an actual connection afterwards.
An exception under a broad rule — drop all inbound except SSH from the LAN, and tell other SSH sources that policy said no:
Rules\Packet\no-inbound Direction.Equal = in
Actions = DROP
Rules\Packet\no-inbound\ssh DstPort.Equal = 22
Actions = REJECT(Prohibited)
Rules\Packet\no-inbound\ssh\lan SrcAddr.Equal = 10.0.0.0/8
Actions = PASS
A flow tag read on the reply — mark DNS queries, report on the replies of marked flows:
Rules\Packet\tag-dns Protocol.Equal = udp, DstPort.Equal = 53
Actions = TAG(dnsq, Add)
Rules\Packet\tagged-reply Protocol.Equal = udp, SrcPort.Equal = 53
Tag.dnsq.GreaterThan = 0
Actions = REPORT(4)
Byte accounting per interface — count outbound bytes and refuse a source that pushes more than 100 MB in a minute:
Rules\Packet\egress-bytes Direction.Equal = out
Actions = COUNT(egress, Length)
Rules\Packet\egress-cap Direction.Equal = out
Counter.egress(1m, SrcAddr).GreaterThan = 104857600
Actions = DROP
A connection-level allow — let the machine connect out, accept SSH in from the LAN, and refuse every other inbound connection with a reset, each decided once per flow:
Rules\Flow\outbound-ok Direction.Equal = out
Actions = PASS
Rules\Flow\inbound Direction.Equal = in
Actions = REJECT
Rules\Flow\inbound\ssh-lan Protocol.Equal = tcp, DstPort.Equal = 22
SrcAddr.Equal = 10.0.0.0/8
Actions = PASS
A curfew that lets downloads finish — no new outbound connections between 22:00 and 06:00 UTC, existing ones run on:
Rules\Flow\curfew Direction.Equal = out
Start.Hour.Equal = 22-23, 0-5
Priority = 10
Actions = REJECT(Prohibited), REPORT(3)
Written with Time.Hour instead of Start.Hour, the same rule cuts
every running outbound connection at 22:00 — on its next packet, with
an ICMP admin-prohibited — and the report says which rule did it.
A static server as an exception — every wired interface takes what the network offers, except the card in slot 3, which stands in a derived profile that changes only the address and the way out:
Profiles\default Address.Offered = 1, Address.LinkLocal = 1
Route.Offered = 1, Dns.Offered = 1
Profiles\default\db1 Address.Offered = 0, Address.Static = 10.0.0.5/24
Route.Offered = 0, Route.Gateway = 10.0.0.1
Rules\Interface\wired Interface.Kind.Equal = wired
Actions = JOIN(default)
Rules\Interface\wired\db1 Interface.Path.Equal = pci-0000:00:03.0
Actions = JOIN(default/db1)
default/db1 still inherits Dns.Offered, but that does not prove a
server is available after offered address acquisition stops. Check
resolv status and pin Dns.Servers if this configuration needs it.
The laptop — a network-dependent selection example, not a working Wi-Fi association procedure. A supplicant and wireless network-name identification are unfinished; the baseline leaves wireless unmanaged. Once the link and identity exist, rules can choose among three profiles:
Rules\Interface\radio Interface.Kind.Equal = wireless
Actions = JOIN(untrusted)
Rules\Interface\radio\home Network.Name.Equal = palfrey-home
Actions = JOIN(home)
Rules\Interface\radio\office Network.Trust.Equal = corporate
Actions = JOIN(corp)
Network.Name and Network.Trust are what the operator wrote on the
record under Networks\; until a network has been identified on the
link they are absent, so the radio stands in untrusted first. The
same two facts open the doors, in the flow layer:
Rules\Flow\no-inbound Direction.Equal = in
Actions = DROP
Rules\Flow\no-inbound\at-home Network.Trust.Equal = home
Protocol.Equal = tcp
DstPort.Equal = 22, 5900
Actions = PASS
Walk into the cafe and the record on the other side of the radio is a
different one, with no Trust written: the exception is false, the
parent drops the SSH traffic on its next packet. DROP is silent; the
reset behavior described for REJECT does not apply to this example.
A card that stays dark, and one another program owns:
Rules\Interface\dead-card Interface.Id.Equal = 3f2a1b8e-…
Priority = 100
Actions = DOWN
Rules\Interface\tap Interface.Equal = tap0
Actions = IGNORE
Connection rate limiting — count connections, not packets, by counting in the flow layer:
Rules\Flow\conn-count Direction.Equal = in, Protocol.Equal = tcp
Actions = COUNT(conns)
Rules\Flow\conn-flood Direction.Equal = in, Protocol.Equal = tcp
Counter.conns(1m, SrcAddr).GreaterThan = 100
Actions = DROP
The PNP viewer
Peios / Using Peios / Networking
Experimental editions run pnpd, the observation and authoring surface of Peios Network Policy (PNP). Browse to port 8081 on a running machine and you are looking at its network life five ways at once: every frame on the wire, the firewall engine's verdict on every evaluation, the policy that produced those verdicts — editable in place — every live flow with the sentence the flow layer gave it, and the counters that policy is keeping.
pnpd is development-phase tooling, and deliberately not part of enforcement: the kernel reads its policy from the registry itself. pnpd watches, explains, and writes the registry like any other author.
The Experimental edition is a development-machine image, not a production deployment profile. Its pnpd service listens on every interface and does not authenticate viewers; a viewer can inspect captured traffic and change network policy. Run it only on a trusted development network, and expose port 8081 to the host through a loopback-only forward. Do not deploy the Experimental image or expose pnpd to an untrusted network.
Reaching it #
On QEMU user-mode networking, forward the port:
make boot NET='-nic user,hostfwd=tcp:127.0.0.1:8081-:8081',
or drive.py --hostfwd tcp:127.0.0.1:8081-:8081, then open
http://localhost:8081.
Start with the question #
| Question | Where to look |
|---|---|
| Did my firewall edit take effect? | Check enforcement, generation and any refusal banner in the header, then the Policy tab. |
| Is this service actually listening, and as whom? | Listeners shows the native owner stamp the Flow layer uses. |
| Which rule allowed or blocked this connection? | Flows shows the cached sentence and its rule; Wire shows individual evaluation badges. |
| Why is there no packet beside a drop? | An outbound drop can occur before the transmit tap; the verdict is still shown as its own row. |
| Did a rate-limit rule count what I expected? | Counters, including refusal counts and window approximation. |
After narrowing the interface, protocol, address or port, compare the flow's rule with the policy and the native listener identity. Do not infer delivery from a captured packet alone. Read Honesty rules before treating a missing frame or badge as evidence.
The wire tab #
Every frame on every interface, both directions, decoded in the browser: Ethernet, ARP, IPv4/IPv6, TCP, UDP, ICMP, ICMPv6 (router and neighbor discovery spelled out), DNS, DHCP and DHCPv6. Each packet expands into a per-layer field view and a hex dump; filter by protocol or free text.
Packets carry a verdict badge — PASS, DROP, or REJECT — painted from
the engine's own event stream (/dev/peios-ntfe), never inferred: each badge
names the rule that decided, as a registry path like
no-inbound/ssh-from-lan, plus the layer and standing seat. A REJECT badge
also names the story it told the sender: REJECT·Refused (nothing is
listening — a TCP reset or ICMP port-unreachable) or REJECT·Prohibited
(policy refused you — ICMP admin-prohibited). A verdict with no matching tap
packet renders as its own row, slotted into the timeline by its timestamp —
for an outbound drop that row is the only possible trace, because the
transmit tap stands after the filter and never saw the packet.
A packet answered by a flow's cached sentence carries no badge of its own: there was no evaluation. The sentence is on the flows tab.
The header shows the engine itself: the policy generation, whether it is enforcing (generation 0 boots loudly permissive), the running pass/drop/reject counts, and the machinery stores' own accounting — tag writes, counter emissions, reports emitted to the KMES event stream, live counter cells, the active reporting level, flows judged, and refusals sent. Hover a count for what the store refused or what it did instead (a flow past its tag tripwire, a table at its key cap, a packet lacking a view's key fact, packets answered from a cached sentence, flows re-judged after a policy change or at a time edge, a REJECT with nothing to send): refusals are counted, never silent. A failed policy ingestion shows as a banner: the previous generation stays active. Correct the named failure and verify a new generation rather than assuming the saved edit is active.
The policy tab #
The rule forest under Machine\System\Network\Rules, rendered as nested
cards: conditions, actions, priority, exceptions indented under their
parents. Rules can be edited, given exceptions, created, and deleted in
place; every save commits in one registry transaction, the kernel re-walks
the subtree, and the generation bump — or the refusal — appears in the
header within a second. The backstop is compiled-in DROP, so everything in
the tree is a visible, deletable permissive statement.
The flows tab #
Each flow shows its owner: the program at this machine's end — its comm and pid, and the service or principal it runs as — or what stood there instead (the kernel, a shared receiver, nobody). A loopback flow shows both ends. Service names are resolved from the service definitions in the registry; a SID the viewer cannot name is shown as is. Hover a verdict badge to see the same for the judgment that produced it.
Every flow conntrack is tracking, refreshed while the tab is open: the protocol and both ends (originator first), which side opened it, the interface it was judged on, conntrack's state and remaining lifetime, the flow's age, packets and bytes in each direction, and the flow layer's sentence — the cached verdict, the rule that gave it (resolved from the policy by the same hash the kernel keys it by), the generation that judged it, and a ⏱ when a consulted time condition will expire it. A loopback flow shows two sentences, one per local endpoint. Tags the flow carries are listed by name where the policy mentions them.
A missing sentence can mean permissive operation or no Flow policy, but it
can also mean the kernel could not allocate storage for the sentence and
judges each packet instead. Check flow_uncached; see
The Flow layer.
The listeners tab #
What the machine is prepared to receive right now, without waiting for
a packet: every TCP socket that is listening and every UDP socket that
is bound, by protocol, address and port, with the program that owns it
— comm, pid, and the service or principal it runs as — or "kernel" for
a socket the kernel opened. Wildcards show as *; a member of a
SO_REUSEPORT group is marked as one. This is the attack surface as a
list, and the first place to look when a rule about a service does not
match: the stamp shown here is the one the Flow layer will read.
The counters tab #
Every cell of every counter table the policy materialized, refreshed while
the tab is open. COUNT(name) in a rule emits into a stream; every
Counter.name(window, key) some rule reads is a table here — one block per
(stream, key), one row per key the wire has produced, one column per window
the table answers plus the cumulative total and the age of the last write.
Windows are eight-bucket sliding approximations, so a value can run up to
an eighth of a window stale. A stream nobody views has no table and nothing
to show.
Honesty rules #
The viewer never silently lies:
- Drops are confessed — both the tap's kernel drop count and the verdict ring's overwrite count are surfaced.
- Its own traffic is counted, not vanished. pnpd excludes its own HTTP flow from the packet ring and shows how many frames that hid. The viewer description says its verdicts remain visible, while the kernel manual's event-stream chapter says pnpd hides and counts its own-port verdict events too. Do not rely on complete visibility of the viewer's own connection when diagnosing it.
- Placement is stated. Inbound frames are captured before the IP stack; outbound as handed to the driver. Verdicts come from the engine's seats, which stand elsewhere — the two views disagreeing is a diagnostic, not a bug to hide.
Network Manager
Peios / Using Peios / Networking
Network Manager lets you inspect and change this machine's networking
and firewall. Open the launcher and type network. Start in Overview
to find the affected interface and the profile it uses. Down the side are
three groups:
- Network: Overview, Networks, Profiles, Profile rules and DNS.
- Firewall: Exposure, Rules and Activity.
- System: Port reservations, Auditing and Advanced.
What it shows comes from netd, resolvd, the kernel and the registry under
Machine\System\Network. What it changes it writes to the registry, as
you, the same as you could with reg. Nothing else holds the network's
settings: see Networking for how they become
real.
Choose a task #
| Task | Use | Verify afterwards |
|---|---|---|
| Find why the machine is offline | Overview, then the affected interface | Carrier, address, gateway, selected rule/profile and any refusal. |
| Set an address, gateway or DNS server | Profiles; Profile rules if selecting a different profile | The profile's Result, affected interfaces, then live Overview and DNS. |
| Treat a recognized network differently | Networks | What this changes before saving; profile and firewall results after. |
| Diagnose a name | DNS → Look up a name | Actual answer, source, interface and server, not just the saved setting. |
| Allow or block a service | Rules → Test a connection, then the rule | Accepted generation and an actual connection test. Exposure and Activity contain example data. |
| Let a program listen on a port | Port reservations | The bind permission and the service's listener; firewall access is separate. |
Read the change and access rules below before applying a change, especially through a remote desktop connection. A Connected summary reports a route; it does not prove the Internet or a particular service is reachable.
Making a change #
Every change is checked before it is written. If the machine would refuse it, nothing is written, and the status line says why.
A change that would block a connection to this desktop that is open now, yours or anyone else's, asks first: This change would disconnect you, with the connections it would cut. Cancel is the safe answer. Apply anyway makes it.
Some changes are kept only when you say so. A bar across the top asks Keep this change?, with Keep and Undo, and undoes the change by itself after 30 seconds unless you keep it. The countdown runs on the machine, not in your browser, so if the change did cut you off, you need do nothing: the previous settings come back when it ends. The bar follows:
- saving or deleting a firewall rule or a profile rule;
- changing a cell on Exposure;
- applying a profile that an interface carrying a desktop connection uses;
- renaming a network, or changing its trust level, while a desktop connection is on it;
- any change made with Apply anyway.
Making another of these while the bar is up keeps the change before it. Other changes, such as DNS settings, static names, port reservations, the audit level and new profiles, are made at once, and there is no Undo. Record their previous values before editing if you may need to restore them. These changes are live; no reboot is needed.
After applying, read Overview for the actual interface result and test the name or connection you intended to use. The status line along the bottom says what the last change did, and that the firewall's policy is in force, with its generation. If the kernel refused the newest rules, it says so, and that the previous generation still stands.
What you may see and change #
What you may change is what the registry lets you write, key by key, so it is asked of the machine, not guessed from your groups. Where you may not, a section is shown read-only, with the reason said once at the top: You can see these settings. Changing them needs an administrator. Exposure and Activity are for administrators only, and say so instead.
Permissions… opens the permissions editor. It shows the same things read-only to anyone who may not change them.
Network Manager reads the machine again every two seconds, and its settings whenever something else changes them in the registry.
Overview #
Along the top is a summary: whether the machine is Connected (it has a way out), Local network only, Connecting or Offline, and its IPv4 address, gateway, DNS servers and hostname.
Under that is a map. Each interface is a line from this machine to the network it is on, with the profile it uses on the wire between them, and the internet at the end when there is a route out. An interface no rule assigns a profile to is Not managed. Wi-Fi isn't supported yet, so a wireless interface is always shown that way.
Select an interface to see it in full:
- its state and speed, with Renew lease to ask the DHCP server for a fresh lease, and Reapply to make every interface match its profile again;
- its Addresses, each with where it came from: DHCP, Static, SLAAC, Temporary (an IPv6 privacy address) or Link-local, and Deprecated for one kept only for connections already using it;
- its gateway and route metric, its DNS servers and search domains, its MTU and MAC address;
- its Traffic, received and sent, over the last minute;
- its Configuration: the profile it uses, the rule that chose it, and the network it is on;
- its Routes;
- its DHCP lease, as a bar from when it was obtained to when it expires, marked where the machine starts to renew it and then to rebind. Older versions of netd don't report a lease's times, and then the lease is shown as three stages instead, Bound, Renewing and Rebinding, with the current one lit.
An interface that isn't managed has Assign a profile, which starts a profile rule for it.
If netd isn't answering, the page says so and shows what netd last recorded. If netd refused the newest profile rules, it says why, and that the last rules that worked still apply.
Networks #
Networks lists every network this machine has connected to: its name, its trust level, what identifies it (its DHCP server or router), its subnet, and when it was last connected. A network seen for the first time is marked New network until you give it a name and a trust level.
Select one to change:
- its Name;
- its Trust level: Private, a network you control, or Public, any other network. A network never given one is offered as Public;
- its Preferred address, an IPv4 address the machine asks for first. The DHCP server may give another.
Rules can name a network and its trust level, so changing either can change what is reachable or which profile an interface uses. Before you save, What this changes lists each listening service and interface that would be treated differently.
Profiles #
A profile says how an interface stands on a network: what it takes from the network and what it sets for itself. A profile inside another inherits every setting from it and changes only what it sets. See Configuring profiles for what each setting means.
On the left is the tree of profiles, with the interfaces using each. Under it, New profile makes one at the top, Inside makes one inside the selected profile, and Delete removes the selected profile and the profiles inside it. A profile a rule still names can't be deleted.
On the right, the selected profile is summed up in five boxes, Addressing, Gateway, DNS, Hostname and MTU, each coloured by whether it comes from the network, is set here, or both. Below them is every setting, with a column for each profile from the top of the tree down to the selected one. The selected profile's column can be changed; the others show what it inherits. Result gives the value that applies, and which profile it comes from, or built in when none sets it. Choose Inherit (or Default at the top of the tree) to stop setting a value here.
Changes wait in a bar, 1 change not applied, with Discard and Apply. The bar says which interfaces the change will apply to at once, or why the machine would refuse it. Review that list before Apply. Changing an active profile's resolved settings restarts its interfaces' address clients, so a change can interrupt connectivity even when the setting edited is DNS. Check live Overview afterwards.
Profile rules #
Profile rules says which profile each interface uses. Now lists each interface, the profile it uses, and the rule that chose it. Under that are the rules. The most specific rule that matches an interface applies; priority breaks ties. An interface no rule matches is left unmanaged.
A profile rule's action is Use profile (with the profile), Unmanaged (leave the interface exactly as it is), Disabled (keep it switched off) or None. Rules are written and changed as on the firewall's Rules page. Before you save, What this changes lists each interface that would use a different profile, and a rule that would tie with another at the same priority is marked.
DNS #
Look up a name asks resolvd for an IPv4 (A) or IPv6 (AAAA) address, and shows the way the question went: the static names, the cache, the interface it was routed to and why, the server that answered, and the answer, or No such name or No answer.
Where names go shows which interface's servers each name is sent to. Check exclusive scopes, matching search domains and the default route; use the actual lookup result to confirm the choice. An exclusive scope needs an address and at least one server to take all queries. Since resolvd started counts the questions asked, how many the cache answered, and how many went to servers and failed. Flush cache empties the cache.
Under Settings are the Fallback servers, used only when no up interface has DNS servers, and extra Search domains, tried after each interface's own. Static names are answered before DNS is asked. See Name resolution for how resolvd decides.
Exposure #
Exposure answers one question: which services on this machine can be reached, and from where. Each listening service is a row, with Any other port at the end for everything else. The columns are Private networks, Public networks and This machine. Each cell is Allowed, Limited (allowed, with exceptions that block some of it), Blocked, or Not listening when the service only takes connections from this machine. Under each is the rule that decided it.
Each cell is judged by your real rules, as a new connection from a network of that trust level. Which services are listening is example data for now.
Select a cell to see why: the same explanation as Test a connection on Rules. Its button changes it in one step, Allow from public networks or Block from private networks, for example, by the smallest change to the rules that does it: narrowing a rule to the other trust level, turning it off, or adding a rule that allows the port. Where no one rule decides a cell alone, you are told to change it in Rules.
Rules #
Rules holds the firewall's rules, in three tabs:
- Connections, judged once per connection. Most rules belong here.
- Packets, every packet, after connection tracking.
- Frames, before connection tracking. Rarely needed.
Exceptions sit under the rule they narrow. Order has no effect: the highest priority wins, and a tie goes to the stricter action. Traffic no rule allows is blocked. Rules that come with Peios or its packages are marked Built-in. Network policy explains how rules decide.
Select a rule, or New rule, to write one:
- its Name, Priority and whether it is On;
- what it Matches, as conditions on facts such as the source address, the port or the network's trust level;
- its Action: Allow, Block (discard without a reply, so the sender sees a timeout), Reject (refuse, and reply "connection refused" or "administratively prohibited") or None (decide nothing and let the rule it is inside decide);
- Also: Count, Tag, Audit and Prompt, which run alongside the action;
- its Exceptions, with Add exception.
The rule is checked as you write it. A condition that can never match, a missing value, and anything the machine would refuse are marked where they are, and it can't be saved until they are fixed. What this changes lists each service whose reachability would change.
Test a connection describes a connection, incoming or outgoing, its protocol, the other machine's address, the port, the network and the service, and shows what the firewall would do with it: each layer it passes, in the order the kernel judges them, which rule decides, and every rule's part in it. There are ready-made examples, such as Your session and SSH from a café. A blocked incoming connection offers Make an allow rule….
Activity #
Activity shows what the firewall has been deciding: a chart of new connections a minute over the last fifteen minutes, allowed, rejected and blocked. Under it, Connections lists the connections open now, in or out, with the network, the result and the rule that decided it; a connection to this desktop is marked Your session. Select one to see how it was decided. Counters shows each count a rule with a Count action keeps, which rules write and read it, and, by source, whether it is over a rule's limit.
All of it is example data for now, judged by your real rules.
Port reservations #
A port reservation says who may listen on a port. Each port uses its
narrowest reservation, and a port without one uses the default. A bar draws
the reservations across the ports from 1 to 65,535. The table lists each,
with who can listen; Permissions… changes that. To add one, write it as
protocols and ports, such as tcp:8443 or tcp,udp:5000-5100. A new
reservation starts with only SYSTEM and Administrators allowed to listen.
Check a port says whether a program running as SYSTEM, an administrator, a standard user or one of Peios's services could listen on a port, and which reservation decides. Who may connect to the port is the firewall's to say, on Exposure. See Port reservations for how the kernel checks them.
Auditing #
A rule with an Audit action writes an event each time it decides, at a level from 1 to 5. Record events from level chooses the lowest level recorded: 1 records everything, 5 only the most severe, and Off nothing. Rules that audit lists each such rule, its level, and whether it is recorded.
Recorded events are in Event Viewer,
as ntfe.verdict.reported events. Recent events here is example data
for now.
Advanced #
DHCP identity shows the DUID this machine gives DHCP servers, and each
interface's Client ID, which is derived from the DUID when it is
empty. A client ID is written as bytes in hex, such as
01:52:54:00:12:34:56.
Access says who may look at and who may change netd's and resolvd's settings. Permissions… beside each opens it in the permissions editor. The hostname is shown here, and changed in System Settings.
The trust store
Peios / Using Peios / Trust
Start with trust status to inspect the machine's trust store before changing it.
Programs that use the machine's TLS trust get their certificate authorities from trustd, the trust store. A program with its own private trust store is separate; see packages and private trust stores.
Choose your task #
| What you need to do | Start here |
|---|---|
| Check store health or inspect a certificate authority | Seeing what is in force, then the trust command reference |
| Add or distrust a certificate authority | Check who may change it, then follow Adding and distrusting certificates |
| Check that a change took effect | Verify the change; a recorded registry decision alone is not proof that consumers use it |
| Investigate degraded health | When the store is degraded |
Seeing what is in force #
Start with trust status to check the store's health and whether the
compatibility files are enabled:
$ trust status
generation 7
health ok
roots 121 in force
shipped 121
added 0
distrusted 0
compat GenerateLinuxTrustFiles = 1
rendered /system/retc/ssl/certs/ca-certificates.crt
rendered /system/retc/ssl/cert.pem
generation increases every time the store changes, so a program holding
a copy can tell whether it is current without fetching it again.
health is degraded when composition or compatibility-file rendering
failed — see
when the store is degraded.
Use trust list for the roots in force and trust show <fingerprint> for
one root's full fingerprint, source and purposes. trust list --distrusted
shows the registry's distrust decisions; it does not by itself prove that
trustd has applied them. After a change, verify the effective
store.
Who may change it #
Machine\System\Trust\Certificates and its subkeys, like any registry
key, are governed by their own security descriptor. By default SYSTEM and
Administrators may write; everybody may read. That descriptor is the
only gate: trust and reg write the
same values through the same check, so there is no second permission model
to keep in step with the first.
Reading is open to everyone, because the root set is public — in the default configuration it is sitting in a world-readable file.
Where trust policy comes from #
trustd composes that set from two inputs and serves the result.
| Input | Where it lives | Who changes it |
|---|---|---|
| Mozilla's root store | /usr/share/ca-certificates/mozilla.crt, shipped by the ca-certificates package | An upgrade of that package |
| Additions and distrusts | Machine\System\Trust\Certificates in the registry | An administrator, or the domain |
The split is the whole design. The roots are package data — a hundred
and twenty-odd certificates nobody on this machine decided anything about
— and package upgrades already do the right thing with them, including
removing a root Mozilla has withdrawn. The registry holds decisions:
each entry is something somebody chose, which is what keeps an audit of
Machine\System\Trust worth reading.
What programs actually read #
The store itself is a socket, /run/trustd/trust.sock. Peios-native
programs can ask it directly and subscribe to changes. A running program
must refresh its own trust state when it receives an update; socket
publication alone does not prove that every consumer stopped using a
cached certificate.
Almost nothing does that yet, so trustd also renders the store to the files the portable software ecosystem expects:
/etc/ssl/certs/ca-certificates.crt the bundle Go and most software read
/etc/ssl/cert.pem OpenSSL's default CAfile
/etc/ssl/certs/<hash>.0 OpenSSL's default CApath
Those files are a copy of an answer, not the answer. Editing one does
not change trustd's policy, and trustd overwrites it the next time anything
changes. Use trust or Security Policy to make a lasting change. The
GenerateLinuxTrustFiles value decides whether they exist at all — see
the compat files.
Installing the root-store package alone renders nothing; the image that runs programs using machine trust must provide the service that composes and renders it. Conversely, trustd being absent or stopped does not prove that previously rendered files are absent or current. Inspect the actual consumer paths and service health rather than inferring trust from the daemon's presence alone.
Composition is all or nothing #
If the shipped bundle cannot be read, yields implausibly few roots, or would leave no usable roots, composition fails before a new set is installed. The inspected trustd 0.1.6 source leaves the previous in-memory set and rendered files untouched in that case and reports degraded health. On a first start, there may be no previous in-memory set or rendered files to retain.
Rendering is a separate step. In that source, the socket's new set is installed before the compatibility files are written, and a rendering failure does not roll it back. The files can therefore disagree with the socket or with one another. Composition's all-or-nothing check is not a transaction across every trust consumer; see the rendering limits and degraded-store diagnosis. This is a pinned source finding, not verification of an installed image.
A single bad entry is different. An addition that is not a certificate, is not a CA, or has expired is skipped with a warning and everything else proceeds — one administrator's typo does not take the machine's trust down.
What is recorded #
Each time the set of roots in force changes, trustd writes one event per root that entered or left it:
| Event | When |
|---|---|
trustd.root.added | A root entered the set: an addition, a distrust lifted, or a root a bundle upgrade brought in |
trustd.root.distrusted | A distrust took a root out of the set |
trustd.root.removed | A root left the set for any other reason: its addition was withdrawn, or a bundle upgrade dropped it |
Each carries the certificate's SHA-256 fingerprint in
object.certificate.digest, its subject name in object.certificate.name,
and what it is trusted for in object.certificate.purposes, written in
kebab-case (ServerAuth becomes server-auth). Search by the digest: it
is the only one of the three that identifies a certificate.
The records say what changed, not who changed it. An addition or a
distrust is a registry write, recorded by the registry itself where the key
is audited, and a bundle upgrade is a package operation, recorded by peipkg.
The set trustd composes when it starts is not recorded, so a change made
while trustd was not running is seen only as part of that starting set.
The events are standard, so the emission policy can switch them off.
What trustd is not #
It holds no private key. Not the machine's, not anybody's. A service that holds keys is a signing oracle if it is ever compromised, and wants almost the opposite security posture from one whose socket is open to every program on the machine. Machine identity — the certificates and keys a machine presents when it is the server — is a separate concern, and will be a separate service.
It does not fetch anything. No root is downloaded on demand and no revocation list is retrieved, in this version. What the machine trusts changes when a package is upgraded or somebody writes to the registry, and at no other time.
Adding and distrusting certificates
Peios / Using Peios / Trust
Two things can be decided about this machine's trust, and both are values
under Machine\System\Trust\Certificates. Change them with trust on a
terminal, or Security Policy on the desktop (type security in the
launcher), which takes the same steps.
Before making a change, check trust status and trust list. You need
write access to the relevant registry key; by default SYSTEM and
Administrators may write, and everybody may read. See who may change
it.
In Security Policy #
Security Policy's Certificates sections show what trust status,
trust list and trust list --distrusted show, and change it.
- Trusted lists every certificate authority in force, those shipped with Peios and those added on this machine, with a search above them. Opening one shows who issued it, when it is valid, its key, serial number and SHA-256 fingerprint, and what it is trusted for. Export… saves it as a PEM file. Distrust… stops trusting it, and Remove… takes back one this machine added; both ask first, under their row.
- Add Certificate… opens a PEM or DER file and shows what is in it before anything is trusted. A file that holds several certificates, a certificate that isn't a CA, one that has expired, or one already trusted or distrusted is shown with the reason and can't be added. Otherwise give it a name and choose its purposes, then Add Certificate.
- Distrusted lists what is refused, with the reason it was given, and names each certificate where this machine has a copy. Trust Again… restores one after asking. Distrust by Fingerprint… refuses a certificate the machine doesn't have, by its whole fingerprint, or by a prefix of one it does.
- Settings shows whether trustd is up to date, with Reload for
those trustd lets reload it; turns the files under
/etc/ssl on and off; and opens the
permissions editor on
Machine\System\Trust(Changing Trust) and on trustd'sControlSecurity(Trust Service).
The window follows trustd, so a change made elsewhere, with trust or
reg, appears in it within a few seconds. What you may not change is
shown read-only, with the reason said once.
Trusting a certificate authority #
An internal CA, a test CA, a partner's CA:
$ trust add corp-ca /path/to/corp-root.pem
added corp-ca
CN=Corp Issuing CA,O=Example Ltd,C=GB
SHA-256 4f2a…c19b
The name is yours to choose — it names the decision, not the
certificate, so corp-ca is a better name than 4f2a. PEM or DER is
accepted; a file holding more than one certificate is refused, because a
bundle added under one name would be one decision covering several
authorities.
The certificate is checked before it is written: it must parse, it must be
a CA (basicConstraints), and it must not have expired. trustd checks it
again when it reads it — it never trusts the writer — but doing it here
means a mistake is a message rather than a line in a log.
To withdraw this named addition:
$ trust remove corp-ca
Purposes #
By default an addition is trusted for ServerAuth — validating a TLS
server. Say otherwise if you mean otherwise:
$ trust add corp-ca corp-root.pem --purposes ServerAuth,CodeSigning
The purpose is recorded with the addition and can filter socket queries.
Do not assume it limits the compatibility files. The inspected
trustd 0.1.6 renderer
writes every composed root to both PEM bundles and to the
hashed directory,
including an addition whose purposes omit ServerAuth. This differs from
the intended ServerAuth-only rendering
described in the source.
Do not use a non-ServerAuth purpose as an isolation boundary for a machine-wide addition on that implementation. The certificate's DER is preserved; this finding does not establish whether a particular TLS consumer accepts it, which also depends on the certificate and that consumer's validation rules. This is source inspection, not a live TLS test or a claim about every installed version.
Distrusting a certificate authority #
This is the one that has to work in a hurry, and it works on any certificate — one Mozilla shipped or one somebody added:
$ trust distrust 018e13f0772532cf --reason "withdrawn, see CA/B incident 2026-08"
distrusted 018e13f0772532cf809bd1b17281867283fc48c6e13be9c69812854a490c1b05
was: CN=DigiCert TLS ECC P384 Root G5,O=DigiCert\, Inc.,C=US
After trustd observes the decision and successfully composes and renders the new set, the root is absent from its socket set, the rendered bundles and the hashed directory. Withdrawal does not require a package update. Check the result below: a composition failure retains the old set, while a rendering failure can leave the socket and files different. Neither a registry write nor socket readback proves that every running application has stopped using a cached copy.
A certificate is named by its SHA-256 fingerprint — never by subject name, which is forgeable and reused. Any of these work:
018e13f0772532cf the prefix `trust list` prints
018E13F0…C1B05 upper case
01:8e:13:f0:… colon-separated, as other tools print it
/path/to/the-certificate.pem the file itself
Distrusting a certificate this machine does not have is legitimate and is recorded rather than refused — the entry sits there, and if that root ever arrives in a bundle upgrade or an addition, it is already refused.
To see what has been decided, and to undo it:
$ trust list --distrusted
018e13f0772532cf withdrawn, see CA/B incident 2026-08
1 distrusted
$ trust restore 018e13f0772532cf
restored 018e13f0772532cf809bd1b17281867283fc48c6e13be9c69812854a490c1b05
Verify the change #
A successful write records a decision. Check that trustd has applied it:
- Run
trust status. If health isdegraded, follow the recovery steps below. If entries were skipped, read the warnings in trustd's log; a skipped entry does not establish trust. - Run
trust listto inspect the effective set. For an addition, usetrust show <fingerprint>to check its full fingerprint, source and purposes.trust list --purpose ServerAuthfilters the socket reply; it does not inventory or constrain the compatibility files. See the purpose limitation. - For a distrust, check that the certificate is absent from
trust listand that the decision appears intrust list --distrusted. The latter reads the registry, so it is not enough on its own. Aftertrust restore, check the effective set again: removing a distrust does not supply a certificate that is no longer shipped or added.
Retest the affected application. Programs with private trust stores use
their own policy; a successful trust command is not an application TLS
test.
Doing it from the registry #
trust writes registry values and nothing else, so reg does the same job
and is subject to exactly the same access check:
Machine\System\Trust\Certificates\
Add\<name>\
Certificate REG_BINARY the certificate, DER
Purposes REG_MULTI_SZ default ServerAuth
Distrust\
<fingerprint> REG_SZ why, for whoever reads it later
Write Certificate last when creating an entry by hand. Until it
exists the entry is incomplete, and trustd deliberately skips incomplete
entries rather than acting on half of one.
This is also how a domain distributes trust: pushing a value to
Trust\Certificates\Add on every member is an ordinary policy write, not
a special mechanism.
Packages may offer trust, never take it #
A package must never widen what the whole machine trusts by being
installed — peipkg install quietly adding a CA is a supply-chain hole,
and every system that allowed it eventually closed it.
A package that comes with a CA ships a seed (/usr/share/regim/…),
inert until an image names it in [registry] autoapply or an
administrator applies it. Installing the package grants nothing.
A package shipping a trust store for its own private use — a browser, a language runtime — is unaffected by any of this. Those are ordinary files that only that program reads.
When the store is degraded #
$ trust status
health degraded — cannot read /usr/share/ca-certificates/mozilla.crt: …
degraded can report either a composition failure or a rendering failure.
They need different readback. In the inspected
trustd 0.1.6 refresh path,
a composition failure leaves the previous set and files untouched. A
rendering failure happens after the socket set has changed; it can leave
old, new or missing compatibility artifacts. Do not assume all consumers
still use one last-good store.
Use the error in trust status to choose the next step:
- For a missing or damaged bundle, check the installed
package
and repair the package problem. Do not edit the generated
/etc/sslfiles. - For a distrust list that would empty the store, review
trust list --distrustedand correct unintended decisions. Do not restore an intentionally distrusted CA just to clear the health warning. - For
render failed, preserve the operation and path in the error and investigate that filesystem failure. Compare the socket set with the files the affected program actually uses; therenderedpaths can be retained from the last successful render and are not a fresh file inventory after failure. Do not hand-edit the generated files or assume a successfultrust listproves they were updated. See compatibility rendering limits.
Once the cause is fixed, ask trustd to recompose:
$ trust reload
Reload requires permission to control trustd; Security Policy exposes its
ControlSecurity permissions under Settings → Trust Service. If reload
is refused, have an authorized administrator perform it.
The inspected daemon replies successfully to an authorized trust reload
even if recomposition or rendering failed. Run trust status again and
confirm health is ok, then verify the intended
change and retest the affected application. Until
recovery succeeds, do not assume a newly recorded addition or distrust is
in force for every consumer.
Skipped entries are different and are not a degraded state: trust status
counts them and the log names each one. Correct the named entry, then check
the effective set again.
The trust command
Peios / Using Peios / Trust
trust is the trust store's operator command. It reads the effective
store over trustd's socket and writes decisions to the registry. Start
with trust status for health, trust list for the roots in force, and
trust show <fingerprint> for one root's details.
The registry holds local additions and distrusts, not the shipped roots.
trust list --distrusted reads those decisions directly. Writes made with
trust and reg pass through the same access check. For changes and
verification, follow adding and distrusting
certificates.
Verbs #
| Command | Reads | Writes |
|---|---|---|
trust list [--purpose P] | socket | — |
trust list --distrusted | registry | — |
trust show <fingerprint> [--pem] | socket | — |
trust status | socket | — |
trust add <name> <file|-> [--purposes A,B] | the file | registry |
trust remove <name> | — | registry |
trust distrust <fingerprint|file> [--reason R] | socket, then | registry |
trust restore <fingerprint> | registry | registry |
trust reload | — | — (asks trustd to recompose) |
Listing #
$ trust list
018e13f0772532cf shipped CN=DigiCert TLS ECC P384 Root G5,O=DigiCert\, Inc.,C=US
0a81ec5a929777f1 shipped CN=GlobalSign,O=GlobalSign,OU=GlobalSign Root CA - R3
4f2ab19c8e10dd77 added:corp-ca CN=Corp Issuing CA,O=Example Ltd,C=GB
121 root(s)
The first column is the first sixteen characters of the SHA-256 fingerprint, and every verb that takes a fingerprint accepts that prefix — what is printed can be pasted back in. An ambiguous prefix is refused rather than guessed at.
--purpose ServerAuth narrows the socket listing to roots carrying that
purpose. It is not an inventory of the compatibility files: the inspected
renderer does not filter them by purpose. See the source-qualified
limitation.
Showing one root #
$ trust show 4f2ab19c
subject CN=Corp Issuing CA,O=Example Ltd,C=GB
fingerprint 4f2ab19c8e10dd77…
source added
added as corp-ca
purposes ServerAuth
expires 2074003199
-----BEGIN CERTIFICATE-----
…
--pem prints the certificate alone, for piping into a file or another
tool.
Exit statuses #
| Status | Meaning |
|---|---|
0 | Done. |
2 | Asked about something that is not there — no root matches, no such addition, not distrusted. |
1 | It went wrong: the daemon is unreachable, the file is not a certificate, the registry refused the write. |
64 | The command line was not understood. |
The separation of 2 from 1 is what lets a script tell "this machine
does not trust that CA" from "I could not find out". A successful write
means the decision was recorded; verify the effective
store before treating
the change as applied. Likewise, a successful trust reload means the
request was accepted: the inspected daemon
returns success even when refresh left degraded health. Read trust status afterwards.
When a write is refused #
$ trust distrust 018e13f0772532cf
trust: not permitted to change Machine\System\Trust\Certificates\Distrust —
changing what this machine trusts is governed by that key's descriptor
trust has no privilege of its own to grant. Changing the machine's trust
is a registry write, and whether you may make it is decided by the key's
security descriptor — the same answer reg would get. Ask an administrator
with write access to make the intended change; editing a generated
certificate file is not a substitute.
trust reload is a service-control request, not a registry write. Its
permission is separate from permission to change trust decisions; Security
Policy exposes trustd's ControlSecurity under Settings → Trust Service.
Reloading through the service manager #
svctl --wait reload trustd and the service manager's Reload action send
SIGHUP to trustd. The daemon consumes it in its normal event loop and uses
the same recomposition path as trust reload, without replacing its main
process. Repeated pending signals may be coalesced into one refresh.
Check trust status for the resulting health. A service reload completing
does not prove that recomposition succeeded; on failure, trustd retains its
last rendered store and reports degraded health. Older daemon versions that
do not handle SIGHUP can terminate on service reload; use trust reload
directly until the updated daemon is installed.
The files under /etc/ssl
Peios / Using Peios / Trust
The machine's trust store is a socket. The files under /etc/ssl are a
compatibility rendering of it, for software that reads trust from a path —
which today is very nearly all software.
They are generated. A manual edit lasts only until the next change, and a
header at the top of the bundle says so. Use adding and
distrusting to change trust policy.
Use trust status to check whether these files are enabled and where they
were rendered.
Three artifacts, because the ecosystem does not agree #
| Path | Who reads it |
|---|---|
/etc/ssl/certs/ca-certificates.crt | Go probes this first; most software is configured with it |
/etc/ssl/cert.pem | OpenSSL's default CAfile on this system |
/etc/ssl/certs/<subject-hash>.0 | OpenSSL's default CApath |
The hashed directory is not decoration. OpenSSL looks a certificate up
there by a hash of its subject, so updating a PEM bundle alone does not
withdraw a root from that lookup path. Roots whose subject hashes collide
use successive suffixes, .0, .1 and so on.
The inspected trustd 0.1.6 renderer
builds a new certs directory, installs it, and then replaces cert.pem
separately. It first tries an atomic directory exchange. Its
fallback path
uses separate renames, which can temporarily leave certs absent; failure
after the first rename can leave it absent until repaired. There is no
single atomic switch covering the socket and all three file paths.
A rendering error can occur after some artifacts changed, and the socket set is updated before rendering starts. Follow degraded-store diagnosis rather than assuming a last-good store everywhere. These are pinned source limits, not live filesystem or installed-image verification. Also see the purpose-filtering limitation: that renderer includes all composed roots, regardless of their recorded purposes.
Turning them off #
Machine\System\Trust GenerateLinuxTrustFiles requests the compatibility
mode. These effects require successful rendering or removal:
| Value | Effect |
|---|---|
1 (default) | Rendered and kept current. |
0 | No files. Programs that read a path find nothing and fail with certificate errors. |
2 | Reserved. Treated as 1. |
0 is the machine whose entire software set asks the socket — an
appliance image, in practice, where you know what is installed. On a
general-purpose system it breaks curl, Python, Go binaries and anything
else that reads a bundle, and the breakage looks like a certificate
problem rather than a configuration one, so set it deliberately.
Switching to 0 asks trustd to remove the rendered files. Successful
removal leaves the directory itself empty. The inspected renderer removes
cert.pem and then the contents of certs in separate operations; a
failure can leave some files behind. Check health and the actual paths
before treating compatibility trust as disabled. A saved 0 or a socket
reply alone does not prove removal succeeded.
$ reg set Machine/System/Trust GenerateLinuxTrustFiles dword:0
$ trust status
compat GenerateLinuxTrustFiles = 0 (no files; the socket is the only store)
The socket keeps serving throughout — trust list still answers, and so
does any program that speaks to trustd directly.
In Security Policy, the Certificate Files switch under Settings writes the same value. Turning it off asks first because programs that rely on these paths can lose their trust store. Allow trustd to apply the change, then verify health and the affected application.
To restore compatibility files after disabling them:
$ reg set Machine/System/Trust GenerateLinuxTrustFiles dword:1
$ trust status
Confirm GenerateLinuxTrustFiles = 1, the rendered paths, and health of
ok, then retry the affected program. If the store is degraded, follow
the recovery steps
before relying on the rendered trust.
Why a copy, when name resolution gets a pointer #
/etc/resolv.conf on Peios is a constant file naming the resolver, and
nothing is ever generated into it. Trust cannot work that way: there is no
"ask over here" a certificate bundle can express. A file that lists
authorities has to be the list.
That is the whole difference between the two, and the reason this one carries a knob and a warning while the other does not.
Keeping the time
Peios / Using Peios / Time
Start by asking timed, the service that keeps the machine's clock, what it is doing:
$ clock status
$ clock sources
These commands inspect the clock; they do not change it. In clock status,
read state, time zone, following, accuracy and the age of the
last update. In clock sources, look for a chosen source, contributing
candidates, authentication and any failure notes. The clock command
reference explains each field and the
command's exit statuses.
Choose the next step from what you find:
- The displayed time is in the wrong zone: check the effective time zone before changing the machine clock. The clock runs in UTC; a zone changes how programs show it. Follow the time-zone procedure.
- The machine is unsynchronised or sources are failing: inspect the
configured policy and each source's state in configuring time
sources. If status says
Automatic is 0: the clock is set by hand, automatic time is disabled. - The state is
settlingorspike: timed is still converging or checking a large change. Read the status meanings before intervening. - There is no suitable time server: use the manual-time procedure. Check the zone and the consequences of a clock jump first.
After a change, run both inspection commands again. A saved setting or a successful reload does not by itself establish synchronisation. Check the effective zone, source list, authentication, state, accuracy and fresh updates rather than relying only on the displayed wall time.
Setting the clock needs SeSystemtimePrivilege. timed's service identity
holds it, and so do Administrators. timed runs from boot and keeps the
clock from network time servers. Changing it behind timed's back can lead
to a correction or a refusal of an excessive offset; see why not just set
the clock.
Registry changes and timed control requests have separate permission
checks.
Why this is worth caring about #
A wrong clock does not look like a wrong clock. It looks like:
- certificates that are expired, or not yet valid, and TLS that fails for reasons that make no sense;
- Kerberos tickets refused across a domain, because Kerberos treats a clock skew as evidence of a replay;
- logs from two machines that cannot be put in order, which is exactly when you most need them to be.
So time is not a convenience. It is something several other things quietly depend on, and an attacker who can move it has more than a wrong clock.
Sources are not trusted individually #
timed compares several servers. Each one reports not just a time but an interval in which it estimates the true time lies, and timed looks for the largest set of intervals that overlap. A server outside that overlap is a falseticker and is discarded, however confident it sounded.
This can reject a wrong source whose narrow interval disagrees with the others. It is not a guarantee that the agreeing sources are correct: shared upstream clocks, operators or network failures can give several sources the same error. Authentication identifies a source; it does not prove that source's clock is right.
Configure at least three sources with independent failure modes. With one, there is nothing to compare it against. With two that disagree, timed reports itself unsynchronised rather than choosing between them. Three can let an agreeing, correct majority exclude one wrong source, provided those assumptions hold. Counting three names alone does not establish that independence.
Every source is authenticated #
By default timed will not use a source that cannot prove who it is. That is NTS (RFC 8915): a TLS handshake on port 4460 establishes a pair of keys, and every NTP packet afterwards carries a tag computed with them. Forging a reply requires the key.
The certificate is validated against the machine's trust store, over trustd's socket — so a certificate authority you distrust is distrusted here too, immediately, without timed needing to be restarted.
When timed needs a new NTS handshake, a network that blocks port 4460
prevents that exchange. Cached cookies can avoid a fresh handshake, as
explained below; blocking the port does not by itself mean existing
authenticated time is lost. If no usable sources remain, the machine
reports itself unsynchronised. AllowUnauthenticated exists for networks
where that is not acceptable; see configuring
sources for what you give up.
The default sources #
The documented shipped fallback set uses four names in our own zone, when no earlier source selection supplies a list:
0.time.peios.org → time.cloudflare.com
1.time.peios.org → nts.netnod.se
2.time.peios.org → ptbtime1.ptb.de
3.time.peios.org → ptbtime2.ptb.de
The list is intended to span three operators in three jurisdictions: a
commercial network, a Swedish national infrastructure operator, and
Germany's national metrology institute. Two of the names are from the
same operator; four names are not four independent sources. Operator
variety reduces a shared failure risk, but is not proof of independent
upstream clocks or correct time. Check clock sources for what the
machine actually selected and can reach.
The indirection through time.peios.org is deliberate. If an operator
withdraws its service, that is a DNS change rather than a new image for
every machine.
Two circles, and how they are broken #
Both are worth knowing about, because both look like bugs when you meet them.
NTS needs TLS; TLS needs a clock. A machine with a dead RTC battery
may boot reading 1970. Otherwise valid certificates can then appear
"not yet valid", blocking the handshake that would obtain authenticated
time. timed sets a lower bound at its own build timestamp, reported as floor
in clock status. The clock-step event
reference calls this boot-floor
and explicitly says the real time is not yet known.
The floor is a bootstrap aid, not current time or a guarantee that TLS will succeed. A remote server's certificate still has to be valid at the clock's reading and trusted by the machine. Reachability, authentication and source agreement still matter, so do not assume the first poll will fix the clock. If it remains on the floor, inspect the source failure notes rather than treating the build date as a verified time.
A fresh handshake needs a clock too. So the cookies from the last one
are kept under /var/state/timed, and a reboot usually needs no handshake
at all. The startup hook establishes a complete protected, inheritable
descriptor before creating the cookie directory: SYSTEM, Administrators and
timed's service SID have access. Inherited public read grants are removed from
the state directory; a Unix mode on a cookie file is not the security boundary.
Steering, not jumping #
Once the time is known, timed slews the clock: it changes the rate slightly so the error is absorbed over the next few minutes. A great deal of software quietly assumes the clock only goes forwards, and stepping it backwards breaks timers, file timestamps and anything measuring a duration.
The documented automatic discipline permits steps in these situations:
- at startup, once, however large the correction, when usable sources agree;
- after fifteen minutes of consistent disagreement, when the offset is too large to slew away. Fifteen minutes because a congested network or a server having a moment looks exactly like a genuine step until time passes, and stepping the clock on a transient is worse than being slow to correct a real one.
An offset larger than a thousand seconds appearing after the machine was synchronised is refused outright and logged loudly. A machine that far out has a problem a time client should not paper over.
Manual sets and the boot floor are also clock steps. Turning automatic time back on starts the sources afresh and can cause an initial correction of any size. Plan for that jump before re-enabling it; source changes and reloads are not a promise of uninterrupted wall-clock time.
Every step is recorded in the event log as a timed.clock.stepped event,
with the clock's reading just before and just after it, so a jump in the
times of other records can be explained. Its operation.name says which
step it was: automatic for timed's own, manual for a clock set by
hand, and boot-floor for
the clock raised to the build's timestamp at startup (see Two circles, and
how they are broken). Its
subject.token.sid is whoever acted: the person who asked, for a manual
set, and timed's own service SID for the other two. Slewing is
not recorded. The event is essential, so the emission policy cannot
switch it off.
Learning the crystal #
The clock is a crystal running at the wrong rate — consistently wrong, by
some tens of parts per million. timed learns that rate and writes it to
/var/state/timed/drift, so the next boot starts with the previously
learned rate rather than learning it entirely from scratch. This can
shorten settling time; it does not guarantee a particular accuracy after
one poll.
The rate estimate also helps when the network goes away. Holdover still depends on how stable the crystal remains and how accurate that estimate was. Use the reported state and accuracy rather than assuming a fixed period of correct time without sources.
What timed does not do #
It is not a time server. It listens on no network port and initiates every conversation it takes part in. Serving time to other machines will be a separate, unprivileged package — the client holds the dangerous privilege, and a server is exposed to the whole network and needs no privilege at all. Keeping them apart is the point.
It does not touch the hardware clock directly. Once timed reports the clock as synchronised, the kernel writes it back to the RTC every eleven minutes on its own.
It does not decide how the time is shown. The clock runs in UTC.
The time zone, Machine\System\Time TimeZone, sets the machine's default
local zone; timed copies the chosen zone into /etc/localtime as the
default local-zone data. For command-specific UTC or TZ choices, see
the time-zone guide.
Configuring time sources
Peios / Using Peios / Time
Inspect the clock and the sources before changing policy:
$ clock status
$ clock sources
$ reg get Machine/System/Time
clock shows timed's current state; reg get
reads the effective registry values. Check
Automatic, Servers, AllowUnauthenticated and UseFromDHCP against the
sources actually shown. If status says
Automatic is 0: the clock is set by hand, follow the manual and automatic
time procedure
before expecting source updates. A wrong displayed zone is a time-zone
setting, not by itself a
reason to replace sources.
Everything about where a machine gets its time is under
Machine\System\Time. There is no configuration file, and clock writes
no policy. Change it with reg, or System Settings' Time Servers group
in Time & Date, which writes the same values.
Policy changes need registry write access to Machine\System\Time.
clock reload and clock set separately need the control right on timed's
control object. Having one does not imply the other; see when reload is
refused. Inspection
through clock is available to everyone under the default descriptor.
When something is wrong #
$ clock sources
source state auth str reach offset delay last
* 0.time.peios.org system-peer nts 3 377 -1.204ms 14.2ms 31s
+ 1.time.peios.org candidate nts 2 377 -0.918ms 22.7ms 44s
x 2.time.peios.org falseticker nts 2 377 +4.102s 18.1ms 12s
3.time.peios.org unreachable nts 16 0 +0ns 0ns -
NTS-KE failed: connecting to 192.0.2.9:4460: Connection refused
falseticker— this server disagrees with the others. Investigate it and the agreement among the other sources; disagreement is not by itself proof of which clock is correct.unreachable— no reply for eight polls. The note says why.reachis the last eight polls in octal, newest in the low bit:377is eight for eight,376means the newest poll was missed, and0means no replies in that window.
In the example, the final source failed to connect for NTS key exchange on port 4460. Investigate the reported failure before changing authentication policy. A source missing from the list may have a malformed configuration entry; check the log for a warning. The clock reference describes the other states, authentication, poll history and failure notes.
If clock cannot reach timed, the command failed to inspect the clock;
that is different from a successful query reporting no usable sources.
Keep the error and check the exit
status before deciding what
to change.
Naming your own servers #
Each entry is a host, optionally host:port, optionally followed by
option words:
| Option | Meaning |
|---|---|
prefer | Lead with this one when several agree. |
unauthenticated (or noauth) | This source speaks plain NTP, not NTS. |
prefer breaks ties and nothing more. A preferred source that disagrees
with the majority is still discarded — a preference is a statement about
which server to lean on, not a licence to be wrong.
A misspelled option is an error and the entry is ignored with a warning in
the log, rather than silently doing nothing. clock sources will show the
entry missing.
Choose NTS-capable servers unless you have deliberately accepted the tradeoff in turning authentication off. Source changes affect future corrections, so plan for their effect on work that depends on timestamps. A successful reload does not guarantee that the clock will only slew; see steering, not jumping.
For example, to replace the source list with three servers you operate,
pass multi: one comma-separated data token, as in reg's value
syntax:
$ reg set Machine/System/Time Servers multi:'dc01.corp.example,dc02.corp.example,ntp3.corp.example'
$ clock reload
Precedence is first match, not merge #
Servers → DHCP, if enabled and offered → the shipped fallback set
That is the selection order in the inspected
timed 0.1.10 source.
A separate domain-discovery tier is mentioned as a future version in its
configuration comments; it is not implemented in this selector. A domain
policy that writes Servers still takes effect as registry configuration.
This is a pinned source finding, not verification of an installed image.
The first nonempty list is the whole list. Servers means entries that
parsed successfully; if all are malformed, there is no explicit list and
selection continues. A valid explicit list replaces the fallback set.
Authentication filtering happens after selection: if the selected list
contains only plain-NTP sources and AllowUnauthenticated is 0, those
sources are excluded without falling through to public NTS servers.
To remove an explicit override, delete the value. This restores selection by the chain above: DHCP can take precedence when enabled and offered, and the shipped fallback is used only if no earlier choice supplies a list. Deletion does not necessarily select public servers:
$ reg del Machine/System/Time Servers
$ clock reload
Turning authentication off #
AllowUnauthenticated is 0 by default: a source must prove who it is or
it is not used.
What you give up is worth being clear about. Without NTS, the only thing standing between you and a forged reply is that the server must echo the exact 64 bits timed put in its request — a random number, so an attacker who cannot see the request cannot guess it. An attacker who can see it can forge freely and move your clock wherever they like.
The reason to think twice is that a wrong clock is not a self-contained problem: certificate validity, Kerberos, and the ordering of your logs all rest on it.
Reasonable uses: an isolated network with its own stratum-1 appliance; a lab; a machine behind a firewall that blocks port 4460 where you control the path anyway.
For an explicitly configured plain-NTP source, both the per-source
unauthenticated (or noauth) word and the machine-wide
AllowUnauthenticated value are needed. The word identifies the plain
source; the value permits its use. Setting AllowUnauthenticated back to
0 excludes all unauthenticated sources, including per-source exceptions;
it does not disable authenticated sources.
The following example replaces the entire source list with one unauthenticated server. Use it only if that is the intended policy; it provides no comparison against independent sources:
$ reg set Machine/System/Time AllowUnauthenticated dword:1
$ reg set Machine/System/Time Servers multi:'ntp.lan unauthenticated'
$ clock reload
DHCP time servers #
UseFromDHCP is 0 by default, as on Windows. DHCP sources are eligible
only when no successfully parsed explicit Servers list supplies names,
according to the source-qualified precedence above. They are unauthenticated, so they also
require AllowUnauthenticated to be 1. Enabling UseFromDHCP alone does
not meet that authentication policy.
Before enabling this, decide whether you trust the network's time servers. On a network you do not control, a DHCP server can offer an attacker's time source. The setting follows the machine to other networks; it is most appropriate for a machine that only attaches to networks you run. The authentication tradeoff applies.
$ reg set Machine/System/Time UseFromDHCP dword:1
This changes eligibility; it does not override a valid Servers list,
enable unauthenticated time by itself, or prove DHCP offered a usable
source. netd's resolver
channel reports DHCP
option 42 time servers independently of Dns.Offered; timed decides
whether to use them under its own policy. Reload after the intended
policy changes and verify the selected list and auth column. Do not
delete a working explicit list merely to make DHCP take precedence.
Poll intervals #
MinPoll default 6 64 seconds
MaxPoll default 10 about 17 minutes
Both are powers of two seconds, and both are clamped to the range 4 to 17.
timed lengthens the interval towards MaxPoll while the clock is being
held steadily and shortens it when the offset starts moving.
There is rarely a reason to change either. Lowering MinPoll makes
recovery from a disturbance faster and costs the servers more traffic; a
long interval measures the crystal's rate better than a short one
measures its phase, and it is the rate estimate that holds the clock right
between polls anyway.
The floor of 4 is not negotiable. A configuration error must not be able to turn a machine into a nuisance at somebody else's public server.
Doing it from a domain #
Every value here is an ordinary registry write, so distributing time policy to a fleet is an ordinary policy push. There is no separate mechanism and nothing timed-specific to learn.
Reload and verify #
After the intended registry changes, ask timed to reread them and inspect the result:
$ clock reload
$ clock status
$ clock sources
Check that the expected sources are present, their authentication matches
the policy, and the failure notes are resolved. Then check the state,
following source, accuracy and age of the last update. settling can be
normal while the frequency estimate converges; inspect again as polls
arrive rather than treating a successful reload as synchronisation.
If reload is refused, preserve the exact error and check the separate control permission. Do not assume a failed reload undid a registry write. If sources are missing or still rejected, return to when something is wrong and compare stored policy with timed's reported state before making another change.
The time zone, and setting the clock by hand
Peios / Using Peios / Time
The clock always runs in UTC. The time zone is how the time is shown:
the offset from UTC, and when summer time starts and ends, for one place.
The machine has a default time zone. Programs can show time in another
zone: use date -u
for UTC, or env for a
one-command TZ override in programs that honor it.
Before changing the machine's default, inspect clock status and
clock sources: a wrong zone changes the display, while
an unsynchronised clock needs a source or manual-time correction. Inspect
the reported zone and any timed errors rather than assuming a saved choice
took effect. An unexpected UTC report can also mean that timed could not
identify the existing file; see the terminal checks.
To choose a 12- or 24-hour display for your own top-bar clock, use Desktop Settings' Time Format.
In System Settings #
System Settings, from the launcher, has the machine's time in its Time & Date section. A choice from a list or a switch applies the moment it is made; one that can't be is put back, and the line along the bottom of the window says why.
- At the top is the time here, the zone and how far it is from UTC, and whether the clock is synchronised, and with which time server.
- Time Zone lists the zones by region and chooses the machine's default. The desktop's clock shows the chosen zone from the next minute. UTC takes it away.
- Set Time Automatically is on until you turn it off. Then a date and a time open under it, in the zone chosen, with Set Clock. Turning the switch on again goes back to the time servers. It starts them afresh and can jump the clock by any size when sources agree. Plan for the effect on timers, file timestamps and log ordering before switching it on.
- Time Servers are where the time comes from: Change… opens the list under its row, and Allow Unauthenticated Servers, Use Network Time Servers and the minimum and maximum poll intervals apply as they are changed. See configuring time sources. Server Status lists each server: whether it is in use, a candidate, or rejected, whether it is authenticated (NTS), its offset, and when it was last heard.
Changing the settings needs write access to Machine\System\Time, which
as shipped only Administrators have. Anyone else sees all of it, and the
section says once, at the top, why they can't change it. Asking timed to
set the clock or reload separately needs its control right, granted by
default to SYSTEM and Administrators. The control
descriptor and the
registry key's write permissions are separate checks.
After a change, check the zone and synchronisation shown at the top, and Server Status when using automatic time. A saved setting is not proof that a source has been accepted or that the clock has synchronised.
In a terminal #
The time zone is Machine\System\Time TimeZone, a name from the tz
database such as Europe/London, America/New_York or Asia/Kolkata:
$ reg set Machine/System/Time TimeZone sz:Europe/London
$ clock status | head -2
generation 12
time zone Europe/London
timed applies the chosen zone to /etc/localtime after observing the
registry change. A name that isn't a zone on this machine is refused
before the copy and logged, leaving the
previous file in place. On restart, timed identifies that previous zone
only when its saved name and the installed zone file still match the copy.
In the inspected timed 0.1.10 source,
an unidentified existing zone file is reported as UTC; see the status
limitation. timed tries a refused
name again every ten minutes and whenever the time settings change, so a zone that a tzdata update adds is taken up
without setting the value again. Deleting the value requests UTC.
If status unexpectedly shows UTC after a restart or a failed zone change, inspect timed's log for zone-copy or saved-name errors before setting a local time. The zone file must already have been prepared by timed's pre-start hook; a failed write is not the same as an invalid zone name. An error after the file was truncated can leave it incomplete, even while status retains the previous name. Do not treat that name as a fresh check of the file after a write failure.
The names are the files under /usr/share/zoneinfo, from the tzdata
package. zone1970.tab there lists the ones worth choosing between, one
per region whose clocks have agreed since 1970.
To set the clock yourself, first inspect the zone reported by clock status and resolve an unexpected UTC report or zone-copy errors as above.
You need registry write access to turn automatic time off and
timed's control right to set the clock. A manual set is a jump: plan for
its effect on timers, file timestamps and log ordering before proceeding.
Then turn off setting it automatically and ask timed:
$ reg set Machine/System/Time Automatic dword:0
$ clock set 2026-10-04 14:05
the clock is set
clock set takes a local time, in the machine's zone. timed refuses while
Automatic is 1, since its next poll would put the clock back. Inspect
clock status after the set: check the reported zone and the
Automatic is 0: the clock is set by hand indication. Manual time is not
network synchronisation.
Setting Automatic to 1 again starts the time servers afresh. Once
usable sources agree, the initial correction can be of any size. Plan
for that clock jump before re-enabling automatic time. Afterwards, run
clock status and clock sources and check the selected sources,
authentication, state, accuracy and fresh updates. Re-enabling the setting
does not itself prove synchronisation.
Why not just set the clock? #
Administrators hold SeSystemtimePrivilege, so date -s would set the
clock. timed would then measure the clock as wrong by that much and put it
back, and if it was more than 1000 seconds after timed had been keeping
the clock, it would refuse to follow its own sources and log it as a
problem. Asking timed is what keeps the two in agreement.
A clock set by hand is written to the hardware clock within eleven minutes, so it survives a reboot.
Asking timed also leaves a record. Each set is a timed.clock.stepped
event with operation.name manual, the clock's reading just before and
just after, and the SID of whoever asked in subject.token.sid. date -s
leaves none: the clock moves underneath every record without anything
saying so.
The clock command
Peios / Using Peios / Time
clock reads over timed's socket and prints. It writes no policy —
time policy is registry configuration, and reg is how a registry value is
set. Registry writes and timed's control requests have separate access
checks. The two verbs that act are reload, and set, which asks timed
to set the clock while it isn't being set automatically; see when reload
is refused.
It is called clock and not time because time is a shell keyword:
time status would run status and report how long it took.
Verbs #
| Command | Reads | Acts |
|---|---|---|
clock status | socket | — |
clock sources | socket | — |
clock reload | — | asks timed to re-read Machine\System\Time |
clock set TIME | — | asks timed to set the clock, while Automatic is 0 |
status is the default, so bare clock is clock status.
Status #
$ clock status
generation 118
time zone Europe/London
state synchronised
following 1.time.peios.org (stratum 3)
offset -412.0us
frequency -12.750 ppm
jitter +180.3us
accuracy within 31.4ms
root delay 28.2ms
root dispersion 4.10ms
sources 4 configured, 3 contributing
updates 118 (last 22s ago)
stepped +2.1d in total since start
floor 1788142329 (the build timestamp; the clock is never set below it)
time zone is the name timed has recorded for /etc/localtime. In the
inspected timed 0.1.10 source, restart identification
requires the saved name and zone data to match that file. If identification
fails, the command prints UTC (none chosen)
even though the file may contain another zone. A later successful apply
restores the reported name. After a copy failure, the retained name is not
a fresh verification of the file either. Check timed's log and the
time-zone procedure
before relying on an unexpected display. This is source inspection, not
installed-image validation.
state is the field to read first:
| State | Meaning |
|---|---|
synchronised | Normal. |
settling | Being steered, but the frequency estimate is still converging. Usual for the first few minutes after boot. |
spike | A large offset has appeared and is being timed to see whether it is real. The clock is deliberately untouched meanwhile. |
unsynchronised | Nothing is believed and the clock is free-running. With Automatic is 0: the clock is set by hand after it, nothing is being asked: the clock is set by hand. |
accuracy is timed's estimate of how wrong this machine's time might be, combining the reported uncertainties between here and the reference clock. Check it when investigating Kerberos clock skew. It depends on the source measurements and is not independent proof that the time is right.
frequency is what the crystal is doing, in parts per million, and it is
persistent — it is written to /var/state/timed/drift and read back at the
next boot. Tens of ppm is an ordinary machine. Approaching ±500 means the
hardware is at the edge of what the discipline can correct.
stepped can be non-zero after a boot on a machine whose clock was wrong. Later growth means there have been further steps; check whether these followed a manual set or re-enabling automatic time. Unexpected or repeated growth warrants investigation. The clock-step event reference explains how to tell manual, automatic and boot-floor steps apart.
floor is the build timestamp, the lower bound timed will use. If the clock remains there after boot, it may not yet have acquired real time. The floor does not establish synchronisation or guarantee a successful TLS handshake; inspect the source notes and see the bootstrap explanation.
Sources #
$ clock sources
source state auth str reach offset delay last
* 1.time.peios.org system-peer nts 2 377 -0.918ms 22.7ms 44s
+ 0.time.peios.org candidate nts 3 377 -1.204ms 14.2ms 31s
- 3.time.peios.org outlier nts 2 377 +8.221ms 61.0ms 58s
x 2.time.peios.org falseticker nts 2 377 +4.102s 18.1ms 12s
* system peer + candidate - outlier x falseticker ? unusable
| Mark | State | Meaning |
|---|---|---|
* | system-peer | Chosen. The machine takes its stratum and root figures from this one. |
+ | candidate | Agrees with the majority and contributes to the combined answer. |
- | outlier | Agrees, but too noisy to be worth including. |
x | falseticker | Disagrees with the majority. Go and look at this one. |
? | unusable | Answering, but saying it is not synchronised itself. |
| unreachable | Not answering. |
auth is nts or none. none means anyone on the path can forge that
source's replies.
reach is the last eight polls as an octal bitmask, newest in the low
bit — the classic NTP display. 377 is eight for eight; 376 means the
most recent poll was missed; 0 means nothing for eight polls, at which
point the source's stored measurements are discarded rather than left to
vote with stale numbers.
last is how long ago the source answered. It exceeding the poll interval by much is the first sign of trouble.
A source that is not contributing prints a note underneath saying why.
Setting the clock #
$ clock set 2026-10-04 14:05
the clock is set
$ clock set 2026-10-04T14:05:30
$ clock set @1791122700
The time is local, in the machine's time zone, to the minute or the
second, or @ and a number of seconds since 1970 in UTC. timed sets the
clock only while Machine\System\Time Automatic is 0; otherwise it keeps
the clock from its sources, the next poll would put it back, and it
refuses:
$ clock set 2026-10-04 14:05
clock: the clock is kept from its sources; set Automatic to 0 to set it by hand
Like reload, it needs the control right. Changing Automatic needs
registry write access separately. A manual set jumps the clock and changes
the basis of later timestamps. Check the zone and plan for that jump
before setting it, then inspect clock status; see the complete
procedure.
Exit statuses #
| Status | Meaning |
|---|---|
0 | Done. |
2 | Asked about something that is not there — no sources are configured. |
1 | It went wrong: timed is unreachable, or refused. |
64 | The command line was not understood. |
The separation of 2 from 1 is what lets a script tell "this machine has
no time sources" from "I could not find out".
When reload is refused #
$ clock reload
clock: not permitted
reload and set need the control right on timed's control object.
Reading does not: what time the machine thinks it is, and how well it knows, is not a
secret, and a program deciding whether the clock is trustworthy enough to
validate a certificate should not need a privilege to find out.
The descriptor is Machine\System\Time ControlSecurity; by default SYSTEM
and Administrators may control, and everybody may query. This is distinct
from write access to the registry key: a successful policy write does not
prove that the caller may reload or set the clock, and a refused reload
does not undo that write.
Session locales
Peios / Using Peios / Locale
Experimental includes org.gnu.glibc-bin: the locale and localedef tools
and compiled C.UTF-8 data. New user sessions default to LANG=C.UTF-8.
This gives UTF-8 character handling with the C locale's conventions. A
locale controls character classification, sorting, and number, money and
date formatting; it does not select a console keyboard layout or font,
which console keyboards and fonts
cover.
To choose a 12- or 24-hour display for your own top-bar clock, use Desktop Settings' Time Format.
Regional locales are precompiled in optional org.gnu.glibc-langpack-*
packages. For example, install org.gnu.glibc-langpack-en for British
English or org.gnu.glibc-langpack-de for German. Installing a pack does
not select it. org.gnu.glibc-locale-source contains source definitions
and is not required to use these packs; no locale-gen step is needed.
Machine and principal preferences #
The machine default is under Machine\System\Locale. An administrator
can change it after installing the corresponding pack, in System Settings
or with reg.
In System Settings, in the Language & Keyboard section, Language
lists every locale installed in full, by its language and place, and
Formats chooses how dates, numbers, money, measures and paper sizes are
written, if not Match Language. Each is written as it is
chosen: LANG, and LC_TIME, LC_NUMERIC, LC_MONETARY, LC_MEASUREMENT
and LC_PAPER for the formats. Anyone may look; changing it needs write access to
Machine\System\Locale, which as shipped only Administrators have.
A principal's own values, under Users\<SID>\Locale, override the
machine's. The principal may set them, and so may Administrators.
On the desktop, a person sets their own in My Settings (type my in
the launcher), in its Language & Formats section. The lists are the
same as System Settings', and the first entry in each, System Default
and Match Language, removes the person's own value. Each is written as it is chosen: LANG and the same five format
categories under CurrentUser\Locale, which is the person's
Users\<SID>\Locale.
Match Language removes the personal format overrides; machine category overrides can still apply. To use the same locale for your language and all five formats despite those machine overrides, select that locale explicitly in both lists. Confirm the result in a new session with the commands below.
From a shell, use reg. Use
lps show NAME to find a local principal's SID, or token user within
that principal's session, then substitute the SID after the principal has
logged in at least once:
Authd provisions a private Users\<SID> root on authenticated logon. The
principal, SYSTEM and Administrators have full access, inherited by child
keys. Creation and its descriptor are committed atomically. Existing roots
and their descriptors are left alone; failure to provision is reported but
does not deny login. A write needs access to the registry layer as well as
to the key, and the base layer lets Authenticated Users set values, so the
principal can write their own preferences there.
Values are REG_SZ locale names. Supported names are LANG, LC_CTYPE,
LC_NUMERIC, LC_TIME, LC_COLLATE, LC_MONETARY, LC_MESSAGES,
LC_PAPER, LC_NAME, LC_ADDRESS, LC_TELEPHONE, LC_MEASUREMENT and
LC_IDENTIFICATION. Each principal value overrides the same machine value.
A machine category override remains in force when a principal changes only
LANG; set that category explicitly to override it too. Categories without
an override use LANG.
LC_ALL, LANGUAGE and LOCPATH are not stored preferences. In particular,
persisting LC_ALL would hide every category choice. It remains useful for
a single command, for example LC_ALL=C sort names.
Malformed values, wrong types and locale data absent from the standard
system locale directory are reported and ignored, retaining the preceding
default for that value. Names cannot contain paths. Absent settings fall
back to C.UTF-8; C and POSIX are also valid choices. Minimal custom
images must include glibc-bin to make the UTF-8 fallback available.
When a change takes effect #
Console login, SSH session children and new GXWI compositor sessions read these preferences. Programs launched from them inherit the resulting environment. Existing sessions keep their environment, including a GXWI session rejoined after a browser disconnect. Sign out and start a new session to pick up a change. Applications may still choose their own locale.
Console login -p preserves other environment settings but refreshes locale
preferences and removes inherited LC_ALL and LANGUAGE. GXWI also clears
inherited locale overrides before applying preferences. For SSH, registry
preferences are defaults; explicitly permitted client environment entries,
user environment files or administrator SetEnv settings retain OpenSSH's
normal ability to override them for that session.
Service environments are not changed. These preferences are resolved by user-session launchers, not applied as peinit global environment variables.
Inspect a new session with:
What the machine records
Peios / Using Peios / Logs and events
Start with Event Viewer to inspect a machine's logs, events and metrics. eventd receives these records, stores them and answers queries about the stored history you may read. If the records you expect are missing, follow Find missing records before changing settings or removing stored data.
Three kinds of record #
eventd keeps three kinds of record, which arrive by different routes and answer different questions.
| Kind | What it is | Where it comes from |
|---|---|---|
| Logs | Lines of text that services and jobs write | The service manager, peinit, which captures stdout and stderr except for services attached to a terminal with TTYPath |
| Events | Structured records of something that happened, such as a service started or an access refused | The kernel and system programs |
| Metrics | Numbers measured over time, such as how many requests a server has answered | Programs that publish them |
Logs #
A log line is what a program wrote to its standard output or standard error, the two streams every program writes text to. Each line carries:
- where it came from, its origin: the service's name, such as
sshd. Output from a service's hooks and health checks carries the service's name and the hook's, such assshd/ExecStartPre[0]. A job someone started carriesjobs/and the job's ID. - which stream it was written to. Programs are meant to write errors to standard error, but many write everything there.
- when it was written, and which run of the service (its job) wrote it.
Log delivery to eventd is best effort: under load, downstream buffering and forwarding can drop lines. A service can still slow down when it writes output faster than peinit reads it, because writes to its output pipe can block. See Service output and logging.
Events #
An event is a record with named fields. Every event has a type, such
as peinit.job.started or kacs.audit.access.checked, and a
source, which says what recorded it:
| Source | What records it |
|---|---|
| Programs | System programs such as peinit, the service manager |
| Kernel | The kernel's event channel, KMES |
| Security | KACS, the kernel's access control |
| Registry | LCS, the kernel's registry |
Every event also carries the process and token that caused it, and the
boot it happened in. Its other fields depend on its type. A
kacs.audit.access.checked event, for example, says who asked, for
what, on what, and whether it was allowed.
Use events when you need the structured record of an operation or access decision. eventd stores the events it receives and records detected loss, but this is not a guarantee that every event survives: the kernel's event buffers are finite and can overwrite events before a consumer reads them. The kernel buffers do not persist across reboots; eventd provides stored history. Check loss reports as well as the events themselves when accounting for what happened. See Auditing and the kernel manual's event failure modes.
Metrics #
A metric is a named number that a program measures again and again, such
as how many requests a server has answered or how full a disk is. Each
measurement is a sample. A metric can have several series, one for
each set of its labels: eventd.store.bytes has a series for each of
eventd's stores, labelled store=logs, store=events and so on.
Every metric is one of three types:
- a counter only goes up, such as requests answered, so what matters is how fast it rises;
- a gauge goes up and down, such as bytes in use;
- a histogram is a spread of measurements, such as how long requests took, read by its percentiles.
A program publishes metrics only under names it has been allowed to.
eventd publishes its own health as metrics named eventd.*. Like logs,
metrics are best effort.
Who may read what #
Every query to eventd runs as the person who asked. eventd checks what they may read against its read policy, which grants reading by name: a log origin, an event type or a metric name.
What a person may not read is left out without comment. eventd never says that something was withheld, because saying so would itself tell them something. An empty answer therefore does not prove that nothing happened. Event Viewer also tries to read the policy and reports which names it permits, restricts, or cannot determine. A restriction notice does not prove that any hidden record exists; even unrestricted results cover only stored, queryable records. See what you may not see and the missing-record checks.
Where to look #
- On the desktop: Event Viewer shows events and logs, newest first, as they arrive, and charts metrics on dashboards of your own.
- In a terminal:
evctlruns a query in eventd's query language and prints what comes back. See Using evctl. - For incident review: Save incident event evidence keeps a bounded query result with its diagnostics, completion status and context.
Find missing records
Peios / Using Peios / Logs and events
An empty result does not establish that nothing happened. Work through these checks in order, keeping the observations as you go. Preserve relevant records before changing retention or cleaning up storage.
1. Check the service, time and filters #
- For a service, open Logs… from Services Manager and confirm the
service name and the incident time. Match the job and boot when
inspecting a record; a restart has a different job ID. Use
Find service logs
for the service-run workflow. A service using
TTYPathwrites to its terminal instead of having its output captured for eventd. - Events and Logs start with the last 24 hours. Choose a range that includes the incident, then use Show older to page back. any time removes the time restriction; it cannot bring back removed records.
- Check From, Containing and Errors only for logs, or Type and Source for events. Remove unintended restrictions and press Enter or Apply for typed filters. Standard error is a stream, not proof that a line describes an error.
For exact controls and paging limits, see Event Viewer.
2. Read the visibility notice #
Check the bottom-right notice described in What you may not see. It reports what the read policy allows for names, not whether matching hidden records exist. Some logs may be hidden from you means that the policy itself could not be read. Even an unrestricted policy cannot establish complete history: the data must still have been captured, retained and available to the query.
Record the notice. If the required names are restricted or the policy cannot be determined, ask an administrator to check the relevant policy and records; do not change permissions just to make an empty result disappear.
3. Distinguish a failed query from an empty one #
Keep any access refusal, timeout or storage error. A failed query does not establish the absence of matching records. A query timeout means the query did not finish, not that ingestion lost data. After recording the error, a narrower time range can help isolate the problem.
In a terminal, retain evctl's diagnostics on standard error as well as
its result. Its result-commitment rules
explain why an error before the initial result completes invalidates
that partial result. If live following stops, record the notice before
using Show the newest to resume; that action alone does not establish
what happened in the gap.
4. Check the retention settings #
Open Event Viewer's Settings, then Keeping records, and note the current values or defaults without changing them. Anyone may inspect these settings; changing them requires the separate settings permission, granted to administrators by default.
The documented defaults are 30 days for events, 14 for logs and 90 for metric samples. Events and logs have no size limit by default; metrics default to 1 GiB. These are retention limits, not a minimum guaranteed history. Both age and any enabled logical-live-size limit are enforced, so size pressure can remove younger records.
Event retention can remove a whole earlier boot, including its loss records. Logs and metrics instead lose their oldest entries or samples under size pressure; see log retention and metric retention. Present settings alone cannot establish which records were previously removed.
5. Look for loss or an unavailable store #
Look around the incident time for eventd.events.lost,
eventd.store.quarantined, and eventd's own log errors. If available,
its health metrics
also show recorded loss, write errors and retention deletions.
- A loss record establishes detected loss, not complete accounting. Power loss can destroy the final volatile event batch without a loss record. Logs and metrics have weaker durability, and full socket queues can refuse their data without a health count.
- A quarantined store is replaced by an empty one. The quarantined files may be the only copy of the earlier records. Query-time corruption instead fails the affected query.
- An unreadable or invalid historical shard can be logged and excluded from queries for that run without preventing startup. A running eventd therefore does not establish that every old store is being queried.
These observations narrow the cause; absence of a loss or quarantine record does not prove that the history is complete.
6. Preserve evidence before cleanup #
Keep the service, job and boot identifiers, incident time range, filters,
visibility notices, exact errors, and retention values. Use Copy on
relevant Event Viewer rows to keep all fields, or retain completed
evctl results together with their query and diagnostics. Follow
Save incident event evidence
for private output files, completion checks and a bounded capture.
Leave quarantined files intact and involve the administrator responsible for recovery. eventd does not automatically repair them. Do not delete, rename or edit database files as a diagnostic experiment, or retry a destructive operation before preserving the evidence it may remove.
Retention measures logical live size, not filesystem usage. Deleting
records frees pages for reuse; it does not immediately shrink the database
file, and eventd never automatically runs VACUUM. See
Reclamation
before planning space recovery; these checks are not a database-repair
or live-maintenance procedure.
Save incident event evidence
Peios / Using Peios / Logs and events
Use evctl to save a finite event query before changing configuration or
cleaning up storage. Keep the result, diagnostics and capture context
together so another authorized reviewer can tell what was queried and
whether it finished.
This procedure saves the records available to your query. It does not recover missing records or establish a complete incident history.
1. Choose a private destination #
Work in an existing, writable directory you control, whose native
security descriptor and new-file inheritance already restrict access to
the intended readers. Event payloads and diagnostics may contain sensitive
information. Check the destination policy before writing, using
Managing file security and
Inheritance. If the required
policy is unknown, have the responsible administrator check it first.
Do not rely on chmod or a shell umask to establish Peios-native privacy.
Choose new, unused filenames for both the result and diagnostics.
The shell opens redirection targets before evctl runs: reusing a name
can truncate previous evidence even if the query later fails. Do not
target existing files, symlinks or pipes.
Allow space for both the result and evctl's temporary spool. The temporary
directory selected by the process must also be suitable for sensitive data,
writable, have free space, and support anonymous temporary files
(O_TMPFILE). A writable output directory alone is not enough.
2. Record what you are capturing #
In your private incident notes, record:
- the host and the identity running the query;
- the incident time range and time zone, and when you start the capture;
- the exact query, output format, destination filenames and any nondefault query socket;
- the installed tool version, shown by
evctl version; - relevant service, job and boot identifiers, if known.
Choose the event names and time range for the incident. The example below
asks for at most 50 readable kacs.* events since one hour ago. It is a
bounded sample, not all events from that hour. A relative range also moves
when you rerun it, which makes the capture time important.
The query runs with the caller's access. Do not widen permissions to make the result appear complete. For query options and formats, see Using evctl.
3. Save the result and diagnostics separately #
After selecting the private working directory and confirming that both names are unused, run the complete quoted query. Save the exit status immediately, before running another command:
capture_status=
JSON Lines writes one JSON object per record. Standard error goes to the separate diagnostic file. Record the displayed status and the capture's finish time in the same notes, then inspect the diagnostics.
Leave STREAM out of a finite capture. Do not pipe the capture through
head or another consumer that can close early: evctl treats a broken
output pipe as success, so a zero pipeline status cannot prove that all
query output was saved. Review or filter the saved file afterward.
4. Decide whether the capture completed #
For the direct-to-regular-file command above, evctl reports:
| Status | Meaning | Next step |
|---|---|---|
0 | The finite query completed and output publication succeeded. | Keep the result with the diagnostics and context. |
2 | Command-line usage error. | Read the diagnostic, correct the invocation, and use new filenames. |
1 | Query, connection, protocol, input or output failure. | Keep the error and mark the capture failed; diagnose before retrying with new filenames. |
If the shell cannot open a redirection target, evctl may never run;
record that shell error too. Treat any interrupted or otherwise nonzero
attempt as failed.
evctl buffers initial records until the server completes the query. A
server error before completion prevents those records from being published.
However, copying the completed result to your file is not atomic: a
destination write or flush failure can leave a partial file. Do not treat
such a file as a completed query result, even if some lines look valid.
Keep it labelled as a failed attempt with its diagnostics.
An empty output file alone is ambiguous. It can accompany a failed query, or a successful query with no visible matching records. Always check status and diagnostics. If the capture failed, inspect both destination and temporary-filesystem errors; do not delete eventd databases to make room.
5. Keep the limits with the evidence #
Retain the exact query and its TAKE limit, times, identity, version,
status and diagnostics with the result. Read policy can omit records
without notice, and retention, collection loss or an unavailable store
can limit what remains queryable. Follow
Find missing records when
expected data is absent.
Keep the original capture private and unchanged while reviewing it. Check the contents and intended recipient before sharing; a result you may read is not automatically suitable for a wider audience.
Implementation reference #
The capture behavior above is backed by these paths in eventd commit
3df88bab845d962e866158896807eac2976b83e3. Check the installed version when
applying it to another build.
Event Viewer
Peios / Using Peios / Logs and events
Event Viewer shows what eventd has recorded on the machine: the events of its kernel and programs, the metrics programs publish, and the logs of its services. It shows events and logs newest first, adds new ones at the top as they are recorded, and shows any record in full. It charts metrics on dashboards you arrange.
It asks eventd as you, so results include only records you may read. It also tries to read the policy to explain restrictions on which names you may see; those notices do not establish that hidden records exist.
Opening it #
- From the launcher: start Event Viewer. It opens on the events.
- From Services Manager: select a service and choose Logs…, in the details pane or on the row's right-click menu. Event Viewer opens on that service's logs, including its hooks' and health checks'.
- From a program:
gxwi-event-vieweropens on the events,gxwi-event-viewer --metricson your dashboards,gxwi-event-viewer --settingson eventd's settings, andgxwi-event-viewer --logs NAMEon whatNAMElogged.
A window opened on one service or one type of event says so in its title, such as Event Viewer: sshd, and on the Metrics tab the title names the dashboard, so several windows can be told apart.
Events #
Events is the first tab. Each row is one event, with when it happened, its type and its source. Select one to see its fields.
To narrow them:
- Type takes an event type, such as
kacs.audit.access.checked. A*stands for any part of a type, sopeinit.job.*is every job event and*.rejectedevery refusal of that kind. It suggests the types that have been recorded. - Source shows only events from programs, the kernel, security (KACS) or the registry (LCS).
Metrics #
Select Metrics, between Events and Logs. On the left is every
metric you may read, with its type and latest value, or how many series it
has. A series is one of a metric's sets of labels: eventd.store.bytes
has a series for each store, for example. Hold the pointer over a metric
to see each series' latest value. Find a metric narrows the list as
you type.
On the right is a dashboard: a page of charts over one time range. Until you make your own, it is eventd's health, which charts how eventd itself is doing: what it stores, whether it is losing events, how full its stores are, and whether queries are being refused.
Charts #
Select a metric on the left to add a chart of it to the dashboard. A chart has a line for each series, a legend naming them, and the values up its left side. Hold the pointer over a chart to see every line's value at that moment.
A chart divides its range into windows, such as a minute each over the last hour, and shows one value in each. Select ⋯ on a chart to change what it shows:
- Show is what each value is. A counter, which only goes up, is shown as its rate per second at first, or as its change in each window, or as recorded. A gauge, which goes up and down, is shown as recorded. A histogram, a spread of measurements such as how long requests took, is shown as a percentile: the median, the 95th or the 99th.
- In each window, the picks the average, least, greatest or total of the values that fall in each window.
- Lines gives one for each series, or one for all of them together.
- Move earlier and Move later reorder the charts, and Remove takes one away.
A chart's right-click menu has the same choices.
Dashboards #
The list at the top left picks a dashboard. Beside it:
- Type in the name box and press Enter to rename the dashboard.
- The time range is 15 minutes, an hour, 6 hours, 24 hours or 7 days.
- New dashboard starts an empty one.
- Delete… deletes the dashboard shown, after asking.
Charts are read again every few seconds, more often over a shorter range. Refresh, or F5, reads everything again now.
Your dashboards are yours: they are kept in your part of the registry,
at CurrentUser\Software\gxwi-event-viewer, and other people have their
own. Delete your last one and eventd's health takes its place again.
Logs #
Select Logs, after Metrics. Each row is one line, with when it was written, where it came from and what it says. Lines written to standard error are shown in red.
To narrow them:
- From takes a service's name, and shows its lines and those of its hooks and health checks. It suggests the names that have logged.
- Containing shows only lines with that text in them, whatever the case of the letters.
- Errors only shows only lines written to standard error. Many programs write everything to standard error, so this narrows less than its name suggests.
The time range and applying a filter #
The Events and Logs tabs look back over the last 24 hours at first. The list beside the filters changes that, to 15 minutes, an hour, 7 days or any time. Searching for text and the suggestions cover the whole range, so a shorter range is quicker on a busy machine.
Typed filters apply when you press Enter or select Apply. Errors only, Source and the time range apply as soon as you change them, and so does anything typed and not yet applied.
Following new records #
While the Pause button shows a dot, new records appear at the top as eventd stores them, usually within a second. Select Pause to keep the list still while you read, and Resume to show the newest again and carry on following.
If following stops, because eventd restarted or the window fell too far behind a burst of records, Event Viewer says so above the list. Select Show the newest to start again.
Older records #
The list starts with the newest 200 records. Show older, at the bottom of the list, adds the 200 before them. When there are no more matching, readable records in the queryable stores for the time range, it says so. Even any time cannot retrieve records removed by retention or held in a store excluded from queries. See Find missing records.
A window holds at most 1,000 records. Past that, the newest are let go to make room and the window stops following. It says so at the top, with Show the newest to go back. Show older carries on as before.
A record in full #
Select a row to see all of it in the pane on the right: for a log line, its whole text, its stream, its job and its boot; for an event, every field, the standard ones first under plain names, with the field's own name beneath. Times are shown to the nanosecond, on this machine's clock.
Drag the pane's left edge to make it wider or narrower. The width is kept while the window is open. On a narrow screen the pane is put away.
- Show only… narrows the list to that record's source or type. A row's right-click menu does the same.
- Copy copies every field, one per line.
The Up and Down arrow keys move through the list, and F5 reads it again from the newest.
What you may not see #
eventd leaves out what you may not read without saying so. Event Viewer tries to read eventd's read policy as you and reports the permissions on names, at the bottom right of the window:
| It says | Which means |
|---|---|
| Nothing | You may read everything of that kind |
| Hidden from you: logs from sshd*, … | The policy keeps those names from you; the rest are shown |
| Only some events are yours to read | Only names the policy lists for you are shown |
| You can't read events on this machine | Nothing of that kind is yours to read |
| Some logs may be hidden from you | The policy itself could not be read, so Event Viewer cannot tell |
The Metrics tab says the same of metrics. By default everyone signed in may read every metric.
If eventd does not let you ask it anything at all, the list says so instead of showing nothing.
Settings #
Select Settings, the last tab, to see how eventd is set up and who may read what. Anyone may look. Only those allowed to change eventd's settings may change them, by default administrators; everyone else sees them read-only, with a line saying why.
eventd's settings #
The settings are in groups: Keeping records first, then taking in
events, logs and metrics, answering queries, and storage. Each one says
what it is, what it is set to or that it has its default, the values it
may take, and whether a change applies at once or when eventd next
starts. These descriptions are eventd's own, from its regman page, which
regman shows in a terminal.
- Type a value and press Enter or Save to change one. A value outside its range is refused, with the range given, and nothing is changed.
- Use the default takes a setting back to its default.
eventd notices a change by itself and records it as an
eventd.config.changed event, which the Events tab shows. The paths
of its stores and sockets are shown but not changed here: eventd needs
them to point at places made ready for it.
Who may read what #
eventd's read policy says who may read which events, logs and metrics. It is a list of patterns, each a name with the permissions for it:
*covers everything of its kind that no other pattern covers.- A name, such as
sshdfor logs orkacsfor events, covers that name and every name under it, such askacs.audit.access.checked.
The most specific pattern that matches decides. Each pattern says, in words, who may read it, who may only read some of its fields, and who may not. For metrics it also says who may publish them.
- Change… opens the pattern in the permissions editor. Changes apply from the next question anyone asks eventd.
- Add a pattern starts a new pattern for a name, with the permissions that apply to that name now, and opens it to be changed. Nothing changes until you change it there.
- Remove… takes a pattern away after asking. The broader pattern
above it then decides. The
*patterns cannot be removed, since eventd would let nobody read what they cover.
Changing eventd's indexes, at the end, says who may ask eventd to index a field.
For example, to keep sshd's logs from everyone but administrators, add a
pattern sshd under Logs and, in the editor, take Read away from
Authenticated Users. Anyone else then sees "Hidden from you: logs from
sshd*" on the Logs tab.
In a terminal #
evctl asks eventd the same questions in its query language. The window's
filters are its LOGS FROM, CONTAINING, ERROR ONLY, EVENTS <type>
and SINCE clauses, and a chart is a METRIC query such as
METRIC eventd.store.bytes[] SINCE 1h ago MAX_OVER 1m, so anything the
window shows, evctl can print. See
Using evctl.
The event manual (evman)
Peios / Using Peios / Logs and events
Every record in the event stream is a type name and a set of fields. The record says that something happened — kacs.audit.access.checked, with outcome.success false and an access.granted of 0x120089 — but not what any of it means. Whether access.granted is the rights the check granted or the rights a handle was opened with, whether a missing trigger.ace is a bug or expected, which of two masks you are looking at: that knowledge lives with the software that writes the records. It lives in evman, the event stream's manual.
evman is to events what regman is to the registry. Give it an event type or a field and it tells you what that is. Reach for it whenever a record does not say enough on its own — before writing a query, a filter or a security descriptor over a field, and whenever a value surprises you.
evman, in one sentence #
evman <name> documents what an event type or field means — when it fires, what each field records, which values it takes — drawn from shipped documentation, never from the live event stream.
Looking up an event type #
Give evman an event type and it prints the event's card: how important it is, what decides whether it is recorded, a description, and every field it carries.
$ evman kacs.audit.access.checked
kacs.audit.access.checked defined by kacs
Tier essential
Gating a matching SACL audit ACE, or the token's audit policy
Cardinality once per matching audit ACE, not once per access
The record that an access check completed, and what it decided. The most
common event on the system, and the one most investigations start from.
...
Fields
subject group, from kernel
subject.token.sid
subject.token.groups
...
object.kind required
object.file.path when object.kind == file
access.requested required
access.granted required
The mask this check granted.
...
| Fact | Tells you |
|---|---|
| Tier | How much the event matters, and whether an administrator can switch it off: essential events are always recorded; standard ones are on by default; verbose and debug ones are off unless switched on. |
| Gating | What decides which occurrences produce a record at all. An event gated on a SACL is silent for an object with no audit ACE, so its absence proves nothing. |
| Cardinality | How many records one occurrence produces, where that is not one. |
Each field line says when the field is present — always, optionally, or only when another field has a given value — and an indented note says what the field means in this event, where that is narrower than its general meaning. Read those notes: the same field can mean subtly different things in different events, and the note is where that is written down.
Looking up a field #
Give evman a field and it prints the field's card: its type, the values it can take, a description, and every event type that carries it.
$ evman subject.token.type
subject.token.type defined by kernel
Type str.enum
Values primary | impersonation
Closed true
Whether the effective token is a primary token or an impersonation token.
Needed beside subject.token.impersonation, because a primary token and an
Anonymous impersonation token both report level 0 there.
Carried by
kacs.audit.access.checked (via group subject)
kacs.audit.handle.used (via group subject)
lcs.audit.key.opened (via group caller)
...
Carried by is the reverse index: the event types whose records can hold this field. It is the list to check before you rely on a field across the whole stream — a query on subject.token.sid finds nothing in an event type that does not carry it.
Closed says whether the list of values is complete. A closed list never grows without the field changing; an open one may gain values, so anything you write over it should cope with a value it has not seen.
Some fields are carried in the record header rather than the payload — the time, the sequence number and who wrote the record. Their cards say so: they are on every event, and no payload repeats them.
Looking up a subtree #
Field names and event types are both dotted, general to specific. Give evman a prefix and it lists what lies beneath it, one summary line each:
$ evman kacs.audit
Event types
kacs.audit.access.checked The record that an access check completed, and…
kacs.audit.handle.used The record of what was done with a handle afte…
kacs.audit.privilege.used The record that a privilege contributed access…
$ evman subject.token
Fields
subject.token.auth-id The LUID of the logon session the effec…
subject.token.groups The group SIDs carried by the effective…
subject.token.integrity The integrity RID of the effective toke…
...
So you can start from a subsystem, or from a part of a record such as "who acted", and drill into any one entry.
A few names are not defined one by one. Any field can have a variant with a suffix such as -previous or -requested — policy.generation-previous is the generation that was in force before — and any thing can have a standard attribute such as count or size. evman resolves these to the field or thing they qualify and says which rule it applied.
Searching, when you do not know the name #
evman -k searches names and summaries — the event stream's apropos. Every term must match.
$ evman -k swap
buffer.capacity-previous The per-CPU ring capacity before a capacity swap…
buffer.generation-previous The ring generation before a capacity swap, nor…
kmes.buffer.swap.failed The record that KMES could not resize its per-C…
evman documents meaning, not what happened #
evman tells you what an event or field means — never which events have occurred on this machine. It reads shipped documentation and never touches the event stream or the event store. Ask it about kacs.audit.access.checked and you learn when the event fires and what its fields hold; you do not learn that anything was denied today. That is a query against the event store, which is the Event Viewer's job, or eventd's query interface.
| To learn… | Look at… |
|---|---|
| What an event or field means | evman — the manual |
| Which events have happened | the Event Viewer, or a query against eventd |
| Whether an event type is recorded at all | its tier, and the emission policy under Machine\Generic\Events — see regman |
Where the documentation comes from #
evman has no built-in list of events. Each package ships documentation for the events it writes and the fields it defines, as files in /usr/share/evman/. Install a package and its events appear in the manual; remove it and they go.
The manual is also the definition. Every field any Peios component writes is defined in exactly one of these files, by the component that owns it, and a field defined nowhere is not a field anything should emit. That is what keeps one value from acquiring two names: a security descriptor that denies a field must not be evadable by writing the same value under another name. The rules are in The Catalogue.
(For the people who write these files, evman lint checks them against those rules. That is a packaging concern rather than an everyday one; see Writing evman pages.)
Where to go next #
- Logs and events — what the event stream is, and how events differ from logs.
regman— the same idea for the registry, including the emission-policy keys that switch event types on and off.
Sign in from a browser
Peios / Using Peios / Signing In from a Browser
The Peios desktop runs on the Peios machine and is displayed in your browser. To open it, you need the machine's address, the port it is configured to listen on, and an account on that machine.
If you have just installed Peios with the graphical or text installer, complete First-boot setup first. On a live installation medium, follow Install on a PC instead: the installer and its temporary account are different from a normal sign-in.
1. Open the machine's address #
Open https://<machine-address>:7780, replacing <machine-address>
with the machine's address or name. Use the configured port if an
administrator changed it.
The desktop service, GXWI, serves HTTPS only. An HTTP request is redirected to HTTPS; it does not serve an unencrypted desktop.
2. Verify the certificate before entering credentials #
Each machine creates its own certificate. A browser does not initially know whether to trust it, so a warning on first use is expected.
Follow The machine's certificate to compare the browser's SHA-256 fingerprint with the fingerprint shown at the machine's own console. Do not use a page received over the connection you are trying to verify as the source of that fingerprint. If the fingerprints differ, do not sign in.
That guide also explains importing the certificate, name mismatches, renewal and why a live medium presents a new certificate after each boot.
3. Sign in and choose your task #
Sign in with an account on this machine. The desktop and its tools use that account's authority; machine-wide changes may require an administrator account or specifically delegated access.
- Desktop Settings changes your wallpaper, clock, dock, shortcuts and default applications.
- Manage the machine points to software, services, accounts, networking and storage tasks.
- Troubleshoot a problem covers reachability, refused sign-in and missing records.
If the machine shows an installation or setup overlay instead of a sign-in screen, follow that flow's guide. While an overlay is configured, normal sign-in and desktops are unavailable.
GXWI replaces Atrium #
GXWI completely replaces Atrium. Use the HTTPS connection and certificate verification steps above; the old Atrium connection and application instructions are retired.
The machine's certificate
Peios / Using Peios / Signing In from a Browser
The desktop is reached in a browser at https:// and the machine's address,
port 7780 unless an administrator has chosen another. GXWI serves it over
HTTPS alone, with a certificate the machine made for itself the first time GXWI
started. No certificate authority has vouched for it, so a browser warns the
first time it is opened, until it is told to trust it.
Opening http:// instead is answered with a redirect to the same address under
https://, and with nothing else: no page is ever served without encryption.
Check the fingerprint before going past the warning #
The warning is the browser saying it cannot tell this certificate from one someone between you and the machine made up. You can: every certificate has a SHA-256 fingerprint, and the machine shows you its own.
The managed console login banner shows the fingerprint before sign-in when GXWI has made its certificate. Read it on the machine's own trusted console. If the banner says it is unavailable, or to read the current fingerprint after renewal, sign in locally and print the public file, which any account may read:
$ cat /var/state/gxwi/certificate.sha256
B8:39:98:AB:E7:F6:64:25:BC:0D:DE:56:F4:78:2E:21:3D:D8:AB:FF:EF:78:8A:81:6E:94:99:17:94:83:73:C3
Read it there, and not from a page in the browser: a page that came over the connection you are checking would show whatever someone in the middle of it wanted. Once you trust the connection, a terminal on the desktop reads the same file.
In the browser, open the warning's details and view the certificate. Its SHA-256 fingerprint is the same 32 pairs of hex digits, though some browsers separate them with spaces rather than colons. If the two match, the connection is to this machine and nobody can read it on the way. If they do not, do not sign in.
Firefox may still describe a connection with a certificate exception as insecure, even when it uses TLS. That label alone does not establish whether the traffic is encrypted or whether the peer is the right machine. Check the presented certificate against the console fingerprint as above. Importing the verified certificate establishes trust in it; name and validity checks still apply.
Trust the verified certificate #
The certificate is in /var/state/gxwi/certificate.pem, also readable by
anyone. After checking its fingerprint through the trusted console, copy it to
the device you browse from and add it to that browser's or system's trusted
certificates. This establishes trust in that certificate; it does not remove
name-mismatch, expiry or other certificate warnings.
The reviewed certificate generator
includes the machine name at creation, localhost, and the loopback addresses
127.0.0.1 and ::1. It does not include the machine's other network addresses.
An existing certificate may have been generated by an earlier version or under
an earlier machine name: inspect the certificate actually presented. Trusting
it does not make an address or name absent from that certificate valid.
An imported certificate lasts until the machine renews it, once a year (see Renewal): import the new one then.
A certificate only gone past at the warning, not imported, also costs the waiting page: GXWI keeps a page in the browser that it shows while the machine is restarting or cannot be reached, and a browser keeps such a page only for a certificate it trusts. Without it, a tab left open while the machine restarts shows the browser's own error, and loads again by hand.
No HSTS #
GXWI deliberately sends no Strict-Transport-Security header. HSTS tells a
browser to refuse any certificate warning for the site from then on, which with
a certificate of the machine's own making would leave nobody a way past the
first warning, including after the machine's key changes.
Where the key is kept #
| File | What it is | Who can read it |
|---|---|---|
/var/state/gxwi/key.pem | The private key, ECDSA P-256 | SYSTEM alone |
/var/state/gxwi/certificate.pem | The certificate | Everyone |
/var/state/gxwi/certificate.sha256 | Its SHA-256 fingerprint | Everyone |
GXWI makes the key once and keeps it. It never replaces a key it cannot read: it stops, and says so. Move the files aside to have a new key made; every browser that trusted the old certificate then warns again.
Renewal #
A certificate lasts a year, from the day before it was made. GXWI renews it
once fewer than 30 days of it remain, for the same key: the machine is the same
machine, but the certificate, and so its fingerprint, is new. Each renewal
means one more warning in every browser that went past the old one, and
importing the new certificate.pem again for anyone who imported the old.
Check the new fingerprint as you did the first.
Renewal uses the machine's current hostname for the certificate's common
name and DNS name, alongside localhost, 127.0.0.1 and ::1. A machine
rename is therefore picked up at the next renewal; renaming alone does
not renew a certificate that is still within its normal lifetime.
GXWI looks when it starts, once a day while it runs, and when it is reloaded:
$ svctl reload gxwid
Nothing restarts, and nobody signed in is signed out. Connections already open
carry on; the next one the browser makes is shown the new certificate. A
renewal is recorded as the event
gxwid.certificate.renewed,
which carries the old and new fingerprints.
Installation and first boot #
On a live medium /var/state is in memory, so every boot makes a new key and
the warning comes back each time. installerd attempts to carry that key and
certificate into the installed system, whether you use its console or browser
surface. In the reviewed installer,
an absent key skips the transfer and a copy failure is logged without failing
the installation. The installation continues,
so its completion message does not prove certificate continuity. A failed copy
can leave partial state; it does not guarantee a clean new identity or working
HTTPS on the next boot.
If the installed system presents the same verified certificate, existing trust may continue subject to the browser and address checks above. If its fingerprint changes, verify it again through the installed machine's trusted console before entering credentials. If you cannot do that during first boot, complete setup on the console first, then verify before using browser sign-in. Follow First-boot setup.
The older peios-install script is a separate path: the installed system makes
its own key when GXWI first starts. Do not infer its certificate behavior from
installerd's transfer attempt.
Desktop Settings
Peios / Using Peios / Desktop Settings
Open Desktop Settings from the launcher (type desktop) to change
your wallpaper, clock, dock, keyboard shortcuts and default apps. Use
For You for your own choices and All Users for the machine's
defaults.
Settings that belong to the machine rather than the desktop live in System Settings, and a person's own account settings, such as their password and language, in My Settings.
For you, and for all users #
The window has two groups of sections.
| Group | Sections | Kept in |
|---|---|---|
| For You | Wallpaper, Clock, Dock, Keyboard Shortcuts, Default Apps | Your own part of the registry, under CurrentUser\ |
| All Users | Sign-In Screen, Desktop, Keyboard Shortcuts, Default Apps, Advanced | The machine's, under Machine\ |
Anyone may change their own settings. The machine's are, as shipped, for Administrators to change; anyone else sees them read-only, with the reason said once at the top of the section.
How the choices stack #
Most settings exist at three levels, and the nearest one that is set wins:
- Your choice, if you have made one.
- The machine's, set under All Users, for everyone who hasn't chosen.
- The built-in default, what Peios does when nobody has chosen.
Desktop Settings calls the second and third together the System Default: choosing System Default for yourself removes your choice, so the machine's (or the built-in) applies to you again.
When changes take effect #
For your personal settings, a switch or a choice from a list applies the moment it is made; a typed value applies with the Apply button that appears beside it once it has changed. What a change did is said along the bottom of the window.
Some settings for all users take effect later: sign-in-screen settings and a new listen address apply when GXWI next starts, and icon themes apply to new desktop sessions. Changes that could cut people off, such as the address GXWI listens on, ask first, under their row.
Where to start #
Desktop components #
For troubleshooting or advanced configuration, the desktop consists of GXWI, which serves it; Fenestra, which arranges the windows; and fenesh, which provides the top bar, dock and launcher. Desktop Settings ships with fenesh. The settings that select those components are under Advanced.
Wallpaper, clock and dock
Peios / Using Peios / Desktop Settings
These are the For You sections of Desktop Settings. Each applies to you alone, and only once you have chosen: until then you have the machine's choice, and failing that the built-in one.
Wallpaper #
Wallpaper shows the pictures you can choose from:
- System Default, the machine's wallpaper, or the built-in one when the machine has none;
- the wallpapers Peios ships, kept in
/usr/share/wallpapers/peios, in four series: Contours (Strata, the built-in one, Fold, Chalk and Relief), Afterglow (Cast Glass, Veil and Amber), Elsewhere (Escarpment, Canopy, Mare and Saltpan) and Impressions (Upland and Winter Ridge); - your own picture, once you have chosen one.
Click one to use it. Choose Picture… opens the file dialog for a picture of your own: a PNG, JPEG, SVG, WebP, AVIF or GIF of up to 16 MB, that you can read. The desktop changes at once, on every screen you are signed in on.
The choice is the picture's path, CurrentUser\Software\fenesh Wallpaper
(REG_SZ). Deleting the value is System Default. A path that can't be
read, isn't a picture or is too large is ignored and the next level down
is used.
Clock #
Time Format sets how the clock in the top bar counts the hours:
24-Hour (14:05), 12-Hour (2:05 PM), or System Default, which is
the machine's choice, else 24-hour. It is CurrentUser\Software\fenesh
ClockHours, a REG_DWORD of 12 or 24; any other number is ignored.
The clock shows the time in the machine's time zone, which is set in System Settings; see the time zone.
Dock #
The apps pinned to your dock are changed from the dock and the launcher themselves: right-click an app to pin or unpin it, and drag a pin to move it. Your first change makes your own set; until then the dock shows the machine's pins.
Dock shows whose pins you have. Use System Default throws your own
set away, so the machine's applies to you again. Your set is
CurrentUser\Software\fenesh Pinned, a REG_MULTI_SZ of application ids
in order.
Get Started, under Show Again, opens the welcome you saw on your first sign-in, and shows it again at your next one if it can't open now.
Where to go next #
- Keyboard shortcuts
- Settings for all users, where an administrator sets the machine's wallpaper, clock and pins
Keyboard shortcuts
Peios / Using Peios / Desktop Settings
The desktop's keyboard shortcuts work wherever the keyboard is, in any window. Each action, such as moving to the next workspace, can have several shortcuts, or none.
Changing a shortcut #
In Desktop Settings, Keyboard Shortcuts under For You lists every action, grouped into Launcher & Dock and Windows & Workspaces, with its shortcuts as keys.
- + Add, then press the keys you want. Holding Ctrl or Alt on its own is waited past; press Esc, or click elsewhere, to stop without changing anything.
- × on a shortcut removes it. An action with none says None.
- Reset puts one action back to the System Default; Reset All to System Default puts them all back.
A shortcut must include Ctrl, Alt or Meta, since the desktop takes it from every window, a text field included, and a key on its own is met while typing. The browser keeps a few shortcuts for itself, such as Ctrl+W, Ctrl+T and F11, and those can't be used. A shortcut already assigned to another action is refused, with which; remove it there first.
Open Dock Item is set by modifiers, not a whole shortcut: Alt means
Alt+1 to Alt+9, one for each of the first nine items in the dock. To change
it, press Change, then hold the new modifiers and press any number.
Changes apply at once, in every window of yours.
The actions #
| Action | Id | Built-in default |
|---|---|---|
| Toggle Launcher | Launcher | Ctrl+Space, Alt+P |
| Open Dock Item | DockItem | Alt (with 1 to 9) |
| Focus Previous Tile | PreviousTile | Alt+J |
| Focus Next Tile | NextTile | Alt+K |
| Previous Workspace | PreviousWorkspace | Ctrl+Alt+←, Ctrl+Alt+J |
| Next Workspace | NextWorkspace | Ctrl+Alt+→, Ctrl+Alt+K |
| Move Window to Previous Workspace | MoveToPreviousWorkspace | Ctrl+Alt+Shift+←, Ctrl+Alt+Shift+J |
| Move Window to Next Workspace | MoveToNextWorkspace | Ctrl+Alt+Shift+→, Ctrl+Alt+Shift+K |
Where they are kept #
Each action's shortcuts are one REG_SZ value, named by its id, in:
| Key | Whose |
|---|---|
CurrentUser\Software\Fenestra\Shortcuts | Yours |
Machine\Software\Fenestra\Shortcuts | The machine's, for everyone who hasn't chosen |
Your value is used if there is one, else the machine's, else the built-in
default. The data is the shortcuts separated by spaces, each written as
its keys joined by +, for example Ctrl+Alt+ArrowLeft Ctrl+Alt+J.
Open Dock Item's is its modifiers alone, such as Alt or Ctrl+Alt.
An empty value is an action with no shortcut, which is different from no
value, which leaves the next level in force. A value that isn't a
REG_SZ, or that holds anything that isn't a shortcut the desktop may
take, is ignored as a whole and the next level is used; nothing repairs
it.
$ reg set 'CurrentUser\Software\Fenestra\Shortcuts' Launcher 'sz:Ctrl+Space'
Where to go next #
- Default apps
- Settings for all users, for the machine's shortcuts
Default apps
Peios / Using Peios / Desktop Settings
When more than one installed app can open a type of file, the default app is the one that opens it when you open the file, for example by double-clicking it in Gexora. The others stay on the file's right-click menu.
Choosing one #
In Desktop Settings, Default Apps under For You lists each type of file that more than one app can open, by its name and its media type, with the apps that can open it. Choose one, or System Default for the machine's choice. It applies at once.
A type only one app opens isn't listed: there is nothing to choose. When no app can be chosen for anything yet, the section says No Choices.
How the default is found #
Choices are made for a whole media type, such as image/png, or for a
family of them, such as image/*. For a file, the first of these that
names an app that can still open it wins:
- your choice for its exact type;
- your choice for its family;
- the machine's choice for its exact type;
- the machine's choice for its family.
So your choice for every image beats the machine's choice for PNGs. When none of them applies, the app that opens the file is the first available. A choice never makes an app open a type it doesn't say it opens, and a choice naming an app that has since been removed is simply passed over.
Where they are kept #
Each choice is a REG_SZ value in CurrentUser\Generic\Applications\Defaults
(yours) or Machine\Generic\Applications\Defaults (the machine's), named
by the media type and holding the app's id:
$ reg set 'CurrentUser\Generic\Applications\Defaults' image/png sz:dev.peios.peitor
The whole rule, for anyone writing a program that opens files, is PGSS Defaults.
Where to go next #
Settings for all users
Peios / Using Peios / Desktop Settings
The All Users sections of Desktop Settings set the machine's choices: what everyone has until they choose their own, and the parts of the desktop that are the machine's alone. Changing them needs write access to the keys below, which as shipped only Administrators have. Anyone else sees them read-only, with the reason said once.
Sign-In Screen #
- Machine Name is the name the sign-in screen shows, up to 64 characters. It is separate from the machine's network name, which is System Settings'. Empty is the System Default.
- Notice is text shown under it, such as an acceptable-use notice: Change… opens it, up to eight paragraphs separated by blank lines and 2,000 characters in all.
Both apply when GXWI next starts. They are Machine\Software\GXWI\Logon
Name (REG_SZ) and Notice (REG_MULTI_SZ, a paragraph each).
Desktop #
- Wallpaper chooses everyone's from the same pictures as your own.
- Time Format is everyone's clock, 24-hour unless chosen.
- Pinned Apps shows everyone's dock; Use My Pins makes the dock you have now everyone's.
These are Wallpaper, ClockHours and Pinned in Machine\Software\fenesh,
the same values as a person's own (see wallpaper, clock and
dock).
- Icon Themes are the icon themes desktop apps use, in order of
preference, each a folder in
/usr/share/icons; the base theme is always used last. They areMachine\Generic\IconsThemes(REG_MULTI_SZ), and apply to new desktop sessions.
Keyboard Shortcuts and Default Apps #
These work as the personal sections do, but set the machine's choices:
Machine\Software\Fenestra\Shortcuts and
Machine\Generic\Applications\Defaults. Reset All to Built-In removes
the machine's shortcuts, and First Available removes the machine's
default for a type. See keyboard shortcuts
and default apps.
Advanced #
The programs that make up the desktop, and how GXWI serves it. Leave a field empty for the System Default.
| Setting | Is | Kept in |
|---|---|---|
| Listen Address | The address and port GXWI serves on. The default, 127.0.0.1:7780, is reachable only from this machine. | Machine\Software\GXWI Listen |
| Compositor | The program each desktop session runs, as its user. Default /usr/bin/fenestra. | Machine\Software\GXWI Compositor |
| Shell | The programs that provide the panels, dock and launcher, separated by spaces. Default /usr/bin/fenesh. | Machine\Software\Fenestra Shell |
| Overlay | A program shown to everyone in place of the sign-in screen, such as an installer. | Machine\Software\GXWI OverlaySession |
| Overlay Account | The account the overlay runs as. | Machine\Software\GXWI OverlayUsername |
Each of these is trusted with every person's desktop, so Review Changes… shows what changes and what it means before Apply Changes. A new listen address takes effect when GXWI next starts, and pages open on the old one, including Desktop Settings, lose the desktop. While an overlay is set, nobody can sign in or reach a desktop.
Where to go next #
Inspecting security state
Peios / Using Peios / Inspecting security state
Inspection answers a specific question about the running system: who a process acts as, which session it belongs to, how it is protected, or what policy covers an object. Start with the supported command or desktop view for that question, and keep any access refusal or incomplete-view notice with the result.
For a failed operation, start with Debugging a denial. It puts the caller, object and requested action together before interpreting security state. A token dump alone does not tell you whether access is allowed.
What you can inspect #
These are read-only ways to begin. Replace PID, TID and ID with targets you
have identified, and the example paths with the actual objects.
| Question | Supported view | Interpretation limit |
|---|---|---|
| Which process is involved? | Task Manager, Processes | Some command, owner and protected-process details may be hidden. |
| Who does a process act as, and what is on its token? | token show --pid PID --all | This selects the primary token; a thread may be impersonating. |
| What identity is a thread impersonating? | token show --pid PID --tid TID --all | Identify the failing thread and record the time; it may have reverted since the failure. |
| What is this process's protection? | logonse psb --pid PID | The PSB reports PIP, mitigations and process GUID, not its security descriptor. |
| Which logon sessions and processes are visible? | logonse list, logonse show ID, or Task Manager's Signed in | The session list and the visible-process list have different access limits. |
| What rules protect a file? | sd show ./report.txt --all | Queries owner, group, DACL and the label subset, not the full SACL. --all adds formatting detail, not audit ACEs or a complete export. |
| What rules protect a registry key? | reg sd Machine/App | The default view is owner, group and DACL; SACL reading needs separate authority. |
| What was recorded about an earlier attempt? | Event Viewer or evctl | Audit policy, reader permissions, query scope, retention and transport all limit the answer. |
For files, sd check can explain a simulated access check without performing the
file operation. Read the rehearsal's limits
before treating its result as an explanation of an application's failure.
Who can inspect what #
The tool does not add authority. The underlying surface determines which checks apply:
- Another process's token:
PROCESS_QUERY_INFORMATIONon the process, PIP dominance, andTOKEN_QUERYon the token's own descriptor. Administrator membership does not bypass PIP or a changed token descriptor. - Another process's PSB:
PROCESS_QUERY_LIMITEDon the process descriptor; PIP dominance is not required for this view. Seeing its protection does not grant access to its token or memory. - Another process's descriptor:
READ_CONTROLplus PIP dominance for owner, group and DACL inspection. SACL inspection additionally needsACCESS_SYSTEM_SECURITY. See Inspecting processes. - The kernel's full session listing: Administrators or SYSTEM, through the
descriptor on
/sys/kernel/security/kacs/sessions.logonsereports a refusal and can fall back to the sessions of processes you may inspect. Even an administrator's process list can omit protected processes.
Self-access is surface-specific. The Kernel TRM documents a process opening its
own /proc token paths without the cross-process or token-descriptor checks.
Opening its own token through the API still checks the token descriptor; a
changed descriptor can revoke that access. Do not turn the first rule into a
promise that every self-query succeeds. See
Token access rights.
A refused inspection is a result in its own right. Keep the target, operation and error; ask an authorised operator or the component owner for the missing evidence when needed. Do not change permissions, disable protection or adjust a token just to obtain a fuller report.
What you cannot inspect #
These views have boundaries even when they return successfully:
- No complete history. They report current state. A destroyed token cannot be recovered from a live query, and an earlier impersonation or privilege state may have changed. Use recorded evidence for historical questions.
- No atomic machine snapshot. Processes and threads start and exit during a listing. Separate token queries can observe adjustments between reads; keep the token ID and modification counter where available.
- No global inventory of every token. Process-based inspection cannot list tokens held only by file descriptors with no running process using them.
- No implied access verdict. A token, PSB or descriptor report supplies inputs, not the outcome of every possible operation.
- No inferred absence from a partial view. Hidden processes, unreadable descriptor components and missing audit records remain unknown. A source or syscall error is not a successful empty result.
For registry output, use the documented result distinctions: reg exit 2
means a key/value was not found, 3 is access denied, and 5 is another syscall
or source error. An empty value or listing alone is not one of those diagnoses.
See reg exit status.
Standard query patterns #
Follow a process to its session. Find the process in Task Manager, inspect
its primary token, and use the reported logon-session ID with logonse show ID.
token show labels this session_id; in the underlying statistics it is
auth_id. It is distinct from interactivity_scope. A service's impersonated
request may belong to a different session, so retain which token you inspected.
Explain why a protected process has limited details. Read its Protection
field in Task Manager or its PSB with logonse psb --pid PID. A visible PSB and
a refused token query can both be correct under the separate access rules.
Investigate a privilege-related refusal. Use token privs --pid PID for the
primary token, or the documented thread selector when impersonation is involved.
Separate present from enabled; neither proves the application supplied any
required intent or that another policy allowed the operation. Continue with
Debugging a denial.
Two ways to read a token #
For an operator, token opens and queries
the token, then renders readable output. Inspecting tokens
explains how to select the target and interpret the fields.
For a tool author, token file descriptors and KACS_IOC_QUERY expose structured
state. /proc/<pid>/token selects the process's primary token;
/proc/<pid>/task/<tid>/token selects the thread's effective token. These and
/sys/kernel/security/kacs/self provide handles, not text to print with cat.
The SDK token-opening and
query references document the programming interface;
the Kernel TRM ABI notes
cover the raw query protocol.
Other pseudo-files have different formats: the documented session listing and
/proc/<pid>/psb are text. The existence of one text surface does not make token
handles text, or make every surface follow the same access rules.
Where to start #
- Inspecting tokens: choose a token and interpret its identity, groups, privileges and restrictions.
- logonse and the session-listing reference: use the command for session reports; consult the reference for the underlying format.
- Task Manager and the process-inspection reference: use Task Manager for process views; consult the reference for the lower-level surfaces.
- Debugging a denial: combine these observations with the failed action and object policy.
- The event stream: raw transport diagnosis when events are not reaching eventd. For ordinary recorded-event inspection, use Event Viewer or evctl instead.
Debugging a denial
Peios / Using Peios / Inspecting security state
Start with the operation that failed, then gather the evidence you are allowed to read. The aim is to explain the refusal and decide whether the caller or the policy needs review. A refusal can be the intended protection working.
Use the inspection commands below before changing permissions, privileges or labels. An empty registry result or a missing process detail is not, by itself, evidence that a particular access-check layer denied the original operation.
Record the failed operation #
Keep these together:
- What failed and when: the application's message, error or exit status, timestamp, and the action you asked it to perform.
- The caller: the application or service, its PID, and the failing thread's TID if the application's diagnostics identify it. A service acting for a client may be using an impersonation token rather than its primary token.
- The object: the exact file path, registry key, process or other resource. For a file, establish whether the operation targeted a symbolic link or its target. For a registry operation, keep the key and value name separate.
- The access requested: read, write, execute, change permissions, or another specific operation. Keep the requested access mask when the diagnostic or audit record supplies it; an application may ask for more than its visible action suggests.
- The point of failure: opening an object, using an already-open handle, or a service's own authorisation check. These are different questions.
Access is normally decided when a handle is opened. Later operations use the rights stored on that handle; inspecting today's descriptor does not reconstruct what an older handle received. See The decision is made at open.
Find the thread and its tokens #
Use Task Manager to find the process by name, PID, person or service. Check its command, owner, service or job, and signed-in session where visible. Read any notice about hidden details or protection before interpreting the view.
In a terminal, use the read-only subcommands of
token. Replace PID and TID with the
identified process and thread:
token show --pid PID --all
token privs --pid PID
token show --pid PID --tid TID --all
--pid selects the process's primary token. The command's --tid selector
is for a thread's impersonation token and is used with --pid; use it when
investigating impersonation. The identity that matters is the one the failing
thread used: its impersonation token while impersonating, otherwise its primary
token. If that identity is no longer available, record the gap rather than
substituting the inspection tool's own token. Running token show --all without
a target shows the tool's own token, not another application's.
The /proc/<pid>/token, /proc/<pid>/task/<tid>/token and
/sys/kernel/security/kacs/self surfaces provide token handles, not text to
read with cat. The process path identifies the primary token; the thread path
identifies the effective token. The command does the querying and decoding.
Inspecting tokens documents those interfaces for tool
authors.
Record the user SID, group attributes, token type and impersonation level, integrity and mandatory policy, present and enabled privileges, restricted SIDs, and confinement identity and capabilities where available. Keep token and session IDs with the time of inspection: the thread can revert impersonation, and token adjustments can change the state after the failure.
For process protection and session context, use
logonse:
logonse psb --pid PID
logonse list
logonse show ID
Here ID is the logon-session ID you have identified. token show calls it
session_id; it is separate from interactivity_scope. A PSB report shows
protection, mitigations and the process GUID; it is not the process's security
descriptor.
Inspection has its own access checks. Reading another process's token needs
PROCESS_QUERY_INFORMATION, PIP dominance, and TOKEN_QUERY on the token.
Reading its PSB needs PROCESS_QUERY_LIMITED and does not require PIP dominance.
The full kernel session list is restricted to Administrators and SYSTEM;
logonse can show a partial process-based view instead. A refused query or an
absent process in that view is a limit on your evidence, not a reason to remove
protection. See Who can inspect what.
Find the object and its SD #
For a file, sd show queries owner,
group, DACL and the integrity-label subset:
sd show ./report.txt --all
Replace the example path with the object you identified. sd follows a named
symbolic link by default; its documented --no-follow-symlinks option selects
the link itself. Inspect the same object the failed operation meant to reach.
--all changes formatting, not selection: this view does not include audit
ACEs or the rest of the SACL, and its summaries omit some ACE payload details.
Do not infer that undisplayed policy is absent.
For a registry key, reg sd prints owner,
group and DACL by default:
reg sd Machine/App
Only when you already have the authority to read the SACL, request it explicitly:
reg sd Machine/App --sacl
The full SACL needs ACCESS_SYSTEM_SECURITY; the default registry
owner/group/DACL view does not establish which SACL labels or policies exist. Similarly, a refused
sd show is a failed inspection, not a decoded explanation of the original
operation. Keep the returned error and the scope of any descriptor you obtained.
For a process's descriptor, use the separate
process-SD inspection reference
with an authorised diagnostic tool; the PSB and a token report do not replace it.
In the parts you can read, look for:
- Owner and DACL: the owner SID, ordered allow/deny ACEs, inheritance flags,
and the SIDs and rights each ACE names.
INHERIT_ONLY(IO) rules apply to descendants rather than this object. A non-inherit-only access-control ACE namingOWNER RIGHTSsuppresses the owner's implicitREAD_CONTROL | WRITE_DACgrant. - Mandatory label: the first applicable
SYSTEM_MANDATORY_LABEL_ACEin the SACL and its policy bits. - Process-trust label: an applicable
SYSTEM_PROCESS_TRUST_LABEL_ACEand the rights its mask allows a non-dominant caller. - Other policy inputs: scoped central-policy references
(
SYSTEM_SCOPED_POLICY_ID_ACE), resource attributes (SYSTEM_RESOURCE_ATTRIBUTE_ACE) and conditional ACEs.
A component omitted from your permitted view is unknown, not empty. Reading an owner or finding one allow ACE is not enough to establish effective access.
Rehearse a file check, within its limits #
For a file, sd check explains a
check without performing the requested file operation:
sd check ./report.txt read --pid PID --explain
Choose the documented permission name or mask that matches the failed request.
Without --pid, the check uses your own token. Save the explanation and any
error, not just the exit status: a non-zero status can also mean the path was
unreachable or the tool could not complete the check.
Treat this as evidence about the inputs tested. The command documents a process token selector, not a thread selector or switches for every optional access-check input. It does not establish that it reproduced a service's impersonated caller, local claims, backup/restore intent or process-trust context. It also does not test rights on an existing handle. An allowed rehearsal is not a guarantee that the application's full operation will succeed. If the result differs, compare the caller, target, requested rights and timing before changing policy.
Check audit evidence #
Check the recorded attempt alongside the live state, rather than treating logs
as a last resort. In Event Viewer, select
the relevant time range and filter Type to kacs.audit.access.checked.
Select a record to inspect its fields. The terminal route is
Using evctl.
Correlate the timestamp, emitting process, subject token/session and object
information actually present in the record. The subject is the caller; an
object.process.* or object.token.* field identifies the target. Some checks
cannot supply a path or a complete object identity, so do not infer one.
For a matching access-check record:
- Compare
access.requestedandaccess.granted. The requested mask is already generic-mapped to the object's rights. access.denied-integrityandaccess.denied-trust, when present, identify bits withheld by the mandatory-integrity or process-trust checks.trigger.aceidentifies the audit ACE that caused a SACL-driven record; it is not a trace naming the DACL ACE that denied access.fields.attestation.userspacemarks an advisory syscall check whose descriptor and supplied context came from userspace. It is not proof that the real operation used those same inputs.
For a kacs.audit.privilege.used
record with operation.name equal to access-check, a false outcome.success
means a privilege contributed rights that did not survive. It does not mean the
privilege was absent, and it does not identify one narrowing layer on its own.
No record is not proof of no attempt. Access auditing depends on matching
SACL rules or token policy, and syscall SACL records additionally require the
calling process's enabled SeAuditPrivilege. Reader permissions, query scope,
retention and transport loss also limit what you can see. One check can produce
several records; MAXIMUM_ALLOWED success is not evidence that a particular
right was granted, and it produces no access-check privilege-use record.
See Find missing records.
Use the raw event stream only to diagnose
the transport itself. It requires SeSecurityPrivilege, can lose events, and
is not a replacement for recorded history.
The six places a denial can come from #
For a completed access check, these are the common policy questions. They are not a classification of every application error or failed inspection:
- Could the impersonation token be used for access at all?
- Did mandatory integrity control (MIC) withhold the requested rights?
- Did process integrity protection (PIP) withhold them?
- Did the DACL grant them for this caller?
- Did a restricted-token pass, confinement or central policy narrow the grant?
- Did an operation that legitimately needs a privilege have it present, enabled and, where required, accompanied by the caller's intent?
Review the relevant policy #
Use the evidence above to choose the relevant review below. The access-decision overview explains the complete ordering; a visible allow rule or a hand-worked DACL alone is not a full verdict.
Impersonation level #
An impersonation-type token at Identification level cannot be used for an access check. Confirm the type as well as the level. Ask the application's owner to review what the client requested and whether the two-gate model downgraded it. Identification may have been deliberate; do not assume the client intended to delegate more authority.
MIC #
Compare the caller's integrity and mandatory policy with the object's applicable
mandatory label and the requested rights. NO_READ_UP, NO_WRITE_UP and
NO_EXECUTE_UP affect the corresponding mapped categories. An object without an
applicable label defaults to Medium with no-write-up; an unreadable SACL does
not establish that this default applies.
Review whether the workload was launched at its intended integrity and whether the object's label matches its purpose. Do not lower a label or elevate the caller merely to make the test pass. MIC does not revoke rights already granted by backup/restore privileges, so an integrity mismatch alone is not the full answer. See Mandatory integrity control.
PIP #
PIP compares process trust, not the user's identity or token integrity. Both
caller trust axes must dominate the object's applicable trust label; otherwise
the label's mask limits the allowed rights and can revoke privilege grants.
Use the permitted PSB view and any recorded access.denied-trust evidence.
For a protected service, review whether the client is using its supported interface and intended binary. Administrator membership or another privilege does not bypass this boundary. Review unexpected protection or signing state with the component owner. See Process integrity protection.
Privileges #
First establish that this operation is intended to use a privilege. Then separate three findings:
- Absent: review the principal's privilege assignment with its policy owner.
- Present but disabled: review the application's use of that privilege.
- Enabled but intent missing: backup/restore intent belongs to the caller's operation, and cannot be inferred from the token dump.
A privilege's presence is not a general access grant. Enabling privileges or adding backup/restore intent is not a routine repair for an ordinary read or write. PIP, confinement and central policy can still constrain privilege-granted rights. See Privileges in the pipeline and Intent-gated privileges.
The DACL walk #
Compare the requested rights with the ordered ACEs and the caller's actual SID
set. Enabled groups participate in allows; deny-only groups participate in
denies. Check INHERIT_ONLY, owner-rights suppression and inherited rules before
concluding that a rule applies. For conditional ACEs, missing claims or resource
attributes can produce UNKNOWN, which does not grant access through an allow
ACE.
ACE order matters: the walk is first-writer-wins for each undecided right, not a
search for any matching allow. Use DACL evaluation
and Ownership for the exact matching
rules, including OWNER RIGHTS and PRINCIPAL_SELF.
If the evidence points to the DACL, ask the resource's policy owner to compare its intended audience and rights with the current rules and inheritance. Avoid broad grants, ownership changes or inheritance resets as diagnostic shortcuts.
Narrowing layers #
A DACL grant may still be narrowed:
- Restricted token: compare the restricting SIDs with the object's rules. The write-restricted variant narrows only write-category rights. Privilege grants can survive this pass; review the application's token construction.
- Confinement: compare the confinement SID and declared capabilities with the resource policy. Privileges do not bypass confinement. Review the workload's intended capabilities with its deployment or policy owner.
- Central access policy (CAAP): check referenced effective policies, their applicability and resource attributes with the directory/policy owner. A missing policy uses the documented recovery policy. A staged-policy mismatch is a rollout signal, not a statement that the staged policy caused the denial.
Do not remove restrictions, confinement or central-policy references to test a hypothesis. See Narrowing layers for the separate mechanisms and their policy-specific references.
A compact checklist #
- Keep the failing action, time, caller, object, requested rights and exact error together.
- Identify the process in Task Manager; inspect the relevant token and PSB using the documented commands and your existing authority.
- Read the descriptor components you are allowed to see. Mark inaccessible components as unknown.
- For a file, use
sd check --explainwith the identified target and rights; record which inputs the rehearsal does and does not reproduce. - Correlate available audit evidence. Account for missing records and changes since the failure.
- Take the finding to the appropriate application or policy owner. After an approved correction, verify the original operation in its intended context; a changed descriptor or an allowed rehearsal alone is not completion.
When the evidence is incomplete #
Hand the investigation to the application or resource-manager developer when the failure depends on a transient impersonation token, caller-supplied claims, intent flags, object-type mapping or other inputs the operator tools cannot reproduce. Include the evidence collected, its timestamps and scope, and each inspection that was refused. Do not fill missing inputs with guesses.
The existing SDK checking-access guide covers programmatic evaluation. Its request reference defines the token, descriptor, desired mask, generic mapping, privilege intent, self SID, local claims, object tree, PIP context and audit context. The check and audit outputs references describe the granted mask, continuous-audit mask and staging-mismatch flag. The Kernel TRM KACS ABI preserves the raw syscall structures.
These checks are advisory. A developer must match the original inputs and understand the syscall's audit limits before treating a reproduction as an explanation. Calling or decoding a syscall is not a required operator step, and its answer does not perform or authorise the original operation.
Where to go next #
- Task Manager and token for identifying the caller.
- sd and registry access control for the object's policy and the supported inspection commands.
- Event Viewer and Find missing records for recorded evidence and the limits of a search.
- Inspecting security state for the underlying interfaces and access requirements.
Inspecting tokens
Peios / Using Peios / Inspecting security state
Use the read-only forms of token to learn
who a process acts as and what identity, privileges and restrictions its token
carries. Choose the target first: the inspection tool's token, a process's
primary token and a thread's impersonation token can describe different callers.
A report is current state, not a record of an earlier failure and not a list of everything that caller may access. For a denial, keep the object and requested operation alongside it; follow Debugging a denial.
Reading from the shell #
Start with a summary, then request only the detail needed:
token
token show --all
token show --pid PID --all
token groups --pid PID
token privs --pid PID
token stats --pid PID
The first two inspect the token running the command. --pid PID selects the
identified process's primary token. Use Task Manager
to find and check the process rather than guessing its PID.
For impersonation, the command documents --tid TID together with --pid PID:
token show --pid PID --tid TID --all
The failing thread's effective token is its impersonation token while impersonating, otherwise the primary token. A primary-token report cannot stand in for an impersonated client's identity. If the thread has exited or reverted, keep that limitation with the result. Do not assume a query made later reconstructs the token used earlier.
The other documented selectors are --self (the default), --real (your primary
token specifically), and --peer SOCK_FD (a connected socket's captured peer
identity). A peer token is the identity conveyed on that connection, not a
general lookup of another process. See Choosing which token.
--raw prints raw SIDs, --label uses labels where known, and --json requests
structured output. Preserve the selector, timestamp and any error with a saved
report. The command also has mutating subcommands; adjusting, duplicating,
restricting or impersonating a token is not part of read-only inspection.
Read the fields as evidence #
| Field or question | Read with | What to check |
|---|---|---|
| Principal | token user | The user SID; a displayed name is a label for that identity. |
| Groups | token groups | Group SIDs and attributes. Enabled groups can match allows; deny-only groups match denies. |
| Privileges | token privs | Present and enabled are different. A disabled privilege cannot contribute, and presence alone does not bypass other policy. |
| Integrity | token integrity | The token's level; compare it with the object's label and mandatory policy when diagnosing MIC. |
| Logon context | token logon, token stats | Logon type, logon SID and session identity. The LogonSession ID is separate from interactive-environment scope. |
| Confinement capabilities | token caps | Which capabilities are present; combine them with the confinement identity from the fuller report and the object's rules. |
| Claims | token claims | User/device attributes available to conditional ACEs; the application may additionally supply per-call local claims. |
| Default owner, group and DACL | token owner, token group, token default-dacl | Defaults for objects created with this token, not the security descriptor of every existing object. |
| Origin and source | token origin, token source | The originating session of a derived token and information about what minted it. |
| Type, impersonation level, restrictions, elevation and mandatory policy | token show --all | The full set of supported query classes, interpreted with the limits below. |
Apply the same target selector to these field commands; without it they inspect
your own token. token query CLASS is the documented raw named-class JSON form
for tooling. The complete class numbers and payloads are maintained in the
Kernel ABI
and token query payload reference,
rather than duplicated here.
show --all means every supported query class, not every internal token field.
The ABI notes explicitly list fields with no query class, including
audit_policy, write_restricted and confinement_exempt. Do not infer these
from their absence in a report.
Patterns by use case #
Who was this thread acting as? Use the process and thread identity supplied by the failure report, inspect the relevant token, and distinguish its type from its impersonation level. A primary token also carries an impersonation ceiling; the level alone does not tell you that the thread is impersonating. For earlier state, correlate the available audit record instead.
Which session is this? token show displays the LogonSession LUID as
session_id; the underlying statistics field is auth_id.
interactivity_scope is a separate interactive-environment scope. Use the logon
session ID with logonse show ID,
subject to the session view's access limits.
Is this the elevated half of a token pair? The elevation type can be Full,
Limited or Default. Full and Limited describe the two sides of a linked pair;
Default means the token is not part of one. This is evidence about the token,
not a recommendation to elevate it. The separate token linked command reports
a linked counterpart where accessible.
Did the token change? Token statistics include its ID and modified_id,
which changes on adjustment. Keep both when comparing observations. Several
queries are not an atomic snapshot, and replacing a thread's token is not the
same as adjusting the one you previously inspected.
Would this token be allowed to access an object? The token is only one input.
For a file, use the documented sd check rehearsal and its
limits.
For other objects or caller-specific inputs, use their supported diagnostic
path or ask the component owner to investigate.
Limits and refused queries #
- Reading another process's token needs
PROCESS_QUERY_INFORMATION, PIP dominance and the token's ownTOKEN_QUERYgrant. A refusal does not tell you which of those checks failed. Protected processes can remain closed to an administrator. - Self-access depends on the surface. The documented own-process
/proctoken path is query-only without those checks; a direct self-open API checks the token's descriptor and can be denied after that descriptor changes. - A missing or exited target, a denied query and an invalid request are different results. Keep the tool's message; a failed query is not an empty token.
- Process-based inspection cannot enumerate tokens held only by descriptors. Current queries cannot recover a destroyed token or historical fields.
- Queries do not grant adjustment, installation, duplication or impersonation
rights. The
usedprivilege state is not a timestamped history of each use; use audit evidence for that question.
See Token access rights for the per-surface checks and Inspecting security state for related views.
Obtaining a token fd #
This is the tool-author boundary. Token pseudo-files are handles, not
human-readable text: cat /proc/<pid>/token is not an inspection command.
The process path selects the primary token and
/proc/<pid>/task/<tid>/token selects the thread's effective token; the
/sys/kernel/security/kacs/self surface also provides a token handle.
The SDK opening reference covers
self, process, thread and socket-peer token acquisition and error results.
The Kernel TRM access-rights chapter
documents the query-only /proc handles, fixed peer-token rights, descriptor
checks and cached handle rights. A handle with only TOKEN_QUERY cannot perform
operations requiring other token rights.
KACS_IOC_QUERY #
Operators use token to perform and decode queries. Tool authors should start
with the SDK query reference and
two-call buffer protocol.
The SDK provides typed readers for common fields and the generic class reader
for the rest.
For a raw-ABI consumer, the preserved
querying-a-token-handle explanation
covers KACS_IOC_QUERY, size probes, short buffers and the two-call sequence.
The generated KACS ABI holds the exact ioctl
number, argument layout and class constants; its
ABI notes
define the returned bytes. There is no need to decode those bytes by hand to
follow this operator workflow.
See also #
- token: the complete command interface.
- Debugging a denial: caller, object, requested action and audit evidence together.
- Tokens: the identity model behind the fields.
- Working with tokens: developer tasks using the SDK.
Mount policies
Peios / Using Peios / Mount policies
Choose a mount policy before attaching a filesystem. It determines what Peios does when a file has no security descriptor (SD): deny access, derive a temporary SD, or derive one and save it. A valid stored SD still supplies the file's access rules, and a corrupt SD is denied under every managed policy.
Start by inspecting the devices and current mounts:
Match the intended filesystem to its stable device name, then choose a class below. For a new system filesystem, format it with an SD before using deny-missing. For an existing volume, check whether its descriptors were preserved before choosing a strict policy.
The four classes at a glance #
| Your task | Class | Result when an SD is missing |
|---|---|---|
| Use a provisioned system or application filesystem | facs_deny_missing | Deny SD-based access. Root, /home and /var are described as strict system mounts. |
| Read removable media, FAT/exFAT, or a volume without writing back synthesised SDs | facs_synthesize_ephemeral | Derive an SD in memory; do not write it back. A cold inode needs synthesis again. |
| Adopt a filesystem into Peios and store descriptors as files are accessed | facs_synthesize_persistent | Derive an SD and schedule write-back. Verify stored SDs before switching to deny-missing. |
Use kernel pseudo-filesystems such as /proc and /sys | unmanaged | FACS does not apply; the kernel's per-operation checks still do. |
Only the three managed classes are administratively settable. unmanaged is
reserved for the kernel, and StrataFS has a fixed deny-missing policy. See
Policy classes for failure and recovery
conditions.
Why mount policies exist #
A managed access check needs an SD, but storage differs: ext4 needs ea_inode
for large descriptors, XFS supports large xattrs, tmpfs loses its contents on
unmount, FAT/exFAT cannot store xattrs, and an NFS client may not receive the
server's descriptors. Choose a policy that fits both the filesystem's storage
and whether you intend to change its metadata. Check SD storage by
filesystem before adopting a
volume.
Per-superblock, not per-path #
The policy belongs to the underlying filesystem instance (the superblock).
All paths and bind mounts sharing that instance share its policy, including a
read-only bind and its read-write source. Binding /etc/peios at
/old/etc/peios does not create an independent policy boundary.
Separate superblock instances can have different policies. Do not assume that
a second mount of the same device creates one: the
mount reference describes superblock reuse and
--exclusive, including its restrictions for writable block devices.
What the policy decides #
For managed filesystems, it selects the missing-SD behaviour, the optional mount-level synthesis template, and whether synthesised SDs are saved. Synthesis tries parent inheritance, then the mount template, then the fallback. The class applies to the whole superblock; it is not a per-file permission.
What the policy does not decide #
- File data persistence and available operations still depend on the filesystem.
- Changing policy does not rewrite stored SDs or change already-open handles' cached access masks. Future access checks use the new policy.
- A policy change does not propagate into another mounted filesystem below the same directory tree.
- Ephemeral SD synthesis prevents synthesis write-back, not other disk writes. Select read-only separately when required, and read the inspection warning: a read-only filesystem mount alone can still replay an ext4 journal.
Where to start #
- Choose the class using Policy classes.
- Check storage and preservation requirements in SD storage by filesystem.
- Use Managing mounts to apply the choice, verify the result and interpret failures.
The commands #
Policy classes
Peios / Using Peios / Mount policies
Choose the class for the files you expect to find, then verify that expectation: use deny-missing for provisioned storage, ephemeral synthesis when SDs must not be written to the volume, and persistent synthesis to adopt existing files. Changing class is not a bulk conversion and does not repair corrupt descriptors.
The command-line spellings are policy=deny-missing,
policy=synth-ephemeral and policy=synth-persist; see
Managing mounts for application and
privilege requirements.
facs_deny_missing #
Use this for a filesystem whose files are expected to carry valid SDs, including
provisioned system mounts such as root, /home and /var. The base-image build
is expected to supply descriptors, and subsequent creation inherits from the
parent. A restore that skips security metadata breaks that expectation.
A missing SD produces -EACCES for operations requiring an SD-based access
check. Ordinary reads, writes, deletion and permission changes can therefore
fail before you can repair the file through the usual path.
Before switching an adopted volume to this class, verify stored SDs on the files you need, including files that ordinary usage may never have touched.
facs_synthesize_ephemeral #
Use this when the filesystem cannot store SDs or when reading it must not add security metadata. Typical uses include removable media, FAT/exFAT and NFS client mounts. The kernel reads a stored SD if one exists; otherwise it derives one through the synthesis chain below and uses it for the access check.
The result is cached only while the inode is in memory. It is never written back. After eviction, a later open synthesises again; unchanged inputs produce the same SD. This costs work on a cold inode, but leaves on-disk metadata alone. It does not prohibit writes to file contents: read-only mounting is separate.
Earlier guidance also lists tmpfs/devtmpfs here. The kernel TRM documents stricter defaults for those filesystems; check the filesystem-specific notes rather than assuming their class.
facs_synthesize_persistent #
Use this to adopt a volume whose missing descriptors should become persistent. The first access derives an SD, uses the cached value immediately, and schedules write-back. A later access reads the stored SD after write-back succeeds.
Write-back is deferred until the triggering operation finishes, normally before its syscall returns to userspace. It is best-effort, not evidence that every file now has a stored descriptor. If the entry is evicted or the task exits first, the next access derives the same value and retries. A policy/template change before write-back discards the pending value and derives against the new inputs, rather than saving a superseded SD.
This is incremental adoption: regularly used files acquire SDs first; rarely-used files wait until accessed. Once all required files have stored SDs, you can switch to deny-missing. Any stragglers become inaccessible to normal SD-based operations. Existing stored descriptors are unaffected.
For the locking and task-work mechanism, use the kernel write-back reference.
unmanaged #
This class is reserved for kernel-managed pseudo-filesystems. It skips FACS, not all security checks:
/proc/<pid>/*uses process-SD and PIP checks under the two-check rule./sysuses the kernel's per-operation rules, including writes restricted toBUILTIN\Administratorsand SYSTEM./sys/kernel/security/kacs/*has explicit descriptors maintained and read by the kernel's own logic.
kacs_set_mount_policy rejects unmanaged with -EINVAL; operators cannot
use it to disable FACS on a regular filesystem. An ephemeral policy with a
permissive template can grant broad access to files missing SDs, but still runs
FACS and must be reviewed as a security change. It is not a substitute for
repairing a broken descriptor.
The synthesis chain #
When a managed synthesis policy finds no SD, it tries these sources in order:
- Parent-directory inheritance, using inheritable ACEs as for a newly-created child. See Inheritance.
- The mount-level template, commonly needed at the filesystem root where no parent on that filesystem supplies a usable descriptor.
- The fallback: owner and group SYSTEM;
GENERIC_ALLfor SYSTEM andBUILTIN\Administrators;GENERIC_READ | GENERIC_EXECUTEfor Everyone.
A missing template does not imply private access: the fallback grants other users read-and-execute. Review the template before exposing a volume. Earlier notes call the template limit 64 KB; the kernel contract specifies at most 65,535 bytes.
The kernel synthesis
reference
also specifies recursive parent synthesis, a 32-ancestor limit that fails
closed with EACCES, and creator inputs independent of the accessor's token.
Use that reference for implementation details.
Corrupt SD handling #
A stored SD that fails structural validation produces -EACCES under all three
managed classes. Bad headers, malformed ACLs, invalid SIDs and oversized
values do not trigger synthesis. Missing and corrupt are different conditions.
The kernel emits a corruption audit event once per inode per cache population, not on every repeated access. A restore producing many corrupt SDs may therefore produce a burst of events as files are first accessed.
Changing to a synthesising policy does not make a corrupt SD valid. Recovery
requires fixing the descriptor or accepting denial. The kernel repair
reference
documents set-security under SeRestorePrivilege and offline xattr repair on
an unmounted filesystem.
Class transitions #
| Transition | Check before changing |
|---|---|
| Persistent synthesis to deny-missing | Confirm the required files have stored SDs; files still missing one will be denied. |
| Ephemeral to persistent synthesis | Confirm that saving metadata is intended and supported. Each later missing-SD access saves its newly derived result. |
| Deny-missing to a synthesis class | Treat this as a deliberate relaxation for recovery/adoption. Review the template; corrupt SDs remain denied. |
A transition changes policy only; no files are touched at that moment. Future accesses re-evaluate stale synthesis state. Existing handles keep their masks, and stored SDs remain unchanged. See Managing mounts before applying the change.
Where to go next #
- SD storage by filesystem: verify where descriptors can persist.
- Managing mounts: apply and read back the class and template.
mount: exact attach-time options.
SD storage by filesystem
Peios / Using Peios / Mount policies
Before choosing a mount policy or restoring a backup, check whether the target can store security descriptors and whether your copy path preserved them. A successful file-data copy is not enough to make a deny-missing volume usable.
Validate descriptor preservation #
-
Identify the source and destination filesystem types with
lsblkand the live mount listing. -
For mounted files, inspect effective permissions through
sd, which uses the security API. This does not establish that an SD is stored: a synthesis mount can return a usable in-memory descriptor while the canonical xattr is still absent. Direct operations on the canonical SD xattr are sealed. -
For an offline ext image or unmounted ext filesystem, check the root xattr:
debugfs -R "ea_list <2>" /dev/vda2Replace the example device only after identifying the target. This confirms presence on inode 2, not validity or coverage of every copied file.
-
Verify stored descriptors on child files and directories as well, using filesystem-appropriate offline inspection on an unmounted target; do not use mounted
sdoutput alone as a persistence test. Include explicit ACLs, private data and rarely accessed files. Persistent synthesis schedules best-effort write-back, so a successful access is not proof it completed. Themke2fs -dpath usesuser.peios.sdas its source carrier; ordinary canonical-xattr copying is not a substitute for that population contract. -
If metadata is missing, choose a deliberate repair/adoption plan before enabling deny-missing. If it is corrupt, synthesis will not repair it.
The sections below retain filesystem-specific storage details. Where the operator guide and kernel reference disagree, the discrepancy is called out; verify the deployed version before depending on that behaviour.
The canonical xattr #
For filesystems that support extended attributes, Peios stores the SD in the security.peios.sd xattr. The name is in the security.* namespace, which requires privileged access to read or write at the filesystem layer — which doesn't matter for FACS-managed access (FACS denies direct xattr operations on the SD xattr unconditionally; access goes through kacs_get_sd / kacs_set_sd), but does mean the xattr survives operations that respect security xattrs.
For NTFS, the xattr name is system.ntfs_security — the native NTFS security attribute, accessed through the same xattr interface. Using the NTFS-native name means SDs round-trip cleanly between Peios and other operating systems that read NTFS.
The choice of xattr name is filesystem-specific:
| Filesystem | xattr name |
|---|---|
| ext4, XFS, Btrfs, tmpfs, others with generic xattr support | security.peios.sd |
NTFS (via the ntfs3 driver) | system.ntfs_security |
For other filesystems (with no xattr support or no convention), the SD is held in memory and re-synthesised on each cold open. See the per-filesystem sections below.
ext4 #
ext4 supports xattrs in the inode body and in a separate xattr block. The inode's available inline space is not 4 KB: it depends on inode size and other metadata. The separate block is commonly 4 KiB and also has overhead and may hold other attributes. See the upstream ext4 storage layout.
The ea_inode feature stores a large xattr value in a separate inode when
it does not fit in the inode body or external block; see large extended
attribute values.
The Peios guide documents support up to the 65,535-byte SD limit with this
feature, enabled at creation or later through tune2fs.
mke2fs on Peios enables ea_inode by default for ext4, and raises the inode size to 512 bytes so that a typical SD fits inline in the first place — see mke2fs. Complex or deeply inherited ACLs can outgrow the available xattr space;
ea_inode accommodates large values without assuming a universal inline limit.
If an ext4 filesystem without ea_inode cannot fit an SD in its available
inode-body or external-block xattr space, the filesystem's xattr write fails. The kernel handles this by either:
- Refusing the
kacs_set_sdcall with an error indicating the filesystem cannot hold the SD. - Or, in the
facs_synthesize_persistentcase, refusing the write-back and treating the file as if it has no SD (re-synthesising next time).
Check whether ea_inode is enabled when large descriptors cannot be stored.
Do not assume that a typical descriptor fitting inline proves every descriptor
on the volume will fit.
XFS #
XFS supports xattrs natively, up to 64 KB per attribute by default. This is comfortably more than the SD size limit (65,535 bytes), so SDs fit without any special configuration.
XFS is a fine choice for FACS-managed filesystems. No ea_inode-style consideration is needed; SDs just work.
Btrfs #
Btrfs supports xattrs, but do not assume any practical SD size will fit. Upstream Btrfs documents xattr items in tree leaves, with attributes sharing a name hash required to fit in one leaf. This does not support the original guide's claim that large xattrs receive their own extents. Peios-specific support for larger descriptors has not been verified. Validate the actual descriptor sizes on the target before depending on a restore or persistent-synthesis workflow.
Btrfs's snapshot and clone behaviour is interesting for SDs: a Btrfs snapshot of a directory includes the SDs of every file within. A snapshot is a point-in-time view; the SDs in the snapshot reflect what they were when the snapshot was taken. If the original files' SDs are subsequently modified, the snapshot still has the old SDs. This is the expected behaviour but worth noting for backup/snapshot workflows.
tmpfs and devtmpfs #
tmpfs is an in-memory filesystem. It supports xattrs, but everything it stores lives in RAM and is lost on unmount or reboot. SDs on tmpfs are present while the filesystem is mounted; they vanish when it is unmounted.
tmpfs is typically mounted with facs_synthesize_ephemeral. The synthesis happens in memory anyway (the tmpfs storage is memory), so there's no operational difference between "store an SD in the tmpfs xattr" and "synthesise an SD into the kernel's per-inode cache".
devtmpfs is the kernel-managed filesystem for device nodes. SDs on devtmpfs are applied by udev rules, not by FACS-driven synthesis. The udev daemon (or its Peios equivalent) sets the SD on each device node as the node is created. This is a different model from the synthesis-based pattern other filesystems use; it works because the device-node population is controlled centrally and the SDs can be set deterministically.
devtmpfs uses the facs_synthesize_ephemeral class, but in practice the synthesis path is rarely hit because udev provides the SDs.
NTFS — round-trip via ntfs3 #
NTFS is the Windows-native filesystem. Peios mounts it through the ntfs3 kernel driver, which exposes the NTFS security attribute via the standard xattr interface under the name system.ntfs_security.
The implications:
- SDs written by Peios to an NTFS volume use the same on-disk format as Windows. A Windows system reading the same volume sees the SD natively.
- SDs written by Windows to an NTFS volume are readable by Peios. Round-tripping a volume between the two operating systems preserves SDs.
This is the "binary-compatible" property the SD format gives. The wire format is the same; the filesystem driver translates between the xattr interface and the on-disk security attribute.
NTFS volumes are typically mounted facs_synthesize_ephemeral rather than facs_deny_missing. The reasoning: an NTFS volume from Windows may have files without Peios-recognised SDs (the SDs are Windows-native and may use principals that don't exist on the Peios system). Ephemeral synthesis lets such files be accessible without modifying the volume's stored SDs.
FAT and exFAT #
FAT and exFAT have no xattr support at all. There is no place to store an SD; the on-disk format simply doesn't have the metadata channel.
FACS handles this by using facs_synthesize_ephemeral for FAT/exFAT mounts. Every file gets a synthesised SD in memory; no SD is ever written back. The synthesised SD applies for the file's time in the inode cache; when the file is evicted, the SD is gone and will be re-synthesised on next access.
This means FAT/exFAT files cannot be given persistent KACS-style permissions. The synthesised SD is the same every time (assuming the same inputs — parent SD, mount template), so the access decision is deterministic, but there is no way to customise per-file.
For most FAT use cases this is fine — FAT is typically used for removable media or boot partitions where uniform-permissions semantics is acceptable. The mount-level SD template can be configured to grant whatever access pattern is appropriate for the mount as a whole.
NFS — synthesise locally, enforce remotely #
NFS client mounts are a unique case. The actual files live on a remote server; the local filesystem driver is a network protocol implementation, not a real filesystem.
The mount class is facs_synthesize_ephemeral. FACS synthesises a local SD per the synthesis chain (typically yielding a sensible default from the mount template) and uses it for local access control. The local FACS check decides whether to forward the operation to the server.
If the local check passes, the operation goes to the server. The server has its own access control (potentially also Peios with FACS, potentially another OS with different rules); the server's access control decides whether the operation actually proceeds. If the server denies, the operation fails with whatever error the protocol returns (typically -EACCES or -EIO).
This is "dual authority" — both client and server have a say. The client's denial is local-final; the server's denial is remote-final. There is no single source of truth for the access decision.
The implications were covered in Special cases under "NFS — dual authority": don't trust local FACS results for security, expect I/O errors from server-side denial, the local synthesised SD is not the server's actual SD.
/proc and /sys #
/proc and /sys are kernel pseudo-filesystems. Their mount-policy class is unmanaged — set by the kernel at boot, not changeable via the public ABI.
/proc doesn't have an SD per-file in the FACS sense. Access to /proc/<pid>/* is gated by the per-process rules (process SD + PIP, from the two-check rule). The kernel implements these checks directly when serving /proc file operations.
/sys similarly has its own per-file rules. The /sys/kernel/security/kacs/* entries have explicit SDs the kernel maintains; other /sys files have hardcoded rules ("writes restricted to BUILTIN\Administrators and SYSTEM").
The unmanaged class is what tells FACS to not interfere with these. The kernel knows what it is doing with its own pseudo-filesystems; FACS stays out of the way.
Stacked filesystems — overlayfs and StrataFS #
A stacking filesystem presents files that physically live somewhere else. overlayfs merges a read-only lower layer with a writable upper one; StrataFS composes several directories into one view. Neither stores a security descriptor of its own, and neither needs to: the descriptor belongs to the file, and the file is on the layer underneath.
So a stack forwards. When the kernel needs the SD of a file in the merged view, it asks the layer that actually holds it, and what comes back is that layer's effective descriptor — which is to say, whatever an access check on the underlying file would have used. If the layer underneath stores an SD, the stack sees the stored one. If the layer underneath synthesises, the stack sees the synthesised one. Synthesis composes upward through the stack; it is not a private arrangement between the kernel and the bottom layer.
That is what makes the common live-boot arrangement work:
overlay <- the merged root, facs_deny_missing
upper: tmpfs <- writable scratch, stores real SDs
lower: squashfs <- read-only image with no SDs, facs_synthesize_ephemeral
The overlay stores nothing and can still be facs_deny_missing, because neither layer beneath it ever answers "missing": the tmpfs has stored descriptors, and the squashfs synthesises one for every inode.
Two consequences worth holding onto:
The stack's own policy class governs only what happens when every layer beneath it has nothing to offer. Pick it for that case. Over a synthesising lower, facs_deny_missing is the strict-and-correct choice; over an unmanaged lower it would lock the whole view.
A copy-up does not carry the lower's descriptor up with it. When overlayfs copies a file from the lower layer to the upper to make it writable, it creates a genuinely new inode in the upper, and that inode gets its descriptor by ordinary inheritance from its parent in the upper — not by duplicating the lower's. This is deliberate: security.peios.sd is not userspace-writable (SD mutation is kacs_set_sd's job), so a verbatim copy could not be performed even in principle, and the inherited answer is the right one anyway. Every other extended attribute is copied up normally.
Summary #
| Filesystem | SD storage | Typical mount class |
|---|---|---|
| ext4 | security.peios.sd xattr; ea_inode for large values | facs_deny_missing (for system mounts) |
| XFS | security.peios.sd xattr, native large support | facs_deny_missing |
| Btrfs | security.peios.sd xattr; verify size limits | facs_deny_missing |
| tmpfs | security.peios.sd xattr in memory | facs_synthesize_ephemeral |
| devtmpfs | xattr in memory, populated by udev | facs_synthesize_ephemeral |
| NTFS (ntfs3) | system.ntfs_security xattr, NTFS-native | facs_synthesize_ephemeral (typical) |
| FAT / exFAT | No SD storage; in-memory only | facs_synthesize_ephemeral |
| NFS client | No client-side storage; synthesise locally | facs_synthesize_ephemeral |
| squashfs | No SD storage unless the image was built with one | facs_synthesize_ephemeral |
| overlayfs | None of its own — reads the real layer | facs_deny_missing |
| StrataFS | None of its own — reads the provider | facs_deny_missing, not settable |
| /proc | n/a — no FACS | unmanaged |
| /sys | n/a — kernel-managed SDs | unmanaged |
Treat the table as the original guide's typical-policy summary, not proof of the policy on a running mount. tmpfs/devtmpfs, squashfs and StrataFS have important source discrepancies noted above. Read back the actual policy before relying on it.
Where to go next #
For what each policy class does with a missing SD, read Policy classes.
For setting and reading a mount's policy at runtime, read Managing mounts.
For the structure of the SD being stored, read Security descriptors.
Managing mounts
Peios / Using Peios / Mount policies
Identify the filesystem and inspect its current state before attaching storage or planning a policy change. Verify both the missing-descriptor class and the synthesis template. If it is already mounted, read the change limits before proceeding.
Inspect live mounts and policy #
Use these read-only listings first:
mount lists live state from the kernel; Peios has no /etc/fstab or
/etc/mtab. The listing is limited to mounts your token may observe. An empty
or partial listing does not prove that a device is unused.
Match the intended filesystem to a verified stable source name, then check its live source, target and filesystem type. To inspect its policy in Disk Manager:
- Open the launcher and type
disk. - Select Mounts, then the intended mount to see its options and policy.
- Open Permissions… to inspect the synthesis template. The UI policy labels explain the missing-descriptor choices.
Check both class and template; the ordinary mount listing is not a template
dump. If Disk Manager cannot read the policy, verification remains incomplete.
Missing details do not establish the intended policy, and the GUI does not
bypass the privilege gate. See what Disk Manager may
show.
Check policy access #
The mount diagnostic in peiosutils 0.8.18 still names only
SeTcbPrivilege.
That hint is misleading for the pinned kernel. Check the deployed kernel and
caller's effective token before changing privileges; do not automatically grant
TCB or globally remap CAP_SYS_ADMIN to make the operation succeed.
SeManageVolumePrivilege is narrower than TCB but still permits substantial
authority over paths and synthesised policy.
Attach a filesystem #
For a new mount, the command shape is:
mount [-t TYPE] [-o OPTIONS] SOURCE TARGET
Choose -o policy=deny-missing, -o policy=synth-ephemeral, or
-o policy=synth-persist using Policy
classes. Use the verified stable source
name and the intended target mount point. Use persistent synthesis only when
writing SDs is intended; select read-only separately if file-data writes must
be prevented.
--synth-sddl SDDL supplies a well-formed template with an owner, and is valid
only with a synth-* policy. Review the synthesis chain and
fallback before
exposing the volume: parent inheritance takes precedence, and omitting a
template does not imply private access.
If setting the attach-time policy fails, no mount with the unintended policy
is attached. The mount reference
describes that guarantee and the full option contract.
After success, inspect the live mount and policy
again. Confirm the source, target, filesystem, writable state, class and template.
Also inspect after a failure: target creation or attachment may already have
happened before a later step fails. The source-verified execution limits
explain why neither a nonzero status nor --fake proves nothing changed.
Before changing an already-mounted policy #
mount sets policy only for a new filesystem mount. Bind, move, remount,
propagation and list mode reject policy=; mount -o remount is not a
policy-change interface. Disk Manager cannot commit changes because its
privileged disk service is missing, including changes offered by its forms.
The kernel exposes a privileged interface for an existing filesystem instance (the superblock), but these operator tools do not provide a ready-to-use command or working GUI flow for changing its policy. Confirm a supported privileged integration for the deployed system before proceeding, including its policy access requirements.
For integration authors, the kernel administration contract, SDK wrappers and syscall notes cover arguments, template validation, buffer handling, errors and generation semantics. The privilege disagreement is resolved for the pinned source above; other differences retained in the syscall notes are not resolved by that finding. A rejected policy change leaves the current class and template unchanged.
A second mount of the same device does not establish an independent policy
boundary. Read the mount superblock-reuse
restrictions before relying on a
separate instance; detaching and reattaching is not a generic policy-change
procedure.
What a policy change affects #
- All bind paths sharing the filesystem instance share the new policy. Independent superblocks are unaffected, even when reached beneath this mount.
- Stored descriptors remain unchanged. Already-open handles retain their granted rights; future access checks use the new policy.
- No file tree is rewritten or provisioned. Changing ephemeral to persistent does not save every cached SD immediately; later missing-descriptor accesses derive under the new policy and schedule best-effort write-back.
- Deny-missing can leave files without stored SDs inaccessible. Corrupt descriptors remain denied under every managed class; synthesis does not repair them.
Before hardening an adopted filesystem, check class
transitions and
validate descriptor
preservation,
including rarely accessed files. Persistent synthesis is incremental adoption,
not a whole-tree guarantee; mounted sd output alone does not prove durable
storage.
Mount non-root storage at startup #
The root filesystem is mounted by the initramfs before peinit takes over. A non-root data partition is mounted by an ordinary Oneshot service; peinit does not mount it directly. See Where peinit takes over.
Use Defining a service for the
definition, validation and boot-trigger fields, and Oneshot
services for the
completion contract. A service using a stable device
name needs a
Requires dependency on the device manager so the boot-time device replay
finishes before it opens that name.
These pages establish the service mechanism, but do not yet give a complete storage-specific recipe: the service identity, required privileges and mount arguments must be confirmed for the deployed system before creating a definition. There is no dedicated mount-registry schema established here, and Disk Manager cannot currently save startup mounts.
A complete integration must also define repeat-start, missing-device and
partial-failure handling. In the reviewed mount implementation,
--onlyonce supplies no existing-mount guard, and mkdir MODE is not applied.
Do not use those options as proof of safe repeat activation or private target
security. Device coldplug completion does not guarantee a particular disk
stays present. A successful Oneshot becoming Inactive does not unmount the
filesystem; command/service state and live mount state are separate.
After the service runs, inspect its status and logs. A successful Oneshot reports successful command completion and can then return to Inactive; its status alone does not establish the current mount or policy. Separately inspect the live mount, checking source, target and filesystem, and read the policy to confirm both class and template.
Check unexpected results #
- Attachment failed: read the command's diagnostic and
mountexit status. Check the source, options and reported permission or filesystem failure. A failed attach-time policy step does not publish a mount with an unintended policy. - Policy details are unavailable: check policy access and Disk Manager's visibility limits. Do not treat an unreadable class or template as successful verification.
- A file is still denied: distinguish a missing descriptor from a corrupt one. Deny-missing rejects missing SDs, and switching to synthesis does not repair corrupt SDs.
- Old access still works through an open handle: existing handles retain their granted rights. Check future access using a new handle.
unmanaged is reserved for the kernel and cannot be set administratively.
StrataFS's policy is fixed; neither is a workaround for an access failure.
See also #
- Policy classes: choose a class and diagnose missing or corrupt SDs.
- SD storage by filesystem: check whether a descriptor can persist.
- Privileges: understand the privilege gate.
- File Descriptor Storage: kernel policy, caching, write-back and repair contract.
mount: attachment options and status codes.umount: detachment options and status codes.
mount
Peios / Using Peios / Mount policies
mount attaches a filesystem to the Peios mount tree. With no operands it instead lists what is currently mounted. It is a faithful reworking of the util-linux mount(8) surface for Peios, with two structural differences: Peios has no /etc/fstab and no /etc/mtab, so every mount is described entirely on the command line and live mount state is read from the kernel; and a mount can apply a KACS mount policy to the new filesystem at attach time (see Mount policies).
mount [-t TYPE] [-o OPTIONS] SOURCE TARGET
mount [-o remount,OPTIONS] TARGET
mount --bind|--rbind|--move SOURCE TARGET
mount --make-shared|--make-private|... TARGET
mount [-l] [-t TYPE]
Internally mount uses the fd-based mount API (fsopen / fsconfig / fsmount / move_mount, plus open_tree and mount_setattr), never the classic single-shot mount(2). This matters in one place you can see: when a mount fails, the kernel's fs_context message log is drained and printed as the reason, even without -v.
Operands and argument shapes #
Because there is no fstab to consult, operand resolution is strict:
- No operands, no verb — list mode (see below).
- Two operands —
SOURCE TARGET. - One operand — an error. In util-linux a lone operand is resolved through fstab; Peios has none, so a single operand cannot be turned into a
SOURCE TARGETpair. Supply both, or use--source/--target. - A lone target is valid only for
-o remountand for a standalone propagation change (--make-*), which act on an existing mount point.
--source SRC and --target DIR name the operands explicitly and may be combined with a single positional. --target-prefix DIR prepends DIR/ to the target after it is chosen. Options may be interspersed with operands (mount SRC -o ro TGT), matching util-linux.
Paths, -o key=value values, labels and UUIDs are handled as opaque byte strings, never assumed to be UTF-8; an embedded NUL is a usage error.
Canonicalisation #
By default source and target paths are canonicalised (made absolute, symlinks resolved). -c / --no-canonicalize disables that; X-mount.nocanonicalize[=source|target] is the -o form and can disable it for just one side. The final path handed to the kernel is protected against a TOCTOU symlink swap on its last component. This does not reject every symlink in the original input: default canonicalisation has already resolved it.
Operation modes (verbs) #
mount performs one of several operations. The structural verbs — bind, rbind, move, beneath and remount — are mutually exclusive with one another. A propagation change is not a structural verb: it may stand alone on a target, or trail another verb or a new mount in the same command (applied afterwards as one or more mount_setattr steps), and several may be combined.
| Verb | How to request it | What it does |
|---|---|---|
| New mount | mount [-t T] SRC TGT | Attach a fresh instance of the filesystem at SRC onto TGT. |
| Bind | -B / --bind, or -o bind | Make an existing subtree visible at a second location. |
| Recursive bind | -R / --rbind, or -o rbind | Bind a subtree together with every mount underneath it. |
| Move | -M / --move, or -o move | Relocate an existing mount to a new mount point. |
| Move beneath | --beneath SRC TGT | Attach SRC beneath the mount currently at TGT (the kernel enforces several constraints; violations surface as exit 32). |
| Remount | -o remount[,...] TGT | Change the options of an existing mount (see Remounting). |
| Propagation | --make-* / --make-r*, or the -o tokens below | Change how mount/unmount events propagate across a subtree. |
| List | mount / mount -l | Print the current mounts. |
Propagation flags #
Each of these sets the propagation type of the mount at the target. The --make-r* (and r-prefixed -o) forms apply recursively to the whole subtree; the recursive form is all-or-nothing (it either applies to the entire subtree or to none of it).
| Flag | -o token | Recursive flag | Recursive -o token | Meaning |
|---|---|---|---|---|
--make-shared | shared | --make-rshared | rshared | Events propagate to and from peer mounts. |
--make-slave | slave | --make-rslave | rslave | Events propagate in from the master but not back out. |
--make-private | private | --make-rprivate | rprivate | No propagation either way. |
--make-unbindable | unbindable | --make-runbindable | runbindable | Private, and cannot be bind-mounted. |
A freshly created mount, bind or move is private by default unless its destination's parent is shared, in which case it joins that peer group — the standard kernel rule.
Source specification #
| Form | Resolution |
|---|---|
Device path (/dev/sda1) | Used directly. |
-L LABEL / LABEL=, -U UUID / UUID=, PARTLABEL=, PARTUUID= | Resolved to a device via libblkid. No match is a mount failure (exit 32). |
| A directory or regular file | Used directly (a bind source or target; a file may be a bind target). |
A pseudo source (tmpfs, proc, sysfs, none, …) | Passed through; -t is required because it cannot be probed. |
| An image file | Attached through a loop device (see Loop devices). |
Filesystem type #
-t TYPE names the type explicitly. -t auto, or omitting -t entirely, asks libblkid to safely probe the source; an ambiguous or multiply-signed device is refused (exit 32) rather than guessed at. X-mount.auto-fstypes=LIST constrains the probe candidates. Type lists and no<type> negation are not accepted in the mounting path (they remain meaningful only as a listing filter).
The -o option language #
-o takes a comma-separated list of key or key=value tokens and is repeatable (all occurrences are joined). A key="..." double-quoted value protects embedded commas; the key=value split is on the first =. Every token is sorted into one of the following categories.
Per-mount attributes #
Applied to the mount itself. Each accepts its negation; ro/rw additionally accept a =vfs / =fs / =recursive scope qualifier (bare is =vfs, non-recursive; =fs targets the superblock read-only flag; =recursive applies to the whole subtree).
| Option | Effect |
|---|---|
ro / rw | Read-only / read-write. |
suid / nosuid | Honour / ignore set-user-ID bits. |
dev / nodev | Allow / disallow device nodes. |
exec / noexec | Allow / disallow execution. |
atime / noatime | Update / never update access times. |
relatime / norelatime | Relative-atime updates on or off. |
strictatime / nostrictatime | Strict-atime updates on or off. |
diratime / nodiratime | Directory access-time updates on or off. |
nosymfollow | Do not follow symlinks on this mount. There is no positive symfollow token — the attribute is cleared only via a remount mask. |
The atime tokens share a single mode field; the last one specified wins.
Superblock flags #
| Option | Effect |
|---|---|
sync / async | Synchronous / asynchronous writes. |
dirsync | Synchronous directory updates. |
lazytime / nolazytime | Lazy on-disk timestamp updates on or off. |
iversion / noiversion | Inode version counting on or off. |
silent / loud | Suppress or emit certain kernel messages. |
mand / nomand | Obsolete (removed from the kernel). Accepted and ignored with a note under -v. |
Filesystem-specific parameters #
Any token not recognised above is forwarded verbatim to the filesystem: key=value as a string parameter, a bare key as a flag. There is no built-in allow-list and no length limit; a rejection by the filesystem is reported with the offending key and the kernel's message.
For example, StrataFS takes its precedence-ordered directory stack through
strata=:
See StrataFS for the stack flags, merge
and write-routing model, and the stratafs inspection command.
Userspace-only tokens #
These never reach the filesystem. They include the meta-verbs remount, bind, rbind, move; the loop controls loop / loop=/dev/loopN, offset=, sizelimit= (numeric values accept K/M/G/T and KiB/MiB/… suffixes); the propagation tokens listed above; and defaults, which expands to rw,suid,dev,exec,async (later tokens override it).
Functional X-mount.* options #
| Option | Effect |
|---|---|
X-mount.mkdir[=mode] | Attempt to create missing target directories, like -m. In the reviewed implementation, the requested mode is not applied. |
X-mount.subdir=DIR | Attach subdirectory DIR of a freshly mounted filesystem at the target. Effective only for a new-instance mount; silently ignored (noted under -v) for bind/move/remount/propagation. |
X-mount.noloop | Suppress the implicit loop device for a regular-file source. |
X-mount.auto-fstypes=LIST | Constrain the -t auto probe to these types. |
X-mount.nocanonicalize[=source|target] | The -o form of -c; with =source or =target it disables canonicalisation for just that path. |
X-mount.idmap and X-mount.owner / group / mode are not supported and are rejected with a usage error: Peios ownership is SID/security-descriptor based, so the fix is to add a SID/ACE to the descriptor rather than remap or chown.
KACS mount policy #
This is the genuinely Peios-specific part of mount. A mount policy is a per-superblock setting that governs how FACS treats the filesystem; the full model is described in Mount policies and its detail pages. mount can set that policy at attach time:
| Option | Policy class |
|---|---|
-o policy=deny-missing | facs_deny_missing — a file with no SD is unreachable. |
-o policy=synth-ephemeral | facs_synthesize_ephemeral — a missing SD is synthesised in memory only. |
-o policy=synth-persist | facs_synthesize_persistent — a missing SD is synthesised and written back. |
--synth-sddl SDDL | Provide the mount-level SD template used during synthesis (only valid with a synth-* policy). |
policy=unmanaged is not user-settable; only the kernel sets the unmanaged class, for its own pseudo-filesystems. policy= is valid only on a new mount of a real filesystem — combining it with bind/move/remount/propagation, or with list mode, is a usage error. See Policy classes for what each class does and SD storage by filesystem for how the SD is physically stored.
The policy is applied to the detached filesystem before it is attached: if setting it fails, that filesystem is not attached with an unintended policy. This is an attachment guarantee, not whole-command rollback: target creation may already have occurred. In kernel source 8e0e22de3a59cad506bbbf8873de456e16ad272d, the policy gate accepts enabled SeManageVolumePrivilege or SeTcbPrivilege and requires successful privilege-use marking. Failure of that gate gives EPERM (exit 1) and no attachment. --synth-sddl is validated client-side first — it must be well-formed SDDL and must include an owner.
Peios applies no coarse uid==0 check anywhere: the mount applet is not installed set-user-ID, and every privileged action is authorised per-operation by KACS.
Remounting #
-o remount changes the options of an existing mount without detaching it. Peios does not perform the util-linux "read the old flags and re-supply them" dance — the fd-based API applies deltas rather than resetting, so each remount touches only what you name. Per-mount attributes (ro/rw, nosuid, atime, …) are changed via mount_setattr; superblock flags, filesystem parameters and ro=fs/rw=fs go through the superblock reconfigure path. =recursive remounts the whole subtree, all-or-nothing.
A bind mount shares its source's superblock, so any superblock-level option on a bind remount (a Category-B flag, a filesystem parameter, or ro=fs/rw=fs) is refused as a usage error rather than silently mutating the shared superblock for every mount of that filesystem. Only per-mount VFS attributes are valid on a bind remount.
Loop devices #
A regular-file source is backed by a loop device automatically when the type is unspecified or the filesystem is recognised by libblkid; X-mount.noloop suppresses this. -o loop forces auto-allocation of a free device, loop=/dev/loopN names one, and offset= / sizelimit= select a region of the file (they imply loop and are an error against a real block device). A backing file already attached at the same offset and size is reused rather than doubly attached, to avoid corruption. Loops created by mount are auto-cleared by the kernel on unmount, so they do not leak; umount -d force-clears the rest (see umount).
Other flags #
| Flag | Effect |
|---|---|
-t, --types TYPE | Filesystem type, or auto. |
-o, --options LIST | Mount options (above); repeatable. |
-r, --ro, --read-only | Mount read-only (-o ro). |
-w, --rw, --read-write | Mount read-write and forbid the automatic read-only fallback on a write-protected device (-o rw). |
--source SRC / --target DIR | Name the operands explicitly. |
--target-prefix DIR | Prepend DIR/ to the target. |
-B, --bind / -R, --rbind / -M, --move / --beneath | The structural verbs. |
--make-*, --make-r* | Propagation changes. |
--exclusive | Force a unique superblock instance (no reuse). Meaningful only for multi-instance filesystems (e.g. tmpfs) or read-only block mounts; on an already-mounted writable block device it fails with EBUSY (exit 32). |
-m, --mkdir[=MODE] | Attempt to create missing target directories. The requested MODE is parsed but not applied at the reviewed source revision. |
-L, --label LABEL / -U, --uuid UUID | Select the source by filesystem label / UUID. |
-c, --no-canonicalize | Do not canonicalise paths. |
-f, --fake | Skip mount and policy syscalls; target preparation still runs. With --mkdir or X-mount.mkdir, directories may be created. See execution limits. |
-v, --verbose | Narrate resolved values and each syscall; drain the kernel fs_context log. Repeatable but -vv is the same as -v. |
-l, --show-labels | In list mode, append each filesystem's label. |
--onlyonce | Accepted and stored, but unused by the reviewed execution path. Do not rely on it to skip an existing mount or make repeat startup safe. |
-N, --namespace NS | Operate inside mount namespace NS (a PID, an ns file path, or a named namespace). Source resolution happens in the caller's namespace; the mount lands in the target namespace. |
-i, --internal-only | Do not invoke a mount.<type> helper. |
-n, --no-mtab | Accepted and ignored (Peios has no mtab). |
--synth-sddl SDDL | KACS synth-policy template SD (above). |
-h, --help / -V, --version | Standard. |
No external mount helpers ship in this version, so a network or FUSE type fails with "no helper for type" (exit 1) — a clean deferral, not a crash.
Source-verified execution limits #
These limits were checked in peiosutils 3344d4690476fd66bfaec99b1ae92190bbcba06f.
They describe that implementation, not a runtime test or a historical release
boundary; check the version in the image you operate.
- No already-mounted guard from
--onlyonce. The request parser stores it, but the executor never consults it. Inspect existing mounts before a repeat operation; this finding does not mean every repeat necessarily succeeds or creates an overmount. --fakecan create targets. Target preparation precedes the new-mount fake return. With either mkdir option it may write directories. It is not a zero-write preview or proof that a real mount's authority and filesystem checks will pass.- MODE is not target security. The creation helper
discards the requested mode and ignores the directory-creation result before
continuing path preparation. Do not rely on
--mkdir=0700for privacy. Establish and verify the target's native security separately; a parsed option is not a verified descriptor. - Failure can follow attachment. New-mount execution sets policy before attachment, but a subsequent propagation step can fail after the mount is attached. Retain diagnostics and inspect the live source, target, filesystem, writable state and policy before retrying. Do not infer an unchanged system from a nonzero exit status.
These gaps matter for startup integration:
--onlyonce is not an idempotence mechanism, and a failed or stopped command
is not by itself a verified detach or recovery procedure.
List mode #
With no operands, mount prints one line per mount, read from /proc/self/mountinfo:
SOURCE on TARGET type FSTYPE (OPTIONS)
mount -t TYPE with no operands filters the list by filesystem type instead of mounting; here type lists (-t ext4,xfs) and no<type> negation (-t nosysfs) are honoured. -l appends [LABEL] to each line when a label is known.
Details of the rendering: the option field is the positional VFS-then-superblock merge with a coalesced ro/rw, not sorted; mountinfo octal escapes are decoded; control characters in the mount point become ?; and the source is shown as the kernel recorded it, with /dev/loopN resolved to its backing file and /dev/dm-N to /dev/mapper/<name>. The listing shows only mounts the caller's token may observe; a partial or empty view is correct, not an error, and the paths of filtered-out parents are never reconstructed.
For a device-oriented view of what is available to mount, use lsblk.
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | Incorrect invocation or a permission denial — bad flags, mutually-exclusive verbs, a lone operand, an invalid policy= or --synth-sddl, an authorisation failure (EPERM/EACCES), an embedded NUL, or "no helper for type". |
2 | System error (out of memory, no free loop device, cannot fork). |
4 | Internal error (an invariant failure). |
8 | Interrupted (SIGINT), after signal-safe cleanup. |
32 | Mount failure — a syscall in the flow failed, a label/UUID had no match, a probe was undetermined, or a --beneath/move/--exclusive kernel refusal. |
126 | An external mount.<type> helper was found but failed to execute (moot until a helper ships). |
Code 16 (mtab) is never produced. Code 64 (some succeeded, some failed) only arises when several source arguments are given and at least one succeeds while another fails.
umount
Peios / Using Peios / Mount policies
umount detaches a filesystem from the Peios mount tree. It is the counterpart of mount and, like it, reads live mount state from the kernel (/proc/self/mountinfo) rather than any /etc/mtab — Peios has none. Each operand is resolved against that live state and unmounted with umount2(2).
umount [-lfRA] [-dr] TARGET|SOURCE...
Operands and how they are resolved #
Each operand is either a mount point or a source:
- If the (canonicalised) operand is itself a mount point in the current mount table, it is unmounted directly. This is always unambiguous.
- Otherwise the operand is treated as a source and matched against the sources in the mount table. If it is mounted in exactly one place, that place is unmounted. If it is mounted in several places,
umountrefuses with an error naming the candidates — resolve it by naming a specific mount point, or use-Ato unmount all of them.
If an operand matches nothing, it is "not mounted": normally an error (exit 32), but -g makes that a success and -q suppresses the message.
Several operands may be given in one invocation, and options may be interspersed with them (umount /a -f /b). Paths are handled as opaque bytes; an embedded NUL is a usage error. By default each operand is canonicalised; -c disables that and additionally selects UMOUNT_NOFOLLOW so the kernel does not follow a symlink in the final component.
Recursive and all-targets unmounts #
Two flags expand a single operand into multiple unmounts. Within one operand the expansion stops on the first failure.
-R/--recursiveunmounts the target and everything mounted underneath it, including any over-mount stack at a single point, ordered deepest-first.-A/--all-targetsunmounts every mount point of the given source in the current mount namespace. This is the live-mountinfo "unmount everywhere" complement to source resolution; it is not an fstab feature.-A -Rtogether compose: for each mount point of the source, recurse underneath it.
-R and -r are mutually exclusive (combining them is a usage error, exit 1).
Flags #
| Flag | Effect |
|---|---|
-l, --lazy | Detach the filesystem now and clean up references later (MNT_DETACH). |
-f, --force | Force the unmount (MNT_FORCE), e.g. for an unreachable server. |
-R, --recursive | Unmount the target and everything under it (see above). |
-A, --all-targets | Unmount every mount point of the given source (see above). |
-d, --detach-loop | After unmounting, free the backing loop device. Best-effort and verified: it clears the device only if it still backs the mount just removed, so a recycled loop number is never clobbered. Usually redundant, since mount-created loops auto-clear on unmount. |
-r, --read-only | If the unmount fails, remount the filesystem read-only instead (via mount_setattr). Mutually exclusive with -R. |
-g, --graceful | Exit 0 when the target is absent or not mounted (rather than failing). Applied unconditionally. |
-q, --quiet | Suppress "not mounted" messages. |
-c, --no-canonicalize | Do not canonicalise paths; selects UMOUNT_NOFOLLOW. Applied unconditionally. |
-v, --verbose | Say what is being unmounted. Repeatable. |
-N, --namespace NS | Enter mount namespace NS (a PID, an ns file path, or a named namespace) before reading its mount table and unmounting. All work happens inside that namespace. |
-n, --no-mtab | Accepted and ignored (no mtab on Peios). |
-i, --internal-only | Do not invoke a umount.<type> helper (none ship). |
--fake | Dry run: resolve operands and report, but skip the unmount syscalls. |
-h, --help / -V, --version | Standard. |
The umount2 flag mapping is direct: -l → MNT_DETACH, -f → MNT_FORCE, -c (or a symlinked final component) → UMOUNT_NOFOLLOW. MNT_EXPIRE is deliberately not exposed, matching util-linux.
On privilege #
As with mount, there is no coarse uid==0 check: the applet is not set-user-ID and every unmount is authorised per-operation by KACS. Where util-linux silently suppresses an option for non-root users (-c, and -g's effectiveness), Peios applies it unconditionally.
Exit status #
| Code | Meaning |
|---|---|
0 | Success (or a -g no-op on an absent target). |
1 | Incorrect invocation or a permission denial — including -R with -r, a missing operand, an ambiguous source, an embedded NUL, or an authorisation failure. |
2 | System error (e.g. cannot read the mount table). |
8 | Interrupted (SIGINT). |
32 | Unmount failed, or the target is not mounted (unless -g). |
64 | Several source/target arguments were given and at least one succeeded while another failed. |
126 | An external umount.<type> helper was found but failed to execute (moot until a helper ships). |
A single -R / -A / -A -R invocation stops on the first failure and yields 32; the aggregate 64 only appears across multiple top-level arguments. To see what is currently mounted before unmounting, use mount with no arguments or lsblk.
lsblk
Peios / Using Peios / Mount policies
lsblk lists the block devices on the system and their relationships — disks, their partitions, and the device-mapper and loop devices layered on top. It is the device-oriented companion to mount: where mount with no arguments shows what is mounted, lsblk shows what is available to mount and what filesystem sits on each device.
lsblk [options] [DEVICE...]
By default it prints a tree, one row per device with children indented beneath their parent. Given one or more DEVICE operands (a name like sda, a full path like /dev/sda, or anything resolving to a device node), it re-roots the output at those devices.
Where the data comes from #
Each column is sourced from one of four places, and any that cannot be read degrades to an empty cell rather than aborting the run:
- sysfs (
/sys/block) — the device tree, sizes, and the topology/hardware columns. Peios leaves/sys/blockunpatched, so this is the standard no-udev path. - libblkid — filesystem identity:
FSTYPE,FSVER,UUID,LABEL, and the partition-table columns (PTTYPE,PTUUID,PARTTYPE,PARTLABEL,PARTUUID). libblkid is opened at runtime, and it reads the device itself, so these columns are empty for anyone who may not open it: as shipped, a disk's node lets only SYSTEM and Administrators open it. - The device node's security descriptor —
OWNERandMODE, read the same wayls -lreads them. - The
/dev/disk/by-*symlink farm —ID-LINK. Populated by the device manager once it has run; there is deliberately no/run/udev/dataparser, so the column reads the links themselves and is empty in the initramfs, where no device manager runs.
Output modes #
The mode selects how the rows are formatted; it does not change which columns are shown.
| Flag | Mode |
|---|---|
| (default) | Indented tree. |
-l, --list | Flat list — the same columns, no tree glyphs. |
-J, --json | JSON. Flag columns render as bare booleans and MOUNTPOINTS as an array; children nest under a children key. |
-P, --pairs | KEY="value" pairs, one device per line. |
-r, --raw | Raw, space-separated. Values that could contain a space or control character are hex-escaped (\xNN) so fields stay parseable. |
-T, --tree[=COLUMN] | Force tree output even alongside -l, optionally attaching the tree glyphs to COLUMN instead of NAME. |
Columns #
With no column flag, lsblk prints the default set: NAME, MAJ:MIN, RM, SIZE, RO, TYPE, MOUNTPOINTS. Three flags swap in a preset, and -o names an explicit list (which wins over all of them):
| Flag | Column set |
|---|---|
-o, --output LIST | Exactly the comma-separated columns named (case-insensitive; an unknown name is a usage error). |
-O, --output-all | Every available column. |
-f, --fs | Filesystem view: NAME, FSTYPE, FSVER, LABEL, UUID, MOUNTPOINTS. |
-m, --perms | Permissions view: NAME, SIZE, OWNER, MODE. |
The available columns are NAME, KNAME, PATH, MAJ:MIN, FSTYPE, FSVER, LABEL, UUID, PTUUID, PTTYPE, PARTTYPE, PARTLABEL, PARTUUID, MOUNTPOINT, MOUNTPOINTS, SIZE, RO, RM, HOTPLUG, TYPE, OWNER, MODE, MODEL, VENDOR, REV, SERIAL, TRAN, HCTL, ALIGNMENT, MIN-IO, OPT-IO, PHY-SEC, LOG-SEC, STATE, ROTA, SCHED, PKNAME, and ID-LINK.
The OWNER and MODE columns #
OWNER and MODE describe the device node, not the filesystem on it, and they mirror ls -l exactly — there is no GROUP column and no POSIX permission bits, because access to a device node is governed by its security descriptor, not by a mode. OWNER is the owner SID (S-1-…); MODE is a three-character [type][x][+]: the device-type character (b for a block device, c for a character device), an x slot (never set for a block device), and a + when the DACL is inheritance-protected. When the SD cannot be read — for instance on a non-Peios host with no KACS syscalls — OWNER shows ? and MODE degrades honestly.
Filtering, sorting and shaping #
| Flag | Effect |
|---|---|
-a, --all | Include empty (zero-size) devices, which are hidden by default. |
-d, --nodeps | Do not print a device's holders or slaves (drop the children). |
-I, --include LIST | Show only devices with these major numbers (comma-separated). |
-e, --exclude LIST | Exclude devices by major number. The default is 1 (RAM disks); an explicit -e replaces that default, and -a clears exclusions entirely. |
-s, --inverse | Print dependencies in inverse order (holders above the devices they depend on). |
-M, --merge | Collapse a subtree shared by several parents (a multipath device) to a single occurrence. |
-E, --dedup COLUMN | Drop rows whose COLUMN value duplicates an earlier one. |
-x, --sort COLUMN | Sort siblings by COLUMN (numeric columns sort by value, not text). |
Formatting #
| Flag | Effect |
|---|---|
-b, --bytes | Print SIZE as an exact byte count instead of a human-readable value. |
-p, --paths | Print full /dev paths in the NAME column. |
-n, --noheadings | Omit the header row. |
-i, --ascii | Draw the tree with ASCII characters instead of box-drawing glyphs. |
-y, --shell | Render column keys shell-safe (MAJ:MIN becomes MAJ_MIN). |
-w, --width NUM | Truncate each table row to NUM columns wide. |
--sysroot DIR | Read sysfs, the mount table and /dev from DIR instead of / (chiefly for testing against a fixture). |
-h, --help / -V, --version | Standard. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | Incorrect invocation (an unknown column, a malformed major-number or width value) or a failed run (for example, /sys/block is unreadable). |
lsblk uses this single non-zero code, matching util-linux. A per-device read failure is not fatal: it degrades the affected columns to empty or ? and the run continues. A broken output pipe (piping into head, for instance) is treated as success.
Disks and filesystems
Peios / Using Peios / Disks and filesystems
Start with the disk's identity and current use. Partitioning and formatting can destroy data; a familiar device name is not enough to identify the target.
Compare size, model/serial where available, partition layout and mount points. Use Disk Manager for a desktop inspection, and Stable device names for references that must survive a reboot. Unreadable contents are unknown, not evidence that a disk is blank.
Where to start #
| What you need to do | Guide |
|---|---|
| Find software, configuration, service state or user data | Where files live |
| Inspect disks, partitions and mounts on the desktop | Disk Manager, currently inspection-only |
| Diagnose block or inode pressure without changing data | Find why a filesystem is full |
| Write or inspect a GPT | Partitioning |
| Create an ext filesystem with its root permissions already present | Formatting with security descriptors and mke2fs |
| Copy the live system onto bootable storage | Installing to disk |
| Record a filesystem, partition or hardware identity | Stable device names |
| Attach existing storage and choose missing-SD behaviour | Mount policies |
| Explain a file in a merged system directory | StrataFS |
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.
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.
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.
What ships #
| Package | Contents |
|---|---|
net.sourceforge.e2fsprogs | The tools below. |
net.sourceforge.e2fsprogs-devel | Headers, linker symlinks and pkg-config files for building against the libraries. |
net.sourceforge.e2fsprogs-static | Static archives. |
net.sourceforge.e2fsprogs-libext2fs, net.sourceforge.e2fsprogs-libe2p, net.sourceforge.e2fsprogs-libcom-err, net.sourceforge.e2fsprogs-libss, net.sourceforge.e2fsprogs-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. |
net.sourceforge.e2fsprogs-libuuid comes from e2fsprogs;
org.kernel.libblkid comes from util-linux. 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 #
The user-facing filesystem tools above are installed in /usr/bin, reached as /bin through the runtime view. See Install destinations for the broader package-layout policy.
The fsck dispatcher also uses type-specific checker names in /usr/libexec/fsck/, exposed through /libexec/fsck:
/libexec/fsck/fsck.ext2 fsck.ext3 fsck.ext4
fsck.fat fsck.vfat fsck.msdos
These backend paths are the dispatch interface: fsck picks fsck.<type> for the filesystem type it detects or is given. The checker can also have a direct operator interface: e2fsck remains in /usr/bin, and fsck.fat remains there with the fat, vfat and msdos backend names linking to it. This layout is explicit in the pinned e2fsprogs recipe and dosfstools recipe. 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 directly available in /usr/bin: Peios ships no mkfs front-end.
Why these tools are packaged, not rewritten #
Peios ships upstream e2fsprogs for ext2/3/4 and dosfstools for FAT. The on-disk formats remain Linux-compatible. e2fsprogs has Peios additions for security descriptors; dosfstools has no functional patch because FAT has no xattr channel in which to store one.
Filesystem creation and offline repair operate below the mounted access-control
layer. mount is where a filesystem enters KACS and receives its policy.
Peios-specific tools such as mount, lsblk and ls use that security model;
rewriting the formatters and checkers would instead duplicate upstream format
and repair logic without changing this boundary.
Where files live
Peios / Using Peios / Disks and filesystems
Use this map to find files and trace merged paths such as /bin and /etc
to the directories that actually store their contents.
The map describes the base filesystem in
dev.peios.fsbase at 92b0caf.
Images and local configuration can differ. Inspect the running machine before
assuming its layout or storage lifetime matches.
Choose where to look #
| What you are looking for | Location and purpose |
|---|---|
| Vendor-supplied software and defaults | /usr: executables in bin/sbin, helpers in libexec, libraries/modules/firmware in lib, shared files in share, defaults in etc/conf. |
| Local software and file-based overrides | /lcl: operator-managed trees such as bin, sbin, etc and conf. Local entries can hide vendor entries. |
| Files generated from registry configuration | /system/retc: reconciler output projected into /etc. Find the owning setting or component before editing its generated output. |
| Service data and identity stores | /var/state: persistent mutable state, including reserved services and protected secrets namespaces. See registry source paths for the machine's hives. |
| Caches, logs and crash data | /var/cache, /var/log and /var/crash. Check service configuration: recorded events and service output need not be text files here. Start with Event Viewer. |
| A person's files | /home: human home directories; services use configured state/runtime directories. Access depends on security descriptors. |
| Operator-managed bulk data | /data. Its name does not imply a separate disk or a backup policy. |
| Content served by this host | /srv: the host's served-content namespace. Check the service configuration for the actual path. |
| Software with its own directory layout | /opt. |
| Runtime sockets and service directories | /run, including /run/services and the /run/lock directory recreated by peinit at boot. Sockets and PID files are runtime resources, not saved service data. |
| Temporary working files | /tmp and /var/tmp; read the lifetime caveat below. |
| Boot inputs and generated artifacts | /boot holds the initramfs source tree and EFI-partition mount location; /system/boot holds generated initramfs output. Check that the intended EFI partition is mounted. |
| Additional mounted storage | /mnt and /media are available mount locations. Their existence does not attach a device. |
| Kernel and device interfaces | /proc, /sys and /dev. These expose runtime interfaces, not ordinary saved application data. |
A path does not grant access or prove that its data is safe to delete, backed up, or kept during installation. For space problems, use Find why a filesystem is full before deleting anything.
Trace a runtime path to its real file #
The base system uses StrataFS views for /bin, /sbin, /lib, /libexec,
/share, /include, /etc and /conf. Ordinary views put the corresponding
/lcl directory above /usr; the vendor stratum is read-only through that
view. This does not make the underlying /usr tree universally read-only.
Use the read-only inspector:
list shows the current mount namespace; resolve explains providers and write
routing without writing or granting permission. See Inspecting a stack
for interpretation and access limits.
/lib64 is different: the reviewed x86-64 base supplies a relative symlink to
usr/lib/x86_64-linux-peios, not a StrataFS view. The ordinary /lib view maps
the whole lib trees, including modules and firmware. These distinctions are
implemented by the
base recipe
and root-filesystem hook.
Find the owner of configuration #
Start with Find where a setting lives
and the component's regman documentation. A visible /etc file may be generated
from a registry setting; find that setting rather than changing generated output.
In the reviewed real-root hook, /etc has this precedence:
/system/retc: generated registry output./lcl/etc: local configuration, also the create stratum./usr/etc: vendor defaults.
An existing generated name wins over the same name in /lcl/etc. /conf has
only local and vendor tiers. The
initramfs hook
also uses only local and vendor tiers for /etc; do not assume the installed
root and the startup environment have identical views.
/lcl/policy is separate from ordinary file-based configuration. Its autorun
and registry-apply directories carry boot policy, not general-purpose storage;
see peinit’s Phase 1 reference
for their authority and execution rules.
Check temporary-file and boot-storage assumptions #
In the reviewed base setup, /tmp is a plain directory. The fsbase package's
temporary-directory policy
explains that it is disk-backed on an installed system and is not cleared at
boot or size-limited by that setup. A live image's volatile upper layer has a
different lifetime. Do not rely on a reboot to erase temporary files, or assume
that /tmp has a separate space budget. /var/tmp is intended for temporary
files that survive reboot; check the actual mount and local cleanup policy.
For boot files, the activation checklist distinguishes generated output, the intended EFI partition and the running kernel.
Stable device names
Peios / Using Peios / Disks and filesystems
Use a stable identity for a boot command line, saved mount reference or script.
Use the current kernel device name only after checking which device it names
in this boot: /dev/vda, /dev/sda and /dev/nvme0n1 depend on discovery order
and can change when hardware, controllers or probe timing changes.
- Inspect disks and partitions with
part listandlsblk. - Choose the identity that matches the object you mean: filesystem UUID, partition UUID or hardware identity.
- Match that identity back to the size, layout and current mounts before any destructive operation. A readable link identifies a device; it does not establish that its contents can be erased.
The links live under /dev/disk/ and point to the current kernel name:
| 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 choice, but labels need not be unique. A filesystem clone also carries its UUID; verify which device a UUID resolves to when copies may be attached.
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.
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.
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 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.
Find why a filesystem is full
Peios / Using Peios / Disks and filesystems
Start with the operation that failed, not with a directory to delete. Keep its exact error, path and time. A permission denial, read-only filesystem or I/O failure is not established as a space problem merely because a write failed.
This procedure inspects capacity and a selected directory tree. It does not delete data, repair or resize a filesystem, change mounts, or stop services. Use your current authorized access; incomplete visibility is a finding, not a reason to broaden privileges automatically.
1. Identify the affected filesystem #
Replace the example path below with a verified existing path involved in the failure. If the attempted new file does not exist, use its existing parent. Quote paths containing spaces.
After each command, retain its exit status and any diagnostics as well as the
report. df reports the filesystem containing
the named path; do not substitute the root filesystem's figures without
checking that it is the one involved.
- Record Filesystem, Type and Mounted on so later measurements refer to the same filesystem.
- Inspect Avail and Use% in the block-space report.
- Inspect the inode counts and free-inode figure in the second report. Inode exhaustion can prevent creating files even when block space remains.
- If the path or filesystem lookup fails, resolve that diagnostic before treating any other printed row as the answer.
These commands use the default no-sync behavior; they do not request a flush. If the reported capacity does not explain the failure, keep the original error and investigate that separately rather than starting cleanup by guesswork.
2. Look within a narrow, permitted tree #
Choose a known directory on that filesystem, such as the affected application's
identified data tree. Substitute that directory for /known/tree:
For suspected inode pressure, count entries instead of allocated space:
Pass the directory itself, not a shell * expansion that omits dotfiles. Start
narrowly and drill into one observed large directory at a time.
Keep stderr and the exit status for every run. A permission or traversal error can occur alongside printed totals; those totals are incomplete. Record files that disappeared or changed during the walk too. Missing observations are not proof that an unreadable directory is empty.
See du for the full options and status codes.
3. Interpret the measurements together #
df describes filesystem capacity. du measures what its walk can reach and
read in the selected tree. Their figures need not match:
- The tree may cover only part of the filesystem, and
-xskips other devices. - Access failures can hide contents from the walk.
- By default,
ducounts allocated blocks, not apparent file length; sparse files and block rounding affect that distinction. - Hard-linked files are counted once by default, and symlinks are not followed.
Do not treat a difference as proof of deleted-open files, quotas, or any other specific cause without a supported observation. Nor does a large total establish how many bytes a particular cleanup would make available.
4. Choose the component's supported remedy #
Identify the owner of the measured data before changing it. Use that component's documented retention, cleanup or recovery procedure, and check what it destroys. This guide supplies no generic deletion, database-file removal, WAL removal, forced-stop or resize recipe.
For event storage, retention settings control which records remain queryable. Deleting records logically does not prove that the physical database files immediately shrink. Preserve incident evidence before a separately authorized destructive retention change.
Evidence capture can itself fail under pressure: evctl needs writable space
and anonymous-temporary-file support in its temporary filesystem as well as
space at the final destination. A destination with free space is not sufficient.
Keep query diagnostics; see failed-query diagnosis
and the temporary-spool implementation.
After applying a separately chosen, supported remedy, rerun the same df
measurements and any relevant narrow du walk. Then check the original failed
operation separately: more free space alone does not prove the application can
write successfully.
Source and scope #
The command behavior was checked in peiosutils 3344d46: named-path filesystem
selection,
du traversal boundaries,
and counting versus printed depth.
This is a source-backed diagnostic workflow, not a runtime-tested cleanup or
capacity-expansion procedure.
Partitioning
Peios / Using Peios / Disks and filesystems
Use part list to inspect storage before making changes. Use part to create,
add or remove GPT partitions only after confirming the target whole disk
and the data you intend to discard. Peios boots through UEFI; part does not
manage MBR, resize or move partitions, or convert between MBR and GPT.
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.
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.
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.
After the table is written #
Writing a table changes the disk; it does not by itself change what the kernel believes is on the disk. So part asks the kernel to re-read the table, which is what makes /dev/vda1 appear — without it the partition exists on disk and nowhere else, and the mkfs you run next has no path to format.
The kernel refuses to re-read while any partition of the disk is open. Immediately after a write that is usually nothing lasting: publishing the new nodes is what prompts whatever probes new block devices to open one, so the refusal and its cause arrive together. part therefore keeps asking for up to three seconds, which outlasts a probe comfortably.
If the disk is still held when that time is up, part stops and says what is holding it:
# part add /dev/vdb --size max --type linux --name "Peios root" --yes
part: /dev/vdb: the table was written, but the kernel would not re-read it
within 3000ms: /dev/vdb1 is mounted on /mnt/rootfs. Release it or reboot
before formatting
When nothing of the disk is mounted, it reports that instead. The original
procedure says to run the command again in that case; the table on disk is
already correct and only the kernel's view is behind. Re-inspect the disk before
repeating a mutating command. This guide does not document a separate reread-only
command or establish that repeating add is idempotent. Do not format until the
kernel's partition view matches the written table.
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.
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.
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.
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.
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
For a new Peios system filesystem, give mke2fs a root security descriptor at
format time. Without one, the freshly-created root has no SD and normal access
under facs_deny_missing fails. A synthesis policy can make missing descriptors
usable, but is a separate choice about how the mount supplies permissions.
Before formatting:
- Identify the partition using Partitioning and Stable device names.
- Back up needed data and ensure the target is not in use. Formatting is destructive; it is not a repair for one missing descriptor.
- Choose the root ACL and its inheritance deliberately. A private home tree needs per-directory rules; a single root ACL cannot express every policy.
- If populating from a source tree, check its
user.peios.sdcarriers before invokingmke2fs -d.
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.
ID is INHERITED_ACE, so inspect the ancestor when tracing inherited rules.
The original guide also says it means the descriptor is derived rather than
stored. That conflicts with the population path below, which merges inherited
ACEs and writes the result to disk. Do not use ID alone as proof that an SD is
absent from storage; validate the stored descriptor separately.
Verify before mounting #
Inspect the unmounted ext filesystem's root xattrs:
debugfs -R "ea_list <2>" /dev/vda2
The mke2fs example shows the
expected security.peios.sd (96) entry for the exact descriptor above. Presence
on inode 2 confirms only that root xattr. Inspect child descriptors and intended
access after population as well; do not infer that every source node was stamped.
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.
Synthesised versus stamped #
Use ephemeral synthesis for genuinely missing descriptors when the media should not acquire security metadata, for example files on removable media or a read-only image. An NTFS volume from Windows may already carry valid stored SDs; those remain the input to access checks. A corrupt stored descriptor is denied, not replaced by synthesis.
For a missing SD, synthesis tries parent inheritance, then the mount template,
then the fallback. The result remains in memory under
facs_synthesize_ephemeral. Changing a template can change future synthesis
where that template supplies the inputs; it does not replace valid stored SDs
or automatically change the entire tree.
For a filesystem intended to be a Peios system, stamp its policy at creation
rather than relying on missing-descriptor synthesis. mke2fs accepts a root SD
and writes it onto the filesystem, so that root has a real descriptor when
mounted under facs_deny_missing with no synthesis template required. Verify
populated children separately under the rules above.
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 the Peios security-descriptor options and defaults. For a new system volume, choose the root ACL before formatting and verify it offline before mounting.
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. |
-E takes a comma-separated option list. Use root_sd_file when the SDDL
contains embedded commas, such as in a conditional expression, or when shell
quoting would be difficult. Multiple ACEs do not by themselves require commas;
the inline example below contains two ACEs.
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 #
This example grants SYSTEM and Administrators full control through inheritable ACEs. It is not the installer's broader readable-system-tree policy.
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.
Troubleshoot before retrying #
Invalid root SDDL:: inspect the exact descriptor or file content. Aroot_sd_fileis SDDL text with trailing newlines stripped; comments and extra content are not ignored.- A filesystem that will not mount: check whether a custom profile removed
Peios defaults or enabled the unsupported
quotafeature described above. - Root access denied: confirm that a descriptor was stamped and that the selected mount policy matches the filesystem's state.
- An SD-writing failure: do not assume the target's previous contents survived. The source groups this with failures where no filesystem is created, but the partial-write/rollback boundary is not documented. Inspect the target before deciding whether a destructive retry is appropriate.
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.
Disk Manager
Peios / Using Peios / Disks and filesystems
Open the launcher and type disk to inspect this machine's storage. Select a
disk to check its identity and partitions, Mounts to inspect current mounts,
or At Startup to review startup entries.
Before moving to a destructive command, compare the disk's device, model, serial, exact size, partition layout and mount points. Not Readable and Contents unknown are visibility limits; neither means the disk is empty.
A disk #
The disk page shows its model or virtual connection, size and device, with a GPT, MBR or System indicator. System identifies storage Peios is running from.
Use the bar or the list below it to inspect each partition. The bar is proportional to size: Linux/ext4 is blue, FAT orange, swap violet, and unformatted partitions grey. Striped areas are free space. Selecting a partition opens its page; selecting free space opens the add-partition form. The list also shows where filesystems are mounted.
This Disk gives the device, model, serial, connection, exact size, sector size and partition table. Removable disks have an Eject control. Erase Disk describes creating a new empty GPT and asks for confirmation, but this and all other change controls still end without changing storage.
A partition #
Check the three groups before choosing a command-line operation:
- Filesystem: type, label, UUID, space usage and the Format form.
- Mounts: mount points, missing-descriptor policy, and Mount or Mount Elsewhere.
- Partition: number, type, name, unique ID, start, exact size and Delete.
A mounted partition cannot be formatted or deleted until unmounted. Storage Peios is running from cannot be changed while it runs. Unavailable actions show why; those checks do not make currently unimplemented changes available.
The Format form describes ext4 or FAT32 with a label. For ext4, its initial
root permissions give SYSTEM and Administrators full control, everyone read
access, and creators ownership of objects they create, matching the installer.
Review that policy rather than assuming it matches the administrator-only
mke2fs example.
The Mount form describes a folder, read-only choice, missing-permissions policy and whether to mount again at startup. Submitting it currently changes none of these.
Files without permissions #
The UI labels map to mount policy classes:
| In Disk Manager | Policy | Meaning for a missing SD |
|---|---|---|
| Deny Access | deny-missing | SD-based access is denied until a valid descriptor is supplied. |
| Give Permissions, Not Saved | synth-ephemeral | Derive from parent/template and keep it in memory. No SD is written to the disk. |
| Give Permissions and Save Them | synth-persist | Derive an SD and schedule it to be saved. |
| Not Managed | unmanaged | FACS does not check this filesystem; the kernel's own per-operation checks still apply. |
A file brought from another system may lack a descriptor, and FAT has nowhere to store one. Filesystems without SD storage are not offered the persistent choice. A mount's Permissions… view shows the synthesis template used when its parent supplies none. StrataFS's policy and kernel-reserved unmanaged policies are fixed.
An ephemeral descriptor can remain cached while the inode is in memory; the UI's “each time it is opened” description should not be read as a promise of re-synthesis on every warm open. Corrupt stored descriptors are denied even with a synthesis policy.
Mounts #
Mounts groups filesystems on disks, shared folders, the system's root and StrataFS views, and the kernel's own filesystems. Select a mount for options and policy. StrataFS pages also list strata in highest-precedence-first order.
If a later mount covers this mount at the same point or above it, the page says so. Check this when a mounted filesystem seems to have disappeared from a path.
At Startup #
Added Here lists filesystems configured for startup, located by UUID so they can be found after moving to another port. Mounted by Peios shows system-managed startup mounts and cannot be changed here. The UI does not yet save changes to either list.
What you may see and change #
Disk changes are described as requiring the Manage Volumes privilege, held by Administrators. Other users get an inspection view with the limitation explained once and no change controls. No user can currently commit changes through Disk Manager because its service is missing.
As shipped, opening a disk to read its table and filesystems is limited to SYSTEM and Administrators. Other users still see kernel-listed partitions and mounted types from the mount table. Unavailable details are Not Readable or Contents unknown, never “not formatted”.
The Disk Manager guide describes policy reads as requiring Manage Volumes. Some command/SDK pages instead say TCB only; see the unresolved privilege discrepancy before diagnosing a policy-read failure.
Disk insertion/removal and mounts changed elsewhere should appear within a couple of seconds. After a command-line change, inspect the refreshed device, partition and mount state rather than treating a completed form as success.
Install, upgrade, or repair a disk
Peios / Using Peios / Disks and filesystems
Boot the Peios install medium to install a fresh system, upgrade a disk to the medium's release, or repair an existing installation. For writing a USB stick and configuring PC firmware, start with Install Peios on a PC.
A live system discards its writes at reboot. An installed system uses a writable filesystem on disk, so its changes persist.
Before you start #
- Back up the target disk before changing it, and identify it by model, size, and device name.
- Keep the machine powered and the medium connected while a job runs.
- Use a trusted network if you use the browser installer. On media that enable it, anyone who can reach the address can open the installer without signing in.
- Choose the operation that matches your goal:
| Goal | Operation | Effect |
|---|---|---|
| Replace everything on a disk with a new Peios system | Install | Erases the disk; first-boot setup creates the administrator account. |
| Keep an installed system and move to the medium's newer release | Upgrade an installation | Updates eligible packages and boot files; does not run first-boot setup again. |
| Restore boot files, check the root filesystem, or reseed its root descriptor | Repair an existing system | Runs the repair you select; it is not a reinstall. |
The current installer is installerd, with console and browser surfaces. The older peios-install script is a separate path: it copies the running system and its accounts. Do not assume the two paths have the same account or certificate behavior.
The installer #
Select and verify the disk #
- Choose the operation on the first page.
- Select a disk using the device, model, size, and bus columns. The boot medium is listed and marked but cannot be selected; other removable disks are marked too. Floppy drives are not listed.
- Use Rescan if the hardware has changed. If disks are missing, read any unclaimed-controller warning beside the table. It identifies the controller kind, PCI vendor/device, and bus location. RAID or VMD mode is a common cause; check firmware settings rather than selecting another disk by guesswork.
- Read the confirmation page. For installation, it names the disk, warns that all of it will be erased, and lists recognised systems and other filesystems containing files, with labels and used space. Back returns to disk selection with your choice retained.
- Start only when the disk and operation are correct. In the browser, hold down the installation button rather than clicking it once.
The confirmation uses a fresh read of the disk. EFI system and Windows recovery partitions are not separately called out, and uninspectable filesystems are not claimed to be empty. A disk that has been removed, or a script request naming the boot medium, is rejected with a reason.
Selecting and inspecting disks does not write to them. See how the scan stays read-only for the supported filesystems and safeguards.
Follow progress and check the result #
The progress page identifies the target, lists phases, and shows every command and its output. It asks for no input. Leave the machine and medium alone until it finishes.
Copy progress measures data written, rather than directory count. The installer estimates the image's size first, for example 1.2 GiB to copy, then checks filesystem usage several times a second. /usr is most of the copy. The estimate runs slightly low, so progress waits just short of complete until copying actually finishes.
Closing the form does not cancel the job. installerd owns the work. Another console or browser surface can attach to the same conversation and watch it. Both surfaces share choices, including the selected disk; coordinate with anyone else using them.
A successful install job reaches Installation complete. Reboot to start Peios. The conversation stays open on this finish page, which offers Reboot now and Back to the start. After the job completes, remove the USB medium before restarting so firmware boots the installed disk. If you land in the installer again, remove the medium and restart; do not reinstall.
In the reviewed installer source, choosing Reboot now ends the conversation only after the restart request is accepted. That is not confirmation that the machine restarted or booted successfully; check the installed system separately.
Check that the installed disk reaches first-boot setup, then complete it and sign in. The current installer copies the shipped image, so live-session accounts are not carried over. Editions without first-boot setup use their own account-provisioning seeds instead.
If a job fails #
- Read the failed phase and its command output. A failed job ends the conversation; it does not resume from that point. The next conversation begins at the first page.
- Do not treat failure as a rollback. Partitioning and formatting may already have changed the disk.
- If copying finished but boot setup failed, Repair boot files can complete that part of a half-finished install.
- If Reboot now fails, the finish page stays open and reports why. The daemon asks peinit to restart the machine, as
rebootdoes; the surface itself has no restart privilege.
Console controls #
| Key | Action |
|---|---|
| Tab / Shift+Tab | Move between fields, including disabled fields. The footer explains why a field is disabled. |
| Up / Down | Move through a list; at either end, move to the next field. |
| Enter | Press the highlighted action or advance from a field. |
| Space | Toggle a checkbox. |
| Esc | Ask before leaving. |
| F1 | Show key help. |
| Ctrl+L | Repaint the whole screen. |
A page focuses its first answerable field. The console form reads terminal size once at startup; first-boot setup uses the same renderer. See console sizing and output if the form is small or its display is damaged.
On Linux virtual terminals the renderer uses the kernel font's supported characters and sets the sixteen-colour palette. On a serial connection it assumes a modern emulator with rounded corners, ticks, and a spinner. --plain forces conservative characters for terminals that need them.
In a browser #
Open https://<machine-address>:7780 from another device, unless the image uses a different GXWI port. Browser installation requires installer-gxwi from dev.peios.installer-gxwi and a medium that enables it as GXWI's overlay. Such a medium opens the installer instead of a sign-in page.
The medium generates its own HTTPS certificate. Before trusting a browser warning, compare the browser's SHA-256 fingerprint with the one read at the machine's own console. The live medium's peios account needs no password:
Do not sign in or enter a password if the fingerprints differ. A page delivered through the connection you are checking is not an independent source for the fingerprint. See the machine's certificate.
installerd attempts to carry the medium's GXWI key and certificate into the
installed system from either surface. This is best-effort: a skipped or failed
transfer does not fail the installation. Existing browser trust can continue
only if the installed system presents the verified certificate and its other
certificate checks pass. If the fingerprint changes, verify it through the
installed machine's trusted console before entering credentials; see
certificate carry-over.
The older peios-install script, or a medium without GXWI, leaves the installed
system to generate its own key when GXWI starts.
After restarting, the browser page waits and follows the machine to setup, the desktop, or the installer if it booted from the medium again. Browser trust affects that waiting page: if the certificate was only bypassed at a warning rather than imported as trusted, the browser may show its own connection error instead. Reopen the address manually in that case.
Upgrading from the medium #
Use Upgrade an installation to move an existing disk to the release on the medium without a network or a fresh installation.
- Select the disk and compare the installed and medium releases on the confirmation page. Both show the edition and full package version.
- Check that Upgrade is enabled. It is enabled only for a newer version of the same edition, using peipkg's ordering including revision.
- Confirm, follow the phase output, and restart from the installed disk when the job completes.
A disabled button explains the mismatch: already current, a newer installed release, another edition, or no recognised Peios installation. Backing out leaves the disk unchanged. To inspect the release without writing to the target, the installer copies its package database to tmpfs and asks peipkg there.
The upgrade follows upgrade-peios on the mounted target:
- Upgrade the edition and its dependency closure, using the alternate-upgrade bypass for the medium's fixed, intentionally stale repository index.
- Stage the release's shared
autoapplyseeds withupgrade-peios --seeds-only. Install-only setup seeds are not repeated. - Upgrade other packages for which the medium carries a newer revision, in every root including the initramfs. Edition dependencies are floors rather than pins.
- Regenerate the boot files from the upgraded disk, as boot repair does.
The medium's repository is removed from the target whether the upgrade succeeds or fails. Operator customisations under /lcl are retained, with the generated boot command line under /lcl/etc/boot/ rebuilt as part of boot setup.
Repairing an installed system #
Choose Repair an existing system, select the installed disk, and choose one operation. Repairs target that disk, never the boot medium.
| Operation | What it does | Limit or next step |
|---|---|---|
| Repair boot files | Regenerates the command line under lcl/etc/boot/, initramfs image, and UKI on the ESP from the disk's kernel and initramfs tree. | It cannot repair a damaged kernel or initramfs tree; that requires reinstalling. |
| Check the filesystem | Runs e2fsck -pf on the root: forced checking with automatic preen repairs. | Refuses if anything on the disk is mounted. Unfixed problems require a shell and manual e2fsck. |
| Reseed security descriptors | Re-stamps the root directory's inheritable descriptor with the value used by the installer. | This is the root policy; it does not define a different policy for every subtree. |
Boot repair uses the installed disk-boot, not a possibly older copy from the medium, and leaves the installed repository list alone. For a half-finished installation whose copy completed but boot setup failed, it detects the medium-only package checkpoint or the remaining live-boot package and finishes the installation's package swap. The checkpoint also covers a failure after live-boot was removed. Packages declared temporary by the medium, including the DWE package on newly built DWE media, are removed; the checkpoint disappears only after that swap succeeds.
Filesystem checking reports whether the filesystem was clean, problems were fixed, or a reboot is required before reuse. Only problems it cannot fix automatically count as failure.
Each repair ends at the finish page. Use Back to the start to perform another repair in the same boot, or Reboot now when finished.
Two ways to invoke it #
This section describes the older peios-install script, not a command-line interface to the current installer conversation. Run this legacy script from a SYSTEM session; direct use from an administrator session is unsupported because its ordinary copy must read SYSTEM-only files and preserve their security metadata. For ordinary console or browser installation, use the current installer described above, whose privileged operations run in installerd.
The whole-disk form runs part to create a fresh GPT, a 512 MiB EFI system partition (ESP), and a root partition using the remainder. It then follows the two-partition form. The whole disk is erased.
A partition table not created by part is refused before formatting; --force explicitly overrides that protection. Do not use it merely to make an unexplained refusal disappear.
For another partition layout, create it with part, then supply the ESP and root partitions to the second form. Both named partitions will be formatted. The installer does not offer separate /home, swap, or encryption setup.
What it refuses before it starts #
Before formatting the named partitions, the script rejects a target that is not a block device, the same partition supplied twice, or either partition currently mounted:
# peios-install --yes /dev/vda1 /dev/vda2
peios-install: /dev/vda2 is mounted; refusing to format it
These checks prevent formatting a mounted running system. A refusal before the first mkfs has not formatted either partition; do not confuse that with a later failure after a whole-disk partitioning step has already changed the disk.
The first-account service is retired, the account is not #
The script copies the running system, including lpsd's account store at /var/state/lpsd/principals. It removes the lpsd-first-account service so that the live-image provisioner does not run against the populated store every boot:
This deletion is made in the live source registry, through the running registryd, before the copy and before either partition is formatted. The script verifies it; a deletion failure stops the install. If the key is already absent, it reports that and continues. Editing the source through its registry service avoids modifying an open database behind registryd's back.
Retiring the service does not remove or secure the copied accounts. Otherwise its non-idempotent lps add would fail on the existing name, and a provisioner that reasserted an image credential could overwrite an operator's later choice.
The current installerd path avoids this transfer: it copies the shipped image, omits the live development-account seed, and uses the edition's first-boot setup to create an administrator.
What the installer does #
This section explains the disk and boot artifacts so you can interpret failures. It is not a sequence to run on top of an installation already in progress.
1. Format the EFI system partition #
mkfs.vfat -F 32 creates the FAT ESP required by UEFI. FAT cannot store Peios security descriptors, so its access policy comes from a synthesising mount policy.
2. Format the root with a security descriptor #
The root is ext4, stamped at format time with the system's inheritable descriptor. The example target is /dev/vdb2; formatting it is destructive:
The root's security.peios.sd makes SYSTEM the owner, grants SYSTEM and Administrators full control, and grants Everyone read and execute. Execute also means directory traversal, so non-SYSTEM services can reach their working directories. The final CREATOR OWNER ACE is inherit-only and grants a newly created object's creator full control.
The live system uses the same root policy. Without the creator-owner ACE, a non-administrator could create a file it could not read back: an inherited non-empty DACL takes precedence over the token's default DACL. See Formatting with security descriptors.
3. Copy the system and preserve its security #
The older script uses cp -ax to copy the running root; the current installer takes the shipped image. cp -a requests owner, DACL, SACL, timestamps, links and extended attributes, but its recursive source-symlink path can suppress a returned copy error. Do not treat a successful status from that older copy step as proof that every link and required attribute was preserved. This limitation does not establish how the current installer reports a failed image copy.
The copy stays within filesystem boundaries. Mountpoints are recreated as empty directories: prelude mount-moves /proc, /sys, and /dev at boot, and StrataFS mounts views over /bin, /etc, /lib, and the other view directories. Their backing content in /usr and /lcl is ordinary root-filesystem content and is copied.
4. Write the kernel command line #
dev.peios.disk-boot supplies the stable template at /usr/share/disk-boot/cmdline. The installer appends root=UUID=<the new root> and writes the per-install command line to /lcl/etc/boot/cmdline on the target.
5. Build the boot artifact #
mkuki bundles the kernel, initramfs, and command line into EFI/BOOT/BOOTX64.EFI on the ESP. UEFI can boot this fallback path without an NVRAM entry. There is no separate bootloader or boot manager between firmware and kernel.
Use UUIDs, not device names #
The installed command line identifies the root by UUID because device names can change. A disk seen as /dev/vdb behind the install medium may become /dev/vda when the medium is removed.
The initramfs probes block devices directly to resolve the UUID. It cannot rely on /dev/disk/by-uuid, because no device manager has created those paths there. See Stable device names.
How the installed system boots #
The reviewed installer replaces the live boot components with the disk-boot
components before rebuilding the installed initramfs. It then verifies that
mount-root-disk.sh is present and that the live mount-root.sh is absent.
An installed image therefore does not select between both hooks at boot.
For disk boot, root= identifies the partition to mount; it is not a live/disk
mode switch. A missing or empty root= makes the disk hook fail rather than
silently leave the work to another hook. The disk hook mounts that partition
directly, without the live medium's writable overlay.
This follows the pinned installer checks and disk-root hook. Check the image you operate; these are source-level descriptions, not a boot repair procedure. For authoring and ordering, see Writing a boot hook.
The disk hook uses policy=deny-missing and does not run seed-sd. The installed filesystem already carries descriptors, so a missing one is an error to investigate. The live squashfs has none: it uses synth-ephemeral and seeds an inheritable descriptor on the tmpfs above it.
To inspect the installed root descriptor:
Check the owner and actual ACEs against the root policy above. ID means INHERITED_ACE; the root's explicit format-time ACEs should not carry it. The installed filesystem carries this access policy itself rather than relying on a mount-time template.
How the disk scan stays read-only #
installerd discovers partitions, used space, and recognised Peios, Linux, or Windows systems without writing to the disks. A read-only filesystem mount alone is not sufficient for ext4, which may replay its journal.
Instead, each inspected partition is bound to a read-only loop device, and that device is mounted. Ext3 and ext4 also use noload. The mount uses synth-ephemeral, allowing filesystems from other systems to be read without writing security descriptors to them.
FAT, exFAT, NTFS, ext2, ext3, and ext4 are inspected this way. Swap, encrypted volumes, and other filesystems are listed by size and partition-table type without opening them. Symbolic links on the inspected disk are never followed, because they could resolve against the install medium. The daemon supplies this detail to surfaces; the console disk page itself shows the table.
Installer services and image configuration #
installerd runs as SYSTEM and performs privileged disk operations. install-tui only draws the console form and has no privileges of its own. They speak MSIP, which also allows scripted interaction through msip-drive and surfaces attaching to an existing job. To study the driver, use Rehearse the headless installer in a lab, a source-derived synthetic dry-run exercise with a private socket. It is not a validated recipe for unattended installation on real disks.
For a browser-enabled medium, the installer-gxwi-overlay seed sets these values under Machine\Software\GXWI:
| Value | Setting |
|---|---|
OverlayUsername | peios, the medium's passwordless account |
OverlaySession | /usr/bin/installer-gxwi |
The edition must list this seed under live_autoapply, not autoapply. installerd deletes the live queue from the installed disk. That leaves the graphical installer program installed without making it the permanent browser entry point. If the seed is shared through autoapply, the installed machine keeps opening the installer; GXWI does not fall back from a requested overlay to sign-in.
What the installer does not do #
- Offer multiple automatic layouts. The whole-disk layout is one 512 MiB ESP and one root using the remainder. Separate
/home, swap, and encryption are not installer options. Use the older script's two-partition form only when you have deliberately prepared another supported layout. - Make every directory's policy the same as its intended use. The root descriptor supplies a baseline; a private subtree needs its own protected descriptor. Account home directories have that separate policy, created at first sign-in.
- Recover every failed system. Boot repair needs an intact kernel and initramfs tree; automatic filesystem checking cannot fix every error. Read the reported failure before choosing a destructive reinstall.
Where to go next #
Complete first-boot setup
Peios / Using Peios / Disks and filesystems
Complete first-boot setup after installing Peios to disk. It creates the first administrator account, sets the machine's name, and optionally applies a manual wired-network address. When it succeeds, you can sign in with your new account and setup does not run again.
The installer copies the system first; these questions are asked on the installed system's first boot. Setup is available on the console and, on images that include oobe-gxwi, in a browser.
Before you start #
- Boot from the installed disk, with the install medium removed after installation has completed.
- Choose an account name, a non-empty password, and a machine name. Their rules are listed below.
- Keep the install medium for recovery until you have successfully signed in.
- You do not need a network for console setup. For browser setup, use a wired connection on a trusted network and verify the machine's certificate.
- Expect an English setup interface and US console keyboard layout. Language and keyboard choices are made after setup, so take care when entering a password with a different physical keyboard layout.
What it asks #
1. Language and keyboard #
The page shows the current language and keyboard information but does not change them. Setup is in English and the console uses the US layout until setup finishes. Afterwards, open System Settings → Language & Keyboard. See the settings overview for the distinction between System Settings, Desktop Settings, and personal settings.
In the terminal, the Language and Keyboard choices are disabled, but each shows its explanation beneath it without needing focus, so this guidance is visible there too.
2. Network #
The network page is informational; lack of connectivity does not block setup. It shows what net status reports for each interface: connected, not connected, or not used. Choose Check again after connecting a cable. In the browser, the interface carrying your current connection is marked.
Wi-Fi joining is shown but disabled. Continue without network configuration, or choose Configure manually for a wired interface.
For a manual address:
- Enter an address with its network prefix, such as
192.168.1.20/24. - Add a gateway and name servers if needed. A gateway must be on the address's network.
- Save and check the network page's confirmation that the setting is kept for later.
Saving validates the values but does not change the active address yet. Setup applies the address after creating the account and setting the machine name, so the browser can keep reaching the current address while you answer the remaining questions. Note the new address before finishing.
3. Account and password #
This account is the machine's first administrator. Choose a password you intend to use for sign-in; the setup page refuses an empty password or mismatched confirmation.
Account names must:
- Be non-empty, plain ASCII, and no longer than 256 characters.
- Contain none of these characters:
@,\,/,:, or,. - Not be
Administrators,Users,Guests,Everyone, orAuthenticated Users. - Not be
peios-oobe-setup, the temporary account reserved for browser setup.
Spaces inside a name are allowed; spaces around it are trimmed. Validation happens when you choose Next, before the account is created. A rejected field explains what to change.
In a browser, typed account details remain in your browser until Next and are not shown to other people viewing the setup conversation. The page previews the account name, checks matching passwords, and rates apparent password strength from Easily guessed to Very strong. The strength meter is advice, not an acceptance rule: any non-empty matching password is accepted.
4. Machine name #
Accept the suggestion or enter one label with:
- Letters, digits, and hyphens only, up to 63 characters.
- No leading or trailing hyphen, dots, or spaces.
- A name other than
localhost.
Spaces around the name are trimmed. This is the name the machine uses for itself and supplies to DHCP. Domain joining is shown but disabled.
The browser validates as you type, previews a prompt such as jack@workshop:~$, and offers suggested names. Shuffle generates more suggestions. The daemon validates the name when you choose Finish.
Finish and verify sign-in #
Choose Finish once the account, name, and any manual network setting are correct. Setup shows each change as it applies:
- Create the account.
- Set the machine name.
- Apply a manually configured network address, if supplied.
On the console, setup releases the terminal and the login prompt appears. In a browser, Setup is complete is shown briefly before the page transitions to sign-in, retaining the same backdrop. Sign in with the account and password you just created.
Check that:
- The new account can sign in.
- The machine name is the one you chose.
- If you configured a manual address, the machine is reachable there from a device on the appropriate network.
- A later boot goes directly to sign-in rather than repeating setup.
For additional accounts and account administration, continue to Creating accounts.
In a browser #
Open https://<machine-address>:7780, unless the image uses a different GXWI port. The graphical surface must be installed at /bin/oobe-gxwi; without it, use the console.
While setup is pending, GXWI shows setup instead of its sign-in page. The console and browser share one conversation, so you can start on one and finish on the other. Coordinate with anyone else who has it open.
Verify the certificate before entering a password #
GXWI uses HTTPS with a certificate the machine makes for itself. Check its SHA-256 fingerprint against a trusted reading from the machine's own console:
Do not use a page delivered over the unverified connection as the source of that fingerprint. If the fingerprints differ, do not enter a password.
installerd attempts to carry the medium's GXWI certificate into the installed
system from either surface, but installation can succeed even if the transfer
is skipped or fails. Compare the certificate actually presented with the
fingerprint you verified on the medium; do not treat installation success as
proof that it is unchanged. A changed fingerprint needs a fresh check through
the installed machine's trusted console before you enter credentials. See
certificate carry-over.
The older peios-install path, or a medium without GXWI, does not carry that key across. If you have not already verified the installed machine and cannot obtain its fingerprint through trusted console access before creating an account, complete setup on the console first. Then sign in locally to verify the certificate before using browser sign-in. See the machine's certificate for the full trust procedure.
If the network address changes #
Setup keeps the current address until its final apply step. If you change the interface your browser uses, the page then checks both the old and new addresses and follows the machine when it answers. At the new address, the sign-in page starts a new backdrop rather than retaining the old page's animation.
If the browser cannot reach the new network, the page reports it; use the machine's screen to check its address. Open the new address from a device that can reach it.
The browser's waiting page also depends on certificate trust. If the certificate was only bypassed at a warning rather than imported, a restart can produce a browser error instead of an automatic reconnect. Reopen the address manually and follow the certificate guide.
The temporary setup account #
Before setup, the machine has no normal account for a graphical session. oobed creates peios-oobe-setup with no password and no groups, and its socket admits that account. The browser overlay uses it to draw the setup conversation.
On successful setup, oobed removes the overlay, restores GXWI sign-in, and removes the temporary account and its home directory. Failed setup leaves them in place for the next attempt.
The account-name page reserves peios-oobe-setup because setup preserves an account that already exists; choosing that name would otherwise leave no usable account after cleanup.
If setup does not finish #
| Symptom | Next step |
|---|---|
| Account, password, or machine name is rejected | Read the field error and compare it with the rules above. Validation happens before final application. |
| No network or Wi-Fi joining is disabled | Continue on the console; a network is not required. Use wired networking for browser access. |
| Browser stays on the old address or a starfield after a manual network change | Check the address on the machine's screen and open the new HTTPS address yourself. See the 2026.8 caveat above. |
| Browser setup is absent | The image may not include oobe-gxwi; use the console. If setup itself is absent, check the edition's provisioning choice below. |
| Setup was closed with Esc or failed | It is not retired. Restart to be offered setup again. A returned login prompt does not prove account creation succeeded. |
| Setup repeatedly fails and no usable account exists | Boot the install medium for recovery. Do not assume you can sign in to repair setup locally. |
| Console text is damaged | Press Ctrl+L to repaint. |
| Form no longer fits after resizing a serial-terminal window | The size was read once at startup. Restart with the desired terminal size, or use the current form size. |
It runs once #
Only a successful setup removes the oobed and oobe-tui service definitions. A failed or abandoned run leaves them so the next boot can retry. It does not retire the only setup path before an account has been successfully provisioned.
A machine whose setup keeps failing may have no account you can use. The install medium is the recovery route, as for a system that will not boot. Installer repair operations describe what the medium can repair and their limits; they are not a guarantee that every setup failure is automatically repairable.
How it gets the console #
This section explains service behavior useful when diagnosing startup or display problems.
| Service | Responsibility |
|---|---|
oobed | Runs as SYSTEM to create the first account and set the machine name. Serves the MSIP conversation on /run/oobed.sock. |
oobe-tui | Draws the console form without privileged rights of its own. |
Setup and the login prompt both use /dev/console. Setup has higher TTYPrecedence, so peinit gives it the console and does not start the login prompt at that point.
When the surface exits, whether successfully, through Esc, or after a crash, it releases the console. The login prompt's tty:released trigger then starts it. The terminal is not left without an owner, even though an unsuccessful setup may still leave no usable account.
How big the form is #
A serial byte stream carries no terminal geometry. The surface moves the cursor beyond the bottom-right corner and reads back its clamped position to discover the size. The installer uses the same renderer.
- The size is read once when the form starts. Later window resizing is not reported to the guest and does not resize the form.
- A terminal that does not answer falls back to 80×24, including captured output or a serial port with no responding terminal.
The console goes quiet while a form is open #
The daemon lowers kernel console output while the conversation is open and restores it when the conversation ends, including if the daemon dies. It does this in the privileged engine rather than in the unprivileged surface. installerd uses the same approach.
Only KERN_EMERG kernel messages continue to print, so a panic is not hidden. Peinit and other services can still write directly to the console. Those writes can disrupt the form, so page changes repaint it in full and Ctrl+L requests a full repaint.
Where it comes from #
dev.peios.oobe ships the inert oobe-service registry seed. An edition opts into it through install_autoapply, so it runs on the installed system rather than taking the console away from the live installer.
An image without dev.peios.oobe, or an edition that omits that seed, boots directly to a login prompt using whatever accounts its own seeds provisioned. Do not assume every custom image provides this setup flow.
A manual network choice is saved as an ordinary profile and rule: Profiles\default\manual-<interface> plus an exception under Rules\Interface\wired, naming the interface by stable ID. After setup, you can change or delete those settings like any other network profile and rule.
StrataFS
Peios / Using Peios / Disks and filesystems / StrataFS
Use stratafs to find which real file supplies a merged path, where a write
would go, and whether local changes hide a packaged default. Start with the
read-only inspector before removing an override or changing a stack.
StrataFS presents ordinary directories as one view while leaving their real paths independently manageable. Highest precedence wins for each name. Directories merge; a non-directory provider masks lower objects of that name, including a lower directory's subtree.
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. |
Check permissions and unexpected results #
- If an inspection fails, check access to every participating real directory. The inspector runs with your rights and fails rather than presenting an incomplete protected view. It does not elevate your token.
- If deleting a local entry exposes a packaged file, that is the documented
no-whiteout behaviour. Use
resolveanddiffbefore removing the override. - A routing answer is not permission to mutate.
write: copy_upstill needs the actual KACS checks and a usable create stratum. - If a direct stratum path works but its merged path is denied, check stored SDs. StrataFS's fixed deny-missing contract does not use a lower mount's synthesis policy to repair a missing descriptor. This conflicts with the general stacking description in SD storage by filesystem.
- For a copied-up file, check preservation rather than assuming ordinary creation inheritance. The StrataFS copy-up contract preserves the source's full descriptor; ordinary new creations inherit. A descriptor-preservation failure fails the copy-up.
For kernel errors and recovery conditions, use StrataFS failure modes. For example, a copy-up made stale by a changed provider requires reopening; an open that succeeds does not guarantee the first write will succeed.
A /bin example #
Suppose /usr/bin contains packaged programs and /lcl/bin is for local
changes. The command below mounts a new stack at /bin; it is a configuration
example, not an inspection command. Check the current stack first and do not
replace a running system view merely to try it out:
/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 dev.peios.fsbase-stratafs-mount-hooks 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 \.
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.
Managing file security
Peios / Using Peios / File permissions
Use sd to inspect selected file policy and make bounded changes to its owner, access rules, audit rules or integrity label. Start with one known path and one intended change. A permission failure is a reason to diagnose the denied operation, not to replace the descriptor or grant full control.
1. Inspect the path and current policy #
These commands only inspect the example file; replace ./report.txt with the intended path:
Record the owner, group, displayed DACL entries and their order, inheritance flags, and label information. Keep the output as a before-change record. sd show queries owner, group, DACL and the label subset, not the full SACL; --all and --sddl do not add audit entries. Its output is not a complete descriptor backup or a tested rollback input. Human and JSON summaries also omit some ACE payload details. If the change requires inspecting audit policy or other omitted data, resolve that verification gap before writing.
A named symlink follows its target by default. To inspect or change the link itself, use the documented --no-follow-symlinks (-P) flag consistently. Confirm which object you mean before writing.
sd check rehearses an access decision without performing the operation. Its result is evidence for the selected token and descriptor; it does not prove that an application's original operation will work. If inspection fails or leaves necessary policy unknown, resolve that visibility gap before changing it.
2. State the intended change #
Choose the smallest component and scope that express the policy. The command reference has the complete syntax.
| Intended result | Documented command family | What to check first |
|---|---|---|
| Add an allow or deny ACE | sd allow, sd deny | Principal, exact rights, ACE order and inheritance flags. --replace removes existing rules for that principal and kind before adding. |
| Remove a principal's DACL rules | sd remove | It removes every allow and deny rule for each named principal. Removing a deny can increase access. |
| Change owner or primary group | sd owner, sd group | WRITE_OWNER and the new-owner SID restrictions below; ownership does not grant file-data access. |
| Change audit rules | sd audit, sd unaudit | SACL authority. unaudit removes every SACL rule for the named principals. |
| Change an integrity label | sd integrity | Intended mandatory policy, WRITE_OWNER and the caller's integrity constraint. Do not lower a label merely to make a denial disappear. |
| Change inheritance or refresh children | sd inherit, sd reset, sd propagate | Whether explicit child rules or protection should survive; inspect the affected descendants first. |
| Replace a reviewed descriptor or selected components | sd set | The full replacement and its recovery path. --components selects which owner/group/DACL/SACL parts to write; do not use a whole-descriptor replacement for a one-ACE change. |
sd reset drops explicit rules and protection and rebuilds the DACL from the parent. sd remove --allow-empty permits a present-but-empty DACL, which grants no rights through its ACE list. An absent DACL has very different, permissive semantics. Neither is a routine way to fix an unexplained denial; see DACL evaluation.
Inheritance is stored on the child. Changing the parent's rules does not update existing children automatically; propagation is a separate, intentional operation.
3. Make the bounded change #
For example, if the approved policy is to add read access for the identity running sd on this one file:
@self means the current caller's user SID, not the file owner and not the user of another application. Choose the intended principal explicitly. This adds an ACE; it does not remove earlier denies or bypass mandatory policy. A later allow cannot change a right already decided by an earlier matching ACE.
Run only the command matching the intended change. Avoid concurrent edits to the same descriptor. With an existing valid descriptor, component selection preserves unselected fields, but a read/edit/write sequence does not reserve the policy against another writer. The kernel storage contract describes last-writer-wins behavior and a cache/xattr publication window, not an end-to-end transaction.
4. Verify policy and the original operation #
After the change, inspect the same object again and compare it with the before-change record:
Check the intended principal, rights and unrelated policy within the returned view. Matching sd show output cannot verify audit ACEs, omitted payloads or complete descriptor equality. Audit-policy changes need a separately established full-SACL inspection path; this guide does not supply one. Then retry the original operation with its actual caller. Check both access that should succeed and access the policy should still refuse, where it is safe to do so.
Existing file handles keep the rights granted when they were opened. A DACL edit is not a revocation of those handles; use a fresh open to test future access. See The handle model.
A nonzero sd status can mean a failed operation, an unreachable path, or a denied sd check; read the diagnostic. After recursive work, inspect the reported failures and representative descendants rather than treating the whole tree as changed. If the result differs from the intention, stop widening the change and use the denial guide.
Why the xattr layer is denied #
Do not edit the backing security.peios.sd or system.ntfs_security xattr. Raw reads, writes and removal are denied. Use sd, or the component-aware SDK interface when writing a program; this keeps validation and the separate SACL gate intact. See File Descriptor Storage.
Rights and constraints #
| Component | Reading needs | Changing needs |
|---|---|---|
| Owner or primary group | READ_CONTROL | WRITE_OWNER; new-owner SID validation also applies. |
| DACL | READ_CONTROL | WRITE_DAC |
| SACL | ACCESS_SYSTEM_SECURITY | ACCESS_SYSTEM_SECURITY, gated by SeSecurityPrivilege. |
| Integrity label only | READ_CONTROL | WRITE_OWNER; without SeRelabelPrivilege, the new label cannot exceed the caller's integrity. |
Requests for several components must pass every required check. MIC, PIP and other access layers still apply; owner implicit rights are not an exception to all of them.
Without SeRestorePrivilege, a new owner must be the caller's user SID or a token group carrying SE_GROUP_OWNER. Being the owner or an administrator does not itself prove the operation can pass WRITE_OWNER. See Changing ownership.
A label-only change preserves non-label SACL entries; a full SACL write replaces the whole SACL. The integrity constraint applies through either path: a label above the caller's integrity needs enabled SeRelabelPrivilege, even for a full SACL write. SeSecurityPrivilege or SeRestorePrivilege alone does not remove that constraint. Existing mandatory resource attributes cannot be removed or modified without SeTcbPrivilege.
For programs, the SDK guide covers kacs_get_sd, kacs_set_sd, mutually exclusive SACL/label requests, the 65,535-byte validation limit and component merging. Operator changes should use the component command rather than construct a binary descriptor.
What about file mode (the POSIX rwx bits)? #
chmod changes mode metadata, not the DACL. FACS does not use the ordinary rwx bits to authorize access; the execute bit still matters as an exec prerequisite. Use sd to change the access policy.
fchmod()needsWRITE_DACin the fd's granted mask; otherwise it fails withEACCES(EBADFfor anO_PATHfd). Legacy opens request this as a compatibility right.chmod()runs a fresh check forWRITE_DAC.- Neither operation changes the descriptor.
See Use-Time Checks for the syscall behavior.
Errors #
If a change is denied, check the required component right, the caller, owner-SID constraints, integrity and other mandatory policy before retrying. A read-only mount can refuse a write even if the descriptor permits it. Do not infer that a failed multi-object run left every object unchanged.
The SDK error reference distinguishes access, validation, buffer and path errors for programs. For an operator, keep sd's diagnostic and follow Debugging a denial.
See also #
- The sd command — exact command syntax and flags.
- Ownership — owner-transfer rules and recovery limits.
- Securing files — SDK examples and the moved raw-interface detail.
- Caller-supplied descriptors — the kernel reference for supplying an SD at creation; see its documentation discrepancies before using the developer examples.
Package management
Peios / Using Peios / Package management
Use peipkg to install, update, remove and inspect package files. Use feat when you need to set up or turn on a feature supplied by a package. Both run as you, and neither gives you additional authority.
Where to start #
| What you need | Start here |
|---|---|
| Find, install or remove software | Installing and removing packages |
| Apply updates or reverse a committed change | Keeping a system current |
| Add or repair a package source | Repositories and trust |
| Understand an unexpected plan or a separate approval | Dependency resolution |
| Check files or identify their owner | Inspecting and verifying |
| Recover after a failed or interrupted operation | Transactions and recovery |
| Choose which package supplies a shared name | Claims |
| Work on an offline installation or initramfs | Named roots |
| Turn a feature on or remove its setup | Using feat |
| Assemble a new image's package tree | Composing a root |
For desktop work, open Package Manager
from the launcher by typing package, or
Feature Manager by typing feature.
What a package is #
A .peipkg file carries a package's identity, version, architecture,
dependencies and payload. peipkg records the installed package and the files
it owns, so it can inspect, replace, remove and verify those files.
Installing a package makes its files available. A feature performs additional setup through scripts you explicitly run, such as registering or enabling a service. Package installation does not run those scripts. Remove a feature's setup before removing the package that supplies it.
A package can also provide a claim, a shared filesystem name with one holder. That chooses a provider; it is not the same operation as enabling a feature. Archive and manifest internals belong in the technical reference.
Where packages come from #
Repositories supply package files and signed metadata. peipkg refresh
updates the cached metadata used to plan installs and updates. Before adding
a repository, obtain its signing-key fingerprint through a trusted channel;
configuration alone is not trust.
A local .peipkg file
can also be installed. Its format and payload hashes are checked, but there
is no repository trust chain authenticating it. You are vouching for that file.
peipkg has no authority of its own #
peipkg is an ordinary program running under your token. It has no privileged daemon, setuid identity, service account or broker. The system checks your rights against the security descriptors on the paths it changes and the state it reads or writes.
As shipped, package administration is for Administrators. Granting write access to package destinations is broader than permission to install one approved package: peipkg cannot provide that narrower permission for you. A package's requested security descriptor cannot give its installer authority the installer did not already have. See the privilege model for the boundary.
Every change is a transaction #
peipkg shows a plan before applying a change. Review every dependency,
removal, source, version and target root, then read the final outcome and
warnings. --yes skips only the routine approval; it does not approve the
separate questions for elevated actions or modified configuration files.
Use undo or downgrade for a completed change you want to reverse, and
recover for a pending interrupted transaction. They do not restore feature
setup, registry state or user data. The technical reference documents
rollback failures and recovery limits, so do not treat an error or a
rolled-back history entry as proof that every file was restored.
Verify after recovery.
Every operation is audited #
Package operations and deliberate authorisations produce semantic events in the audit stream. There are documented exceptions: automatic recovery emits no peipkg event, and event emission can fail without stopping a package change. The audit reference describes these limits. The kernel's audit of the actual file operations is the security record; peipkg's events are a useful summary.
The command surface #
Every command is invoked as peipkg <command> [arguments].
| Command | Does |
|---|---|
install | Install packages, with dependencies, from a repository or a local .peipkg file. |
remove (alias uninstall) | Remove installed packages. |
upgrade | Update installed packages under the available sources and constraints. |
downgrade | Move one package to a specific older version. |
undo | Reverse the most recent committed transaction as a new change. |
claim | Show or change which package holds a claim — a shared name that exactly one package may own. |
refresh | Update the cached metadata of the configured repositories. |
repo | Configure repositories — add, list, remove. |
root | Manage named roots — add, list, remove, show. |
list | List the installed packages. |
info | Show one installed package's details. |
files | List the files a package owns. |
owns | Report which package owns a given path. |
search | Search the configured repositories for a package. |
verify | Check installed files against what was recorded at install. |
history | Show the transaction log. |
recover | Recover pending work; cross-root recovery may roll forward. |
clean | Delete unused metadata-cache data; see the documented cache-scope limits. |
One global option sits before the command: --root TARGET makes peipkg operate on a Peios installation other than the running system at /. TARGET can be a literal path — a Peios installation mounted at DIR, the form used by image builders and offline maintenance — or the name of a named root. Named roots are more than a convenience: they are how peipkg keeps components such as the initramfs current. See Named roots for the detail.
The producer side #
Building and signing packages is pekit's
job; publishing repositories is peipkg-repo's.
See the Producing packages
reference for that work. peipkg-compose is a separate consumer-side binary
for assembling a fresh root, not a peipkg subcommand or a live-system updater.
Package Manager
Peios / Using Peios / Package management
Package Manager is the desktop's window on peipkg. Open it from the
launcher (type package). It has no authority of its own: it runs
peipkg as you, so what you may install or remove is exactly what you
could with the command, and every change is the same transaction, with
the same history and audit record.
Sections #
| Section | Shows | Does | The command |
|---|---|---|---|
| Installed | Every installed package, searchable, with its version and where it came from | Opens one: its details, what it needs and provides, and its files. Check Files, Update to…, Remove… | list, info, files, verify, upgrade <name>, remove |
| Available | What the repositories offer that isn't installed | Install, and Install from File… for a .peipkg file | search, install |
| Updates | The newer versions waiting, after refreshing the trusted repositories | Update one, or Update All | refresh, upgrade --dry-run, upgrade |
| History | Every change, newest first, by what it did | Undo… the most recent, and Resolve an interrupted one | history, undo, recover |
| Repositories | Each repository, its address, how many packages it offers, when it was refreshed, and whether it is out of date or not yet trusted | Refresh All, Add Repository…, Trust…, Remove… | repo list, refresh, repo add, repo remove |
| Roles | Each claim, what holds it, and its links | Choosing another package moves the claim to it | claim |
| Installation Roots | The named roots | Nothing: roots are added with peipkg root | root list |
Making a change #
Before removing a package that supplies a configured feature, inspect the feature in Feature Manager and use its Remove… action while the definition and scripts are still installed. Check that the feature reaches Not Installed, then return to Package Manager and review the package-removal plan below.
If teardown fails or is interrupted, keep the package installed, retain the script output and follow Recover an interrupted feature. If the feature's definition is already missing, follow When its package is removed first.
Every change goes the same way, on one page:
- The plan. What will be installed, updated, moved back or removed, with sizes, before anything happens.
- Each elevated action, on its own. A downgrade, a low-trust package filling a high-trust role, or a package taking over another's files needs your specific approval. Each is asked with Allow This, and Cancel is the keyboard's. Your approval is recorded in the audit stream, as it is at a terminal.
- The plan's own question: Install, Update, Remove or Undo. For a removal, Cancel is the keyboard's.
- Progress, through downloading and checking, preparing, putting the changes in place and recording them.
- The outcome, with anything peipkg warned of under Notes.
A removal can stop partway to ask about a configuration file you changed since it was installed: Keep It leaves it in place, belonging to no package; Delete It removes it and keeps a copy beside it; Stop Everything cancels the removal.
Once you approve the plan, the change finishes even if you close the window.
When peipkg refuses #
Some refusals have a way on, which Package Manager offers once:
- A repository's information is out of date, and refreshing it
brought nothing newer. Continue Anyway carries on with what this
machine already has, as
--allow-staledoes, and is recorded. Package Manager remembers it for its update checks until you close it. - A file is in the way: one the change would install is already there
and belongs to no package. For an install, Replace It authorises
replacement and keeps the displaced file, as
--overwrite-unowneddoes. Other commands have no overwrite option: move the conflicting file aside, then retry the operation. - This is updated another way: the package's publisher says it is
updated by other means. Update Anyway is
--bypass-alternate-upgrade. - A repository isn't trusted yet: it is set up on this machine, as an
image sets up its own, but its trust has never been confirmed.
Repositories takes you to it, where Trust… shows the signing key
given for it and runs the trust ceremony when you confirm, as
peipkg repo add <name>does.
Package Manager never confirms a repository's trust for you, including when it refreshes repositories to look for updates: it refreshes only those already trusted.
What you may do #
Package Manager asks the system rather than guessing. If you may not read peipkg's records, every section says so once and shows nothing more; as shipped, only Administrators may. If you may read them but not change them, everything is shown and nothing can be changed, with the reason said once under each section's heading.
Where to go next #
- Installing and removing packages
- Keeping a system current
- Repositories and trust
- The driven mode, for a program of your own that runs peipkg the same way
Installing and removing packages
Peios / Using Peios / Package management
Use peipkg for package files and feat for the
setup a package's features perform. Installing a feature's package makes its
scripts available; it does not turn the feature on. On the desktop, use
Package Manager.
Before changing software #
- Check that you are working on the intended root.
The default is
/; a package'sdefault_rootcan redirect an install when you have not specified--root. - You need access to peipkg's records and authority to write the affected paths. peipkg runs as you and grants no additional rights.
- Use a repository whose signing-key anchor you have verified through a trusted channel. See Repositories and trust.
- Keep an independent copy of important local changes before replacing or removing files. Transaction recovery and version undo have documented limits.
Install and check a package #
For example, to install GNU Grep from a configured, trusted repository:
peipkg refresh
peipkg search grep
peipkg install org.gnu.grep --dry-run
peipkg install org.gnu.grep
peipkg info org.gnu.grep
peipkg verify org.gnu.grep
Review the source, versions, dependencies, removals, claims and target roots
in the plan before approving. A dry run does not reserve its inputs: review
the plan again when applying, especially after refreshing metadata or
another package change. info shows the installed version and origin;
verify checks files, not whether the application is running or configured.
If the operation fails or is interrupted, use the recovery procedure before treating the system as restored.
Installing packages #
peipkg install <package|file.peipkg>...
Each argument is either the name of a package to fetch from a configured repository, or the path of a local .peipkg file (recognised by its .peipkg suffix). You can mix the two in one command.
Canonical package names #
Use the complete canonical name returned by peipkg search. Current catalogue
commands do not resolve a short final component such as grep to
org.gnu.grep. Virtual capabilities remain unqualified and are a separate
dependency mechanism.
Canonical names use reverse-DNS namespaces, for example org.gnu.bash,
com.amd.amd-ucode and org.peios.peinit. Third-party software retains its
upstream namespace; Peios-owned software uses org.peios. A name identifies
the software, not who signed or endorsed the package. Check its repository
origin separately.
The catalogue package-family reference retains the full name list, split-library and development packages, included commands, unsupported components, and build/trust qualifications.
Installing a package rarely means installing just that package. peipkg works out everything the request implies — the dependencies the package needs, and the dependencies of those in turn — and presents the whole set. How that set is computed is the subject of Dependency resolution; this page is about the flow around it.
The plan-and-confirm flow #
install, remove, and the commands on Keeping a system current all work the same way. peipkg first produces a plan — the ordered list of changes that satisfy your request — and prints it:
$ peipkg install nginx
the following changes will be made:
install pcre2 10.44
install zlib 1.3.1
install nginx 1.27.4
proceed? [y/N]
For repository packages, presenting this plan does not download or install their payloads. peipkg waits for an answer. Anything other than y or yes — including pressing Enter, or end-of-input — is a refusal, and the package plan is not applied. This is the package command's outcome; a wrapper such as plain upgrade-peios can still continue with its own registry-seed work.
Answer y and peipkg carries the plan out as a single transaction: it downloads and verifies every package, then applies the change. Read the outcome and any warnings; after an interruption, follow the recovery procedure.
| Option | Effect |
|---|---|
--dry-run | Produce and print the plan, then stop before its approval questions and package application. Planning can still refresh repository metadata for trust-freshness checks. |
--yes, -y | Skip the proceed? prompt and apply the plan. |
--no-claim | Install a provider without taking any claim it offers. |
--allow-stale | Proceed although a repository's trust state exceeds its maximum trusted age. See Repositories and trust. |
--claim <names> | Comma-separated claims to force-claim, overriding the current holder(s). |
--claim-all | Force-claim every claim the installed packages provide, overriding incumbents. |
--dangerously-bypass-path-restrictions | Permit packages that declare special_system_package to install outside the payload layout rules. Exempts nothing that has not declared itself special. The technical sources disagree on whether /lcl/policy remains protected under the bypass; do not rely on that exclusion (see the warning below). Needed only for the handful of packages whose job is to lay down the filesystem structure those rules protect. |
--dry-run previews the package plan without applying it. In
peipkg 8b588ae8,
trust-freshness checks precede planning, and the
dry-run return
is before plan authorisation and execution. Do not treat it as a guarantee
that cached metadata or all filesystem state is untouched. Check the source
scope against the version in your image.
--yes is for scripts and unattended runs — but note that it skips only the routine prompt. A plan that contains an action needing deliberate authorisation will still stop and ask; --yes does not override that. See Elevated authorisation for which actions those are and why.
--claim-all cannot be combined with --claim or --no-claim. Claims — shared names exactly one package may hold — are covered in Claims.
An install can also target a root other than the current one — either explicitly with --root, or because a package declares its own default root. See Named roots for how roots are named and nested.
Installing from a local file #
When an argument is a path ending in .peipkg, peipkg installs that file directly:
$ peipkg install ./nginx-1.27.4.peipkg
This is a raw install, and it differs from a repository install in one specific way: it skips the repository trust layer. There is no signed index to check the file against, no signing key to verify it under, and none of the freshness or rollback protection a repository provides. You are vouching for the file yourself.
Everything else still happens. The package format is fully verified: the archive structure, the manifest, the integrity manifest, and the hash of every payload file are all checked before anything is staged. A corrupt or truncated .peipkg is rejected in the same way as one from a repository. The file's dependencies still resolve normally against your configured repositories — a locally-installed package can pull in repository packages to satisfy what it needs.
A package supplied as an explicit local file takes precedence over any repository's version of the same package, so install ./foo.peipkg installs that file even if a repository offers foo too. In the plan, a local-file operation is marked so the choice is visible:
install nginx 1.27.4 (local file)
There is currently no policy gate that refuses raw installs as a class. Format and hash checks establish integrity, not the file's publisher or trustworthiness.
Removing packages #
peipkg remove <package>...
peipkg uninstall <package>...
remove and uninstall are the same command. Each argument names an installed package; peipkg plans the removal — the files to take off disk — and runs the plan-and-confirm flow described above.
Before removing a package that supplies a feature, inspect and remove that
feature with feat first. Removing the package does not undo the feature's
setup and takes away the scripts needed to undo it. If that already happened,
restore the feature definition
before removing the feature.
Inspect the package and preview the removal:
peipkg info <package>
peipkg files <package>
peipkg remove <package> --dry-run
peipkg remove <package>
peipkg list
peipkg history
A removal releases the package's files and claims. Shared or non-empty directories remain; empty directories owned by no remaining package can be removed. If the package held a claim, no alternative is promoted automatically.
Keep or remove local changes #
For a modified configuration file under /usr/etc/ or the legacy /etc/,
peipkg asks whether to remove it, keep it, or abort:
- Remove: the prior content is kept at a sibling backup path.
- Keep: the file stays but becomes unowned.
- Abort: the removal stops. End-of-input also aborts.
--yes does not answer this per-file question. Binaries, libraries and data
are not hashed at uninstall, so a hand-patched binary can be removed without
a question. Use verify and preserve anything you need before approving.
The uninstall reference describes
these rules and database-integrity warnings.
Removing something that is depended on #
peipkg will not, by default, leave the system inconsistent. If you ask to remove a package that another installed package depends on, the plan is refused: peipkg tells you what still needs it, and stops.
| Option | Effect |
|---|---|
--cascade | Also remove every installed package that depends on the ones named. |
--dry-run | Print the plan and stop. |
--yes, -y | Skip the proceed? prompt. |
--cascade turns that refusal into a wider plan: peipkg computes the full set of packages that would be left with a broken dependency and adds them to the removal. The plan then shows everything that will be removed. Review it before approving, because a cascade can reach further than expected. There is no implemented system-critical-package guard: even removing the package manager is an ordinary removal. Do not use a cascade as a shortcut around a dependency you have not identified.
$ peipkg remove --cascade libfoo
the following changes will be made:
remove toolA 2.1
remove toolB 1.0
remove libfoo 3.3
proceed? [y/N]
Exit status #
| Code | Meaning |
|---|---|
0 | The operation succeeded — or, for --dry-run, the plan was produced. A declined prompt is also 0: nothing failed. |
1 | The operation failed — a package was not found, a dependency could not be satisfied, a download or verification failed, or a file operation was denied. |
2 | A usage error — an unknown command or a malformed option. |
Keeping a system current
Peios / Using Peios / Package management
For routine maintenance, refresh, review the upgrade plan, apply it, then
check the outcome. First choose the intended root
and preserve important local changes. upgrade can also update nested roots,
so review the whole plan and check every participating root afterwards.
peipkg refresh
peipkg upgrade --dry-run
peipkg upgrade
peipkg history
peipkg verify
Read refresh failures before proceeding: a dry run and the final invocation can use different inputs if metadata or installed packages changed in between. After the change, review verification differences and configuration-file warnings, and check the affected software separately. A file check does not establish that a service or feature is working. For kernel and service updates, follow Check what an update has activated to distinguish installed payloads, boot artifacts and running code.
| Your situation | Command |
|---|---|
| Apply available updates | upgrade, after refresh and plan review |
| Reverse the most recent committed transaction | undo, after checking history and its dry run |
| Restore one package to a particular older version | downgrade <package> <version> |
| An operation was interrupted and is pending | recover, then verify |
Version reversal changes package payloads. It does not reverse registry state, runtime or user data, or setup performed by feature scripts.
For the Peios edition itself, use Upgrading Peios.
Its plain CLI also reconciles release seeds after a no-op or declined
package request; peipkg's exit 0 is not a cancellation signal to that
wrapper. Use upgrade-peios --check to preview without staging or applying
seeds.
Refreshing repository metadata #
peipkg refresh [repository]...
peipkg plans against a local, verified copy of each repository's metadata. That copy does not update itself. peipkg refresh is what updates it: for every configured repository — or just the ones you name — peipkg fetches the current signed descriptor and index, verifies them, and replaces the cached copy.
$ peipkg refresh
refreshed "official"
refreshed "internal"
Refresh has two properties worth knowing:
- Repositories are independent. If one repository is unreachable or fails verification, peipkg reports it and carries on with the rest. One bad repository never blocks a refresh of the others.
- A failed refresh changes nothing. If a repository cannot be refreshed, peipkg keeps the metadata it already had. It never falls back to unverified or stale-but-unchecked content. The worst a failed refresh does is leave you on yesterday's view.
What "verified" means here — signing keys, freshness floors, the handling of an unsigned repository — is the subject of Repositories and trust.
Run refresh before an upgrade. An upgrade plans against cached metadata, so without a recent refresh it cannot consider newer releases that the cache does not contain. The selected version still depends on the request, source preferences and constraints.
Skip it for long enough and peipkg stops waiting for you: an install, upgrade, or downgrade against a repository whose trust state has passed its maximum trusted age (30 days by default) refreshes that repository itself, and refuses to proceed against one that stays stale — unreachable, or frozen on an unchanging index — unless you pass --allow-stale. The bound and its configuration are covered in Repositories and trust.
Upgrading #
peipkg upgrade [package]...
With no arguments, upgrade considers every installed package in the
current root and its reachable named roots for a newer version that satisfies
resolution. With package names, it considers those packages in each reachable
root where they are installed, including any new dependencies they need.
--no-recurse confines resolution and execution to the current root.
$ peipkg refresh && peipkg upgrade
the following changes will be made:
upgrade zlib 1.3.1 -> 1.3.2
upgrade nginx 1.27.4 -> 1.27.5
proceed? [y/N]
upgrade uses the same plan-and-confirm flow as install — peipkg shows the full set of changes and waits for approval — and the same options:
| Option | Effect |
|---|---|
--dry-run | Print the plan and stop. A good way to preview what an upgrade would move. |
--yes, -y | Skip the proceed? prompt. |
--no-recurse | Confine the upgrade to the current root only — disable the cascade into nested named roots. |
--allow-stale | Proceed although a repository's trust state exceeds its maximum trusted age. Warned and audited. |
By default, upgrade resolves one combined plan over the current root
and its reachable named roots. One approval covers that plan. If it changes
more than one root, peipkg coordinates the participants as a cross-root
transaction, not independent per-root upgrades. Preparation failure attempts
rollback; a later commit failure can leave some roots committed and others
pending. Read the named-root transaction boundaries
and cross-root recovery
before treating a failed operation as unchanged. This describes
peipkg 8b588ae8's upgrade entry
and single approval/executor selection;
check the revision supplied by your image.
If the plan contains no updates, peipkg says so and exits. That is a result against the available metadata and constraints, not proof that every upstream release is installed.
Downgrading #
peipkg downgrade <package> <version>
downgrade moves one package to a specific older version. You name the package and the exact version you want:
$ peipkg downgrade nginx 1.27.4
Older versions are not in a repository's active index — that index lists current versions only. They live in its archive index, which peipkg fetches on demand when you ask for a version that is not current. The package must still exist in some configured repository's archive for the downgrade to be possible.
A downgrade is treated as a deliberate move. Going backward — onto a version that may have known issues a newer one fixed — is one of the actions peipkg flags for explicit authorisation: beyond the routine proceed? prompt, peipkg asks you to authorise the specific downgrade, and --yes does not stand in for that answer. See Elevated authorisation for the full set of actions that work this way.
downgrade accepts --dry-run, --yes, -y, and --allow-stale with the same meaning as elsewhere.
Undoing the last change #
peipkg undo
Before applying undo, identify the most recent committed transaction:
peipkg history
peipkg undo --dry-run
undo reverses the most recent committed transaction. If that transaction installed a package, undo removes it; if it upgraded, downgraded, or removed packages, undo restores each one to the version it had before.
$ peipkg undo
undoing transaction 47 (upgrade nginx, zlib)
the following changes will be made:
downgrade nginx 1.27.5 -> 1.27.4
downgrade zlib 1.3.2 -> 1.3.1
proceed? [y/N]
Check that the old packages are available before relying on undo. The technical reference describes archive-based resolution requiring a reachable repository or usable cache; it does not promise retained transaction backups. Even where undo is exempt from freshness gating, that does not guarantee it can fetch an old version offline.
Be precise about what undo is. It is not a rollback of committed state — the previous transaction happened and stays in the history. undo computes the inverse of that transaction and applies it as a new transaction of its own. The history grows; it does not rewind. That new transaction can itself be undone, and so on.
Because restoring an older version is a backward move, undo carries the same explicit-authorisation requirement as downgrade for any package it walks back. It accepts --dry-run and --yes, -y.
To undo something other than the most recent transaction, or to see the history undo works against, use peipkg history — and revert a specific package directly with downgrade.
The routine cycle #
For day-to-day maintenance the loop is:
$ peipkg refresh # learn what is available
$ peipkg upgrade --dry-run # preview the move
$ peipkg upgrade # apply it
downgrade and undo are the recovery commands — use them when an upgrade has brought in a change you want to remove. They apply a new package transaction. Read the recovery limits before relying on another reversal, and verify after a failed or interrupted attempt.
Exit status #
| Code | Meaning |
|---|---|
0 | The operation succeeded — including a dry run, a plan with nothing to do, and a declined prompt or authorisation (nothing failed). |
1 | The operation failed — a repository could not be refreshed, a repository's trust state exceeded its maximum age and a forced refresh could not clear it, resolution or a download or verification failed, cross-root preparation, commit or recovery failed, there is no committed transaction to undo, or a command's arguments were wrong (downgrade without a package and version, an unparsable version, a malformed option). |
2 | A usage error before any command ran — no command, an unknown command, or a malformed global option (including a --root reference that does not resolve). |
Repositories and trust
Peios / Using Peios / Package management
Before adding a repository, obtain its base URL and signing-key fingerprint through a channel you trust. The fingerprint is your trust decision; peipkg cannot tell you whether the publisher deserves that trust.
Choose the repository task #
| Task | Command or check |
|---|---|
| See configured sources | peipkg repo list |
| Inspect trust and freshness | peipkg repo list --json |
| Add a new source | peipkg repo add <name> <base-url> --anchor <fingerprint> |
| Trust configuration already supplied by an image | peipkg repo add <name> after checking that configuration |
| Fetch current metadata | peipkg refresh <name> |
| Stop using a source | peipkg repo remove <name>; installed packages remain |
After an add or refresh, check the result and preview the package operation that will use it. Check package origin in the plan; a trusted signature authenticates a publisher's content, not its safety or suitability.
When a repository blocks work #
- Not trusted: inspect the configured URL and anchors, then deliberately
run
repo add <name>if they are the source you intend to trust. - Signature or anchor mismatch: verify the URL and fingerprint through the publisher's trusted channel. Do not switch to unsigned mode to make the error disappear.
- Unreachable or stale: retry refresh when the source is reachable. Only
use
--allow-stalewhen you deliberately accept older metadata; it is warned and audited. - Unusable cached metadata: the cache reference says to refresh or remove the repository from configuration. Another failure chapter describes warning and continuing without it. Because the sources disagree, inspect the resulting plan's repositories instead of assuming the same candidate set was used. See The index cache and An unreachable repository.
Removing a repository is a deliberate source change, not a neutral retry: it removes trust state and the recorded freshness floor as well as the configuration. Do not remove and re-add it merely to bypass a rollback warning.
What a repository serves #
At its base URL a repository serves three signed documents:
- A descriptor — the repository's identity: the set of signing keys it uses, each with a status. This is the root of trust for everything else.
- An active index — the catalog of current package versions: names, versions, dependencies, hashes, and download locations.
- An archive index — the catalog of older versions, kept so a downgrade can reach them. peipkg fetches it only on demand.
The descriptor and the indexes each carry a detached signature. peipkg verifies all of them; an unverifiable document is discarded, never used.
Adding a repository #
peipkg repo add <name> <base-url> --anchor <fingerprint>
repo add does two things: it records the repository in your configuration, and it runs the trust ceremony that anchors it.
$ peipkg repo add official https://pkgs.peios.org \
--anchor ef86709c4b1d8a02e5f3c719d640aa8b7c2e9105f8d3b6470a1c2e9d8b5f3a04
added repository "official"
The --anchor value is a signing-key fingerprint, and it is the part of the command that establishes trust. peipkg cannot tell you whether a repository is genuine; only you can, by obtaining its fingerprint through a channel you trust (the project's website over HTTPS, a colleague, a printed reference) and supplying it here. The anchor is your out-of-band statement that the key is trusted.
Given an anchor, the ceremony runs:
- peipkg fetches the repository's descriptor and its signing keys.
- It checks the fetched keys against the anchors you supplied — a fingerprint must match bit for bit.
- Only if the descriptor's signature verifies against an anchored key does peipkg accept it and record the trust state.
If the fetched descriptor does not verify against any anchor, repo add fails and leaves nothing behind — no half-added repository, no configuration file. You can supply --anchor more than once to trust several keys at first contact.
| Option | Default | Effect |
|---|---|---|
--anchor FINGERPRINT | — | A signing-key fingerprint to trust. Repeatable. |
--priority N | 50 | Resolution priority — a lower number wins. See below. |
--policy required|optional | required | Whether a valid signature is required. See Signature policy. |
--min-index-version N | 0 | A freshness floor for the index. See Freshness. |
--max-trusted-age-days N | 30 | How stale the repository's trust state may grow before operations force a refresh. See Freshness. |
--max-index-staleness-days N | 90 | How old the index's own generated_at may be before operations force a refresh. See Freshness. |
--insecure | off | Permit a plain http:// base URL. |
--insecure exists for a repository on a trusted local network with no TLS. It lowers transport confidentiality and integrity; the package signatures still protect authenticity, but prefer https:// whenever it is available.
Adding a repository your system already carries #
An image can ship a repository's configuration — its base URL, its policy, and its trust anchors — as a .repo file in /conf/peipkg/ (stored at /lcl/conf/peipkg/, which is what that view exposes). A Peios installation medium does exactly that for the offline repository it carries.
Configuration alone is not trust. peipkg does not trust a configured repository on sight: until the ceremony has run there is no recorded trust state, and peipkg neither uses the repository nor carries on without it. An install, upgrade or removal is refused, naming the repository, until you add it or remove it from the configuration: going on with the others would let a lower-priority repository's package quietly take the place of one this repository was meant to supply. The decision to trust a key is yours, never a default.
What a baked-in .repo file changes is only where the anchor comes from. It arrived with the image rather than being typed at the prompt, which is still out-of-band — it did not come from the repository it authenticates. So the ceremony can run against it without you retyping a 64-character fingerprint that is already on disk:
$ peipkg repo add peios-medium
added repository "peios-medium"
Given a name and nothing else, repo add reads that repository's existing configuration and runs the same ceremony as the two-argument form. It is still an explicit act — nothing here happens on its own.
Two differences from the full form are worth knowing:
- It refuses a configuration with no
trust_anchorsunder therequiredpolicy, because there would be nothing to verify the descriptor against. - A failed ceremony leaves the
.repofile alone. The two-argument form wrote that file and removes it again on failure; here it came from somewhere else, and deleting another party's configuration because a ceremony failed would turn a retryable problem — a medium not mounted yet, a repository not published — into lost settings.
How trust survives key rotation #
A repository operator will, over time, rotate signing keys — retiring an old one, bringing a new one into use. If trust were pinned permanently to the fingerprint you first anchored, every rotation would break every consumer.
Rotation does not break trust, because the descriptor carries the keys. Each key in the descriptor has a status:
| Status | Meaning |
|---|---|
active | In current use for signing. |
transitioning | Being phased in or out — honoured until its stated expiry. |
revoked | Withdrawn — never honoured again. |
Each time peipkg refreshes a repository, it verifies the new descriptor against the keys in the descriptor it already trusts, and then adopts the new descriptor's key set as the current trust state. A rotation that is itself signed by a still-trusted key propagates automatically: you anchored once, and the chain carries forward from there without you re-anchoring. A revoked key is dropped and never accepted again.
The anchor therefore matters only at first contact. After that, trust is a chain, and each refresh extends it.
Signature policy #
--policy chooses how strict peipkg is about signatures for this repository.
required(the default) — every descriptor and index must carry a valid signature from a trusted key. An unverifiable document is rejected. This is the correct setting for any repository reached over a network.optional— combined with no trust anchors, this puts the repository into unsigned mode: peipkg fetches and uses its metadata and packages without cryptographic verification.
Unsigned mode is a deliberate escape hatch for a scratch repository on a build host, or a local mirror you fully control. Every operation that touches an unsigned repository prints a warning:
peipkg: warning: repository "scratch" is unsigned — its metadata and
packages are not cryptographically verified
In unsigned mode, only the transport stands between you and a substituted package. Do not use it for anything reachable from an untrusted network.
Priority #
--priority is a number: lower is stronger, and the default is 50 for
all repositories, including the official one. Set it deliberately when you
configure multiple sources. It is a selection preference and a comparison
used by trust guards; it is not a restriction on what a repository can publish.
The detailed candidate-selection rules place repository priority before version in their ordered preferences; other operator descriptions previously presented version first. Review the actual plan rather than assuming either the newest build or one particular source must win.
A lower-priority repository using replaces to displace a package from a
higher-priority one needs explicit authorisation. The resolver also guards
certain low-trust virtual providers. See Elevated authorisation
for the separate questions and their limits.
Freshness — defeating a stale-repository attack #
A signature proves a document is authentic; it does not prove the document is current. An attacker who cannot forge a signature can still serve you a genuine, correctly-signed, but old index — one from before a security fix was published — and a naive consumer would accept it.
peipkg closes that gap by tracking, per repository, the highest index version it has ever seen, and refusing any index that goes backward. Once you have seen version 5 of an index, version 4 — however well signed — is rejected. A repository can only ever move forward.
--min-index-version N lets you set that floor explicitly, out of band: if you know the current index is at least version N, anchoring that number means peipkg will reject anything older even on the very first fetch, before it has a history to compare against.
Going backward is one half of the attack; standing still is the other. A frozen repository — one that serves the same, correctly-signed index forever — never trips the rollback check, yet it can hold you on a view from before a security fix indefinitely. Against that, peipkg tracks when each repository last refreshed with progress and enforces a maximum trusted age: 30 days by default, tunable per repository with max_trusted_age_days in its .repo file or --max-trusted-age-days at add time.
When an install, upgrade, or downgrade finds a repository's trust state older than its maximum, peipkg refreshes that repository first. If it cannot — the repository is unreachable, or it refreshes without progressing — the operation is refused rather than planned against outdated metadata. Passing --allow-stale to the operation overrides the refusal; the override is warned about and recorded in the audit stream. A maximum above 180 days draws a warning on every operation, because a bound that loose effectively disables the check.
The operator interface also documents a maximum index staleness measured
from the index's generated_at: 90 days by default, configured with
max_index_staleness_days in the .repo file or --max-index-staleness-days
at add time. This is intended to catch a publisher advancing the version
while retaining an ancient generation timestamp.
The two documented settings are independent: raising one does not widen the other — a max_trusted_age_days of 180 still leaves the 90-day staleness window in place. The operator interface describes a forced refresh for an index past the window, the same warned and audited --allow-stale override, and a warning on every operation when the staleness bound is above 365 days. The implementation conflict above applies to those expectations too.
Listing and removing repositories #
peipkg repo list
peipkg repo remove <name>
repo list prints the configured repositories — name, base URL, priority, signature policy. With --json it also gives each repository's trust state: name, base_url, priority, signature_policy, trust_anchors, allow_insecure_transport, trusted (whether the trust ceremony has run), last_refresh and index_generated as RFC 3339 times, stale (whether either is past its maximum age), and packages, the number its cached index offers.
$ peipkg repo list
official https://pkgs.peios.org priority=10 required
internal https://pkg.corp.example priority=20 required
repo remove deletes a repository's configuration and the trust state peipkg recorded for it. Packages already installed from that repository stay installed — removing a repository controls where future packages come from; it does not remove past ones. Check their origin with list and info, and review any future source
substitution. The cross-repository guard reference
describes packages whose origin has disappeared as orphans, including named
upgrade refusal when no configured source offers them. Do not infer that
removing a source also removed or replaced its packages.
Where the configuration lives #
Each repository is one file: /lcl/conf/peipkg/<name>.repo, in flat TOML.
= "https://pkgs.peios.org"
= 10
= "required"
= ["ef86709c4b1d8a02e5f3c719d640aa8b7c2e9105f8d3b6470a1c2e9d8b5f3a04"]
The files are hand-editable, and editing one is a legitimate way to configure a repository — a trust anchor written into the file is you supplying that anchor out of band. peipkg repo add is the convenient front end: it runs the ceremony and writes the file for you. Who may edit these files is, like everything else on Peios, the security descriptor on /lcl/conf/peipkg/.
A repository's allow_sd_overrides setting defaults to false. The
configuration reference
documents refusal of packages declaring file security descriptors unless
you explicitly permit that repository to supply them. This permission is a
separate trust decision; it does not give peipkg rights beyond your account.
Exit status #
| Code | Meaning |
|---|---|
0 | The operation succeeded — the repository was added, listed, or removed. |
1 | The operation failed — the trust ceremony failed (a newly created configuration is removed; a pre-existing .repo file is retained), or the subcommand was missing, unknown, or given the wrong arguments (a missing repo add <name> <base-url>, a malformed option). Removing a name that is not configured is not a failure. |
2 | A usage error before any command ran — no command, an unknown command, or a malformed global option. |
Transactions and recovery
Peios / Using Peios / Package management
After an interrupted or failed package operation, inspect its history,
recover pending work, and verify what is on disk before making another change.
Use undo or downgrade only when you mean to reverse a committed change.
| What you see | What to do |
|---|---|
| A pending transaction after an interruption | Run peipkg recover, then inspect history and verify files |
| A committed update you want to reverse | Review undo or downgrade |
A rolled-back record but damaged or missing files | Follow failed-rollback checks; history alone is not proof of restoration |
| A committed change with a maintenance warning | Read the warning; a failed post-commit side effect does not undo the package change |
| An interrupted feature script | Use feat recovery; package recovery does not reverse feature setup |
Recovering an interrupted transaction #
Use the same root as the failed operation. If it was an offline or named
root, put --root TARGET before each command below.
peipkg history
peipkg recover
peipkg history
peipkg verify
- Record the transaction id, state, affected packages and error. Do not delete staged files or backups before recovery has had a chance to use them.
- Run
recover. A pending single-root transaction is rolled back; if none is pending, the command says so. A committed transaction has only cleanup left and is not reversed byrecover. - Read the result and inspect history again. Recovery can itself fail.
- Verify the affected packages, or all packages as above. Review each
difference against intentional edits and any
.peipkg-newfile.
Ordinary install, upgrade and removal commands attempt recovery of pending
single-root work before starting new work. Explicit recover lets you deal
with the interruption first and produces a recovery audit event; the automatic
path does not. Read-only package queries remain available without waiting
for the transaction lock. A second writer reports transaction in progress;
wait for that operation to finish before trying another change.
Across more than one root #
A multi-root upgrade has one combined plan and coordinated execution; it is not a series of independent per-root upgrades. See Named roots for the preparation and sequential-commit boundaries. Preparation rollback can fail; in the reviewed source that path retains a pending journal. Do not treat it as the older closed-journal failure described elsewhere on this page.
For recovery:
- Retain the original named-root topology and make every participating
root present and reachable. Run
peipkg recoverexplicitly from the same system anchor, using the original--root TARGETif one was selected. An ordinary single-root operation refuses pending cross-root work in isolation. Recovery discovers reachable roots; it does not prove that a missing or unregistered participant has been found. - Read the outcome. If no participant committed, pending participants are rolled back. If a participant committed, pending participants are rolled forward using persisted completion data. Missing or malformed data causes refusal; a later recovery step can fail after another root was reconciled.
- Inspect history and verify files in every participant. In this source,
the CLI's rolled back cross-root transaction line is printed after
either backward or forward reconciliation. It is not proof of rollback;
the forward path records
rolled forward: cross-root recoveryin the transaction. Keep the diagnostics and treat any refused or unreachable root as unresolved. - Review maintenance separately. Roll-forward recovery applies persisted
package metadata and attempts backup cleanup, but does not invoke the
normal post-commit side-effect runner. Do not assume it re-ran
depmodorman-db. Use the invocation reference to identify the affected root and kernel release, and verify the needed maintenance before relying on that system.
This follows peipkg 8b588ae8's
CLI root discovery and report,
coordinated recovery decision,
and roll-forward implementation.
It is a source-level contract, not evidence that recovery or maintenance
succeeded on a particular machine. Check the revision supplied by the image.
The technical recovery chapter
keeps the implementation context.
When recovery does not restore the files #
The failure reference
retains an earlier description of a closed-journal failure after I/O
errors, a filesystem becoming read-only, or permissions changing. If that
is the state you actually find, another recover can find nothing to retry.
Do not assume all failed rollbacks have that outcome: the reviewed
cross-root preparation rollback reports errors
and retains the affected pending journal. Inspect the transaction and tool
version before choosing the recovery path.
- Use
peipkg verifyto identify recorded files that differ or are missing. - Inspect the affected paths for staged or backup siblings, and keep the evidence while deciding whether the old or new content is wanted.
- Establish why writes or restoration failed before attempting repair.
- Reconcile affected files and package records manually where necessary.
There is no
reinstallverb: the reference describes removal and installation as two transactions, or a version change and change back. Review dependency, feature and claim consequences before removing anything. - Verify again after repair. A clean file check does not test services, feature scripts, registry state or user data.
The indeterminate-state reference
describes proposed handling beyond the available history, recovery and file
verification tools; it does not establish a forensic-report command or a
recover option to accept the current filesystem as authoritative. Its older
blanket recovery wording must not override the coordinated cross-root path
above. A closed journal and a pending cross-root journal require different
responses.
The transaction log #
peipkg history
history prints the transactions peipkg has carried out, most recent first — each with an id, a timestamp, its state (committed, rolled-back, or pending), and a short summary.
$ peipkg history
49 2026-05-19T14:02:10Z committed upgrade nginx, zlib
48 2026-05-19T09:31:55Z committed install nginx
47 2026-05-18T22:14:03Z rolled-back install brokenpkg
| Option | Effect |
|---|---|
-n N | Show at most N transactions. -n 0 shows all of them. The default is 20. |
--json | Emit JSON: for each transaction its id, state, started_at, summary, and operations, each an action (install, upgrade, downgrade, remove or claim), the package name, and its from and to versions where they apply. |
The history is what undo reads to find the most recent transaction, and what ties a stray *.peipkg-backup-<id> file back to the operation that created it.
What an interruption leaves behind #
| Name | Meaning |
|---|---|
<name>.peipkg-staged-<id> | Incoming content staged beside its destination |
<name>.peipkg-backup-<id> | The displaced original, renamed beside its destination |
The names have no leading dot and carry the transaction id shown by
history. They are recovery material, not ordinary cache files. Do not
remove them just to tidy a directory after an interruption. A committed
transaction can also leave cleanup behind after a crash; use its recorded
state and the recovery reference
to distinguish the cases.
Configuration files on upgrade #
Package defaults live under /usr/etc/; /etc/ is the merged configuration
view. Modified-file protection also recognises legacy package paths under
/etc/. It does not make /etc/ a current package destination.
When a default is unchanged since installation, an upgrade can replace it.
When you edited it, peipkg preserves your file and writes the new default as
<name>.peipkg-new, with a warning. Compare and merge the changes deliberately.
The new sibling is unowned and is not removed by uninstall. The original
path can continue to appear in verify because its recorded hash is now the
new package's hash; that report alone does not mean your preserved edit is
corruption. See Configuration files.
Side effects run after commit #
A package can request a standard maintenance operation from a closed set; it cannot supply arbitrary install-time scripts. Those maintenance steps run after commit. A failure is reported as a warning, and the package change stands. Read which step and root failed, then use the invocation and retry rules for that step. A later transaction that requests it can run it again; a warning does not mean the cache was already repaired.
The three phases #
For operating the system, distinguish the plan you can decline, the work in progress, and the reported outcome. A dry run previews a request without applying package changes; it does not reserve the metadata or installed state for a later invocation. The commit procedure and journal describe the implementation.
The one instant that matters #
For a single-root transaction, the database commit is the durability boundary:
pending work is recovered by rollback, while committed work is not reversed
by recover. This boundary does not remove the failure and visibility limits
above. See Atomicity and
Crash recovery.
Backups make rollback free #
Renaming displaced files avoids copying their contents for rollback; it does not make restoration infallible or promise a lasting backup. The backup reference says ordinary transaction backups are discarded at commit, while earlier operator guidance described a retention window. Do not rely on transaction backups for a later undo. The undo reference describes resolving from archive metadata and requiring a reachable repository or usable cache. Keep independent backups for data you must recover.
Why this shape #
Planning, journalling and a database commit support recovery; they are not a whole-system snapshot. Package-version reversal affects the package payload, not registry state, runtime data, user data or feature setup. The transaction and rollback chapters hold the implementation detail and its limits.
Where to go next #
Dependency resolution
Peios / Using Peios / Package management
A request to install one package can add dependencies, replace another
package or affect another root. The plan is the full change you are
approving. Use --dry-run on the intended install, upgrade, downgrade or
removal to inspect it before applying anything.
The plan #
Review these points before answering proceed?:
- Are all named packages and dependencies expected?
- Which versions and repositories will supply them? Does a local-file marker indicate that you are vouching for a file directly?
- Is a virtual name being satisfied by a different package than you expected?
- Will
replacesor--cascaderemove something you still need? - Will claims move, or entries tagged
-> <root>change another root? - Does a downgrade or cross-repository takeover need a separate approval?
Decline the plan if any change is unexpected. Check repository configuration and package details, change the request, and preview again. Do not assume that the package named on the command line is the only package affected.
When resolution refuses the request #
Resolution rejects an unsatisfied dependency, conflicting packages, an architecture mismatch, an ordering cycle or a graph that exhausts its work budget. Read the named packages and constraints before retrying.
- Refresh and search the intended repositories if a package is unavailable.
- Correct a missing or untrusted repository rather than silently accepting another source.
- For a blocked removal, inspect the dependents. Use
--cascadeonly if you intend to remove all of them. - For a conflict, choose a compatible package set; there is no automatic conflict-removal approval that makes the rejected plan safe.
- For root-related errors, inspect the root references and preview each affected target.
The failure conditions and removal-cascade reference document what is checked. Optional dependencies are not installed automatically; name a wanted companion explicitly.
Elevated authorisation #
Most of a plan is routine, and the single proceed? prompt covers it. A plan can also contain an action that a routine yes should not cover — one you must review and approve individually, on its own.
peipkg detects three such actions and, for each one in a plan, asks a separate question:
this operation requires elevated authorisation:
nginx would move backward from 1.27.5 to 1.27.4
type "authorise downgrade nginx" to authorise this action (anything else refuses):
The three actions that trigger it:
| Action | Why it is elevated |
|---|---|
| A downgrade | Moving a package backward can reintroduce a problem a newer version fixed. It should be a conscious choice, never a side effect of some larger plan. |
A foreign replaces | A package from a lower-priority repository using replaces to displace a package you installed from a higher-priority one. Left unguarded, a low-trust repository could quietly take over a package you trusted a better source for. |
A low-trust provides | A package from a low-trust repository advertising a virtual capability in a way that shadows a package from a more-trusted source — satisfying a dependency with its own build instead of the one you would expect. |
Two things make this prompt different from the routine one:
--yesdoes not satisfy it.--yesskips the routineproceed?prompt; it has no effect on an elevated-authorisation question.- Name the action and package. The text prompt requires its displayed phrase, such as
authorise downgrade widget, followed by a newline. Genericyoryes, closed input, and an unterminated phrase refuse the action. Scripts can supply the specific phrase; see the authorisation reference for the text and structured interface contracts.
Each elevated action is presented and authorised on its own; approving one does not approve the next. And the authorising act is itself written to the audit stream — the record shows not just what was done, but that it was specifically authorised and what was authorised.
If an elevated action is not one you want, the plan as a whole is the thing to reconsider: where the package is coming from, whether the repository priorities are right, whether the downgrade is really what you meant.
The relationships between packages #
A package's manifest declares how it relates to others. Four relationships drive resolution.
Dependencies. A package can require other packages, each by a version range. peipkg pulls every dependency into the plan, and their dependencies in turn, until the request is closed.
Conflicts. A package can declare that it cannot coexist with another. If a plan would put two conflicting packages on the system at once, resolution rejects it.
Provides. Several packages can advertise the same capability — a virtual name that is not itself a package. A dependency written against that name is satisfied by any package that provides it. This is how "needs a mail transport agent" can be met by whichever one you actually install.
You can ask for a virtual name directly, too: peipkg install coreutils works even when nothing is named coreutils, and installs whichever package provides it. Because you asked for one name and got a package with another, peipkg says which one it chose. Upgrades and removals are the exception — those name a package you already have, so they match by name only and never substitute one package for another.
Replaces. A package can declare that it supersedes another — the usual case being a rename, or a merge of two packages into one. Installing a package that replaces another causes the replaced package to be removed as part of the same plan.
provides has a stronger cousin: a claim, a single shared name that exactly one package may hold at a time. Where any number of packages can advertise the same provides name at once, a claim has one holder — see Claims.
A dependency can also be routed into a different root, written Depends: foo IN <root>, so that a whole root can be composed through the dependency graph — see Named roots.
Choosing a version #
Selection depends on version constraints, architecture, repository origin and priority, and whether a name is a concrete package or a virtual capability. For upgrades, the installed version is the starting point; a plan with no update may reflect its constraints and available sources.
Repository preferences apply before version within one package name. An
architecture change from x86_64 to noarch does not by itself keep a
package on an older release: architecture only breaks a remaining version
tie within that name. Between distinct providers of a capability, the
primary architecture remains preferred. The detailed
candidate-selection reference
sets out the ordering and virtual-version handling.
Resolution is a pure calculation #
Resolution uses metadata rather than downloading package payloads. It produces a plan or explains why no plan was found. A dry run is a preview of those inputs, not a reservation: refreshing a repository or changing the installed set before applying can change the result. Review the final plan too.
See Inputs and outputs for the
resolver's inputs, ordering and determinism discussion, and
Candidate selection for its
selection and tie-breaking rules. The implementation is bounded and can
return a too complex rejection rather than continue indefinitely.
Where to go next #
Claims
Peios / Using Peios / Package management
Use peipkg claim when several installed packages can supply the same shared
name and you need to check or choose its holder. This does not enable a
feature or restart a service.
peipkg claim registryd
peipkg claim registryd grant org.example.altregd
peipkg claim registryd
In this example, grant only after the first command lists
org.example.altregd as an installed eligible provider. Review the affected
links, approve the change, and inspect the holder again. Revoking a claim or
removing its holder leaves it unheld; another provider is not chosen for you.
Some filesystem names can be provided by more than one package. Two registry daemons — dev.peios.loregd and an alternative — both install a working binary, but only one of them can own /usr/sbin/registryd. peipkg calls that shared name a claim, and it owns the machinery that decides which package holds it.
Three words are used precisely on this page:
- A claim is a single shared filesystem name that many installed packages may be able to answer, but exactly one may hold at a time.
- An eligible provider is an installed package able to answer a given claim.
- The holder is the one package that currently answers it.
(This is unrelated to token claims — the security-token attribute concept in the identity docs. The same word names a different subsystem.)
What a claim is #
A claim materialises as a symlink on disk. The link lives at the claim path — the shared name, such as /usr/sbin/registryd — and points at a file inside the holder's payload, the target, such as /usr/sbin/loregd. Ask the system for the shared name and you reach whichever provider currently holds it.
/usr/sbin/registryd -> /usr/sbin/loregd
The claim symlink is owned and managed by peipkg. It is not shipped inside any package's payload; no provider installs it, and removing a provider does not remove it out from under peipkg. peipkg creates, repoints, and tears down the link as part of the transactions that install, remove, grant, and revoke.
Package declarations determine which paths a claim exposes and which target each provider supplies. The role model, eligibility checks and materialisation reference describe those internals. Claim links are relative on disk so roots remain relocatable; the status output shows logical paths and targets.
Claims and provides/replaces #
A claim is the "exactly one owner of a shared name" extension of the provides and replaces relationships covered in Dependency resolution. There, several packages can advertise the same virtual name and any one of them satisfies a dependency written against it — that mechanism answers "is something here that provides this?". A claim goes one step further: it answers "which single provider owns this concrete filesystem name right now?", and enforces that the answer is never more than one. Provides establishes that a name can be answered; a claim decides which package answers it on disk.
Auto-claim on install #
Installing an eligible provider auto-claims every claim it provides that is currently unheld. If no package yet holds registryd, installing dev.peios.loregd makes it the holder and materialises the link as part of the same transaction — you get a working shared name without a second step.
The rule is strictly unheld-only. Auto-claim never overrides a claim that is already held by another package. Install a second registry daemon while dev.peios.loregd holds registryd and the newcomer is installed as an eligible provider but takes nothing; dev.peios.loregd stays the holder. Reassigning a held claim is always a deliberate act — see the claim command below, or the install flags that force it.
Install flags #
peipkg install accepts flags that override the default auto-claim behaviour for the packages in that install:
| Option | Effect |
|---|---|
--no-claim | Claim nothing. Install the provider(s) without taking any claim, even ones that are currently unheld. |
--claim <names> | Force-claim the named claims (comma-separated), overriding the current holder of each. |
--claim-all | Force-claim every claim the installed packages provide, overriding incumbents. |
Two combinations are hard errors:
--claim-alltogether with--claim.--claim-alltogether with--no-claim.
--claim and --claim-all are how you take a claim that is already held during an install; without them, an install only ever fills claims that are empty.
The claim command #
peipkg claim inspects a claim and reassigns its holder. It has three forms, and a fourth that lists every claim.
peipkg claim <claim>
peipkg claim <claim> grant <package>
peipkg claim <claim> revoke
peipkg claim --json
List every claim. --json with no claim name emits every claim that an installed package can fill or a package holds, as an array. Each has role, holder when it is held, links (each a path and target), and providers, the installed eligible providers.
Report status. With just a claim name, peipkg prints the current state of the claim: the current holder, the materialised links shown as path -> target, and the installed eligible providers.
$ peipkg claim registryd
holder: dev.peios.loregd
links:
/usr/sbin/registryd -> /usr/sbin/loregd
eligible providers:
dev.peios.loregd
org.example.altregd
Grant. grant <package> makes an installed eligible provider the holder. peipkg repoints the claim's links to that package's targets in one transaction. Review the transaction and recovery limits before treating that as a whole-system snapshot. The named package must be an installed eligible provider for the claim.
$ peipkg claim registryd grant org.example.altregd
Revoke. revoke removes the grant. The claim becomes unheld and its links are torn down. peipkg does not automatically promote another provider — a revoked claim has no holder until you grant one.
| Option | Effect |
|---|---|
--yes, -y | Skip the confirmation prompt. Applies to grant and revoke. |
grant and revoke each run as a standalone transaction, appear in history, and can be reversed with undo. They also emit a claim-change event under the system's emission policy; the audit reference describes the policy and emission-failure limits. Use the claim command rather than silently editing its symlinks by hand.
What happens on uninstall #
Uninstalling the current holder auto-withdraws the claim. The holder is going away, so peipkg tears down its links and the claim becomes unheld as part of the removal.
peipkg does not auto-promote another provider in its place — an automatic promotion would be the kind of silent reassignment claims exist to prevent. Instead it surfaces the remaining eligible providers and hands you a ready-to-run command to reassign the claim yourself:
$ peipkg remove dev.peios.loregd
...
claim 'registryd' is now unheld. eligible providers: org.example.altregd
to reassign it, run:
peipkg claim registryd grant org.example.altregd
If the holder was the only eligible provider, the claim is left unheld with nothing to promote, and any consumer relying on the shared name will find it absent until a new provider is installed.
If a claimed name is missing or wrong #
Inspect peipkg claim <claim> first. An unheld claim needs an explicit grant
to an eligible provider. A held role can legitimately have no link if no
package declares a path for it. For a missing target or a disagreement between
reported links and disk, check the holder's files with peipkg verify and
consult A broken claim. Do not assume
that a normal unrelated transaction will repair a link whose database record
already matches the desired state.
Exit status #
| Code | Meaning |
|---|---|
0 | The operation succeeded — the status was reported, or the grant or revoke was applied (a declined prompt is also 0: nothing failed). |
1 | The operation failed — the claim has no eligible provider, the named package is not an eligible provider, or a named package or claim is not installed. |
2 | A usage error — an unknown subcommand (the only subcommands accepted after the claim name are grant and revoke) or a malformed option. |
Inspecting and verifying
Peios / Using Peios / Package management
Use the query commands to identify installed software and its source, or
verify to compare installed files with peipkg's records. These checks are
read-only. The optional clean command below does delete cache data.
Run them against the same root as the change you are checking, with
peipkg --root TARGET ... when that is not /.
| Question | Command |
|---|---|
| What is installed? | peipkg list |
| Which version and source supplied a package? | peipkg info <package> |
| Which files does it own? | peipkg files <package> |
| Who owns this path? | peipkg owns <path> |
| What can my repositories install? | peipkg search <term> |
| Did files change or disappear? | peipkg verify [package]... |
| What operation ran? | peipkg history |
A successful file check does not test service health, feature state or user
data. After a failed transaction, read the
recovery limits
even if history says rolled-back.
Listing what is installed #
peipkg list
list prints every installed package — name, version, and architecture.
$ peipkg list
nginx 1.27.5 x86_64
pcre2 10.44 x86_64
zlib 1.3.2 x86_64
With --json it emits the same set as a JSON array. Each package has name, version, architecture, origin (the repository it came from, empty for a local file), orphaned, installed_at, size_installed in bytes, and description where its manifest has one.
Showing one package's details #
peipkg info <package>
info prints the full record of one installed package: its version and architecture, the repository it came from (or (local file) if it was installed from a .peipkg directly), when it was installed, and — from its manifest — its description, license, and homepage.
$ peipkg info nginx
name: nginx
version: 1.27.5
architecture: x86_64
origin: official
installed: 2026-05-19T14:02:10Z
description: HTTP and reverse proxy server
license: BSD-2-Clause
homepage: https://nginx.org
With --json it emits the same record as one JSON object: the members list --json gives, and — where the manifest has them — license, license_class, homepage, alternate_upgrade, dependencies (each a name and any version constraint) and provides.
Listing the files a package owns #
peipkg files <package>
files prints every filesystem object the package owns — the files, directories, and symlinks that were placed by its install and are tracked against it. This is the record peipkg uses to remove the package cleanly and to verify it.
$ peipkg files zlib
/usr/lib/libz.so.1
/usr/lib/libz.so.1.3.2
/usr/include/zlib.h
With --json it emits an array of objects, each with path, type (file, dir or symlink) and, for a symlink, target.
Finding which package owns a path #
peipkg owns <path>
owns is the reverse lookup: given a path, it reports which installed package placed it.
$ peipkg owns /usr/lib/libz.so.1
zlib
If no installed package owns the path, owns says so and exits non-zero. A path that nothing owns is either not part of any package or was created outside peipkg. With --json it emits the owners' names as an array, empty when nothing owns the path.
Searching the repositories #
peipkg search <term>
search looks through the configured repositories' active indexes for packages whose name or description contains the term, case-insensitively. It searches what is available, not what is installed — it is how you find a package before installing it.
$ peipkg search proxy
nginx 1.27.5 [official] HTTP and reverse proxy server
haproxy 2.9.7 [official] Reliable, high-performance TCP/HTTP load balancer
search reads the cached repository metadata, so run peipkg refresh first if you want it to reflect the latest catalog. A repository with no usable cached metadata is skipped with a warning rather than failing the search. --json emits the matches as an array, empty when nothing matches. Each has name, version, architecture, repository, size_download and size_installed in bytes, and description, license and homepage where the repository gives them.
Verifying installed files #
peipkg verify [package]...
verify checks that what is on disk still matches what was recorded when each package was installed. For every file a package owns it checks:
- a regular file — that it is present and its content still hashes to the recorded value;
- a symlink — that it is present and still points where it was recorded to point;
- a directory — that it is still present and still a directory.
With no arguments, verify checks every installed package. With package names, it checks only those.
$ peipkg verify nginx
nginx: /etc/nginx/nginx.conf has been modified since install
verify: 1 problem(s) found
A reported file is not necessarily a fault. A configuration file you edited on purpose will show up — verify is telling you the file diverged from the package's version. verify reports; it does not judge intent and it does not change anything.
If every checked file is intact, verify says so and exits 0. If anything diverged, it lists each problem and exits non-zero — which makes it usable as a check in a monitoring script.
With --json it emits the problems as an array, each a package and a problem, and exits 0 whether or not it found any: an empty array means every checked file is intact.
Investigate an unexpected verification result #
- Use
infoandfilesto establish the package, version and affected paths. - Compare the result with intended edits. For configuration under
/usr/etc/or legacy/etc/, a.peipkg-newsibling can mean an upgrade preserved your edit and wrote a new default beside it. That original path can continue to report a mismatch against the new recorded hash. - If the difference is unexpected, consider an interrupted rollback, storage corruption or tampering; a hash mismatch alone does not distinguish them. Preserve files you need before attempting replacement.
- Repair deliberately, then verify again. There is no
reinstallcommand; the file-mismatch reference describes removal and installation as separate transactions or a version change and change back. Preview dependency and removal consequences first.
verify does not repair anything, and an unowned .peipkg-new or backup is
not proof that the current package's recorded files are intact.
Cleaning the metadata cache #
peipkg clean
clean deletes unused metadata-cache data; it is a write operation, not a
package verification or recovery command. The cache reference
describes content-addressed objects, with pointers naming each repository's
current metadata, and collection of objects no pointer references.
$ peipkg clean
removed 2 orphaned cache file(s)
Earlier operator guidance described deletion solely by removed repository;
the technical reference instead describes unreferenced-object collection,
which can also cover superseded objects from a still-configured source. Do
not rely on a repository being configured to preserve every old cached object.
This guide does not establish which cache layout your tool version uses.
Cleaning is optional housekeeping, not a way to recover a pending transaction;
it does not replace reviewing history, running recover or checking files.
Exit status #
| Code | Meaning |
|---|---|
0 | The command succeeded. For plain verify, every checked file was intact. verify --json also exits 0 when it finds problems: inspect the array. |
1 | The command failed — a named package is not installed, a path is owned by nothing, or, for plain verify, at least one file had diverged. |
2 | A usage error — an unknown command or a malformed option. |
Named roots
Peios / Using Peios / Package management
Before changing an offline installation or an initramfs tree, check which
root peipkg will use. With no --root, the default is the running system
at /; install can also follow a package's default_root.
Check the target before changing it #
peipkg root list --tree
peipkg root show initramfs
peipkg --root initramfs list
Use a registered name such as initramfs, or a literal path containing /,
such as /mnt/image or ./tree. Put the global option before the command:
peipkg --root /mnt/image list
peipkg --root /mnt/image upgrade --dry-run --no-recurse
Read the resolved path and status before applying changes. A dangling root
has a binding but no target tree; repeating an install does not make that a
valid target. An unregistered name or a resolution cycle is an error.
An upgrade normally resolves the selected root and its reachable named
roots together. Review every target in its combined plan. Use --no-recurse
when you intend to update only the selected root. After a cross-root failure,
preserve all participating roots and recover from the same system anchor;
check history and files in each participant afterwards.
What a named root is #
A named root is a registered name for a subtree, recorded in the registry of the root it lives under. The name initramfs bound to the path boot/initramfs is a named root of /: it says "within this root, the name initramfs means the subtree at boot/initramfs, and that subtree is itself a root."
Two ideas follow from that.
Every root has its own registry. The bindings a root knows about are its own. / may register initramfs; what initramfs registers is recorded inside initramfs, not in /. Paths in a registry are stored relative to the root that owns them, so a registry travels with its tree — an image built in one place and mounted in another resolves the same way.
References are resolved from the current root. The current root is whatever --root points at (or / when it is omitted). A reference is resolved by starting there and walking its registry.
Referencing by name #
Once initramfs is registered under /, these two commands target the same tree:
$ peipkg --root boot/initramfs install live-boot
$ peipkg --root initramfs install live-boot
The second names the root instead of spelling out its path. The name is looked up in the current root's registry and resolved to the bound path.
Dotted composition #
Names compose by dotting. initramfs.subroot resolves left to right: from the current root, walk into initramfs's registry, then from there into subroot. Each dot is one more step outward through one more registry.
$ peipkg --root initramfs.subroot install ...
Every dotted segment must match the grammar [a-z0-9][a-z0-9_-]* — a lowercase-alphanumeric lead followed by lowercase alphanumerics, hyphens, and underscores. A segment that names no registered root is a hard error, not a silent miss. A registry arrangement that loops back on itself is detected and reported as a resolution cycle rather than followed forever.
Path or name: how --root decides #
--root accepts either form and tells them apart by a single rule:
- Any value containing
/is a filesystem path and is used exactly as before —--root boot/initramfs,--root /mnt/image,--root ./tree. This is unchanged behaviour. - A bare dotted identifier is a named reference —
--root initramfs,--root initramfs.subroot— resolved through the registries as above.
The / is the discriminator. If you mean a literal directory, include a slash (even ./name); if you mean a registered name, use the bare identifier.
The root command #
root manages named roots. Every subcommand operates on the current root's registry — the registry of whatever --root points at. To manage the initramfs's own named roots, aim --root at the initramfs first.
peipkg root add <name> <path>
peipkg root remove <name> [--purge]
peipkg root list [--json] [--tree]
peipkg root show <reference> [--json]
root with no subcommand is a usage error — a subcommand is required (add, remove, list, show). An unknown subcommand is a usage error too.
root add #
peipkg root add <name> <path>
Register a named root in the current root's registry. <name> is a single root segment — it must match [a-z0-9][a-z0-9_-]* and must not contain a dot; you register one name at a time, not a dotted chain. <path> is stored relative to the current root. There are no flags.
$ peipkg root add initramfs boot/initramfs
root remove #
peipkg root remove <name> [--purge]
Unregister a named root. By default this removes only the registry entry — the files on disk are left in place, so the subtree survives and can be re-registered. --purge additionally deletes the named root's filesystem tree.
| Option | Effect |
|---|---|
--purge | Also delete the named root's filesystem tree, not just its registry entry. |
root list #
peipkg root list [--json] [--tree]
Print the current root's registry. Plain output lists the names registered directly under the current root.
| Option | Effect |
|---|---|
--json | Emit JSON — for each entry its name, its stored path, its resolved_path, its status, and its children. |
--tree | Recurse into each child's registry and print the whole nested arrangement. The recursion is cycle-guarded, so a registry that loops is reported rather than followed endlessly. |
root show #
peipkg root show <reference> [--json]
Resolve one reference and report on it. <reference> is either a dotted named-root reference or a path — the same path-or-name rule as --root. show reports the reference's resolved path, its status, and the number of packages installed in it.
The status is one of two words:
present— the resolved path exists on disk and is a real root.dangling— the reference resolves through the registries, but the path it names is not there. The binding exists; the tree it points at does not.
| Option | Effect |
|---|---|
--json | Emit the same report as JSON. |
present and dangling are the vocabulary the rest of this page uses for "the binding points at something real" versus "the binding is valid but its target is missing".
Automatic target selection with default_root #
A package's manifest can declare the root a top-level install of it should land in — its default_root. When you install such a package without saying where, peipkg reads that field and re-roots the install onto it for you.
$ peipkg install live-boot
If live-boot's manifest sets default_root: initramfs, this installs into the initramfs root even though no --root was given. The package's manifest records where it belongs, so you do not have to specify it.
Two rules keep this predictable:
- It applies only when no explicit
--rootwas given. Passing--rootwith any value, including a named one, states the target outright and suppressesdefault_rootre-rooting entirely. An explicit root always wins. - The packages in one command must agree. If the packages named in a single install declare two or more distinct default roots, peipkg cannot pick one and does not guess — the command is rejected as an error. Split it into one command per target.
default_root only chooses a target; it does not create or resolve anything the registry could not already reach. The chosen root is resolved through the registries like any other named reference.
Cascading upgrade #
By default peipkg upgrade resolves one combined plan over the current
root and its reachable named roots. With no package names, it considers the
installed packages in that combined world; a named upgrade considers that
name in every reachable root where it is installed. On a dynamic-initramfs
system, this lets / and boot/initramfs participate in the same plan.
Review the whole plan before approving it.
If the plan changes more than one root, peipkg uses a coordinated cross-root transaction. A plan changing only one root uses the single-root executor. The multi-root sequence is:
- Lock the participating roots and check pending recovery, then fetch and verify the packages for all participants before extracting any.
- Prepare each root. If preparation fails, stop and attempt to roll back prepared changes. Rollback can itself fail; the affected journal remains pending and the installation must not be treated as restored.
- Commit package state root by root. A commit failure does not begin an independent upgrade in the next root: peipkg continues the coordinated commit attempts and reports a partial-commit error. Roots already committed remain committed; use cross-root recovery for the remaining work.
This is not an instantaneous all-files/all-roots switch or a whole-system snapshot. Files can become visible during preparation, and one root's post-commit maintenance can run before another root's commit is attempted. An error is not proof that every participant stayed unchanged.
This sequence follows
peipkg 8b588ae8's upgrade scope,
reachable-root planning,
and coordinated executor.
The prepare/commit boundary
and rollback-failure handling
set its limits. These are source-backed behaviours, not a runtime test or
proof that a particular released image contains that revision.
| Option | Effect |
|---|---|
--no-recurse | Confine the upgrade to the current root only. The cascade into nested roots is disabled. |
Use --no-recurse when you deliberately want to move just one root — for example to upgrade the initramfs on its own without touching /, by pointing --root at it and disabling the cascade.
Cross-root dependencies #
Installing a package can pull dependencies into another registered root. The default upgrade also resolves across reachable roots, as described above. Review every target before approving: the operation can affect more than the root you selected.
The cross-root dependency reference keeps the manifest syntax and routing details, including the separately documented limits for other verbs and cross-root autoremoval. Removing the last dependent does not automatically remove its dependency from another root. Inspect the relevant roots rather than assuming every later operation repeats the install's routing or cleans up another tree.
A plan that reaches beyond the current root announces it before you approve:
- a
note: this also changes other roots: ...line naming the other roots the plan touches, and - a per-line
-> <root>tag on each plan entry that lands outside the current root, so you can see at a glance which change goes where.
You are never taken across a root boundary silently; a cross-root plan always shows its routing.
Cross-root undo and recovery #
The operator interface describes undo of a cross-root transaction as
reversing all participating roots together; there is no supported partial-root
undo of that transaction. Check the history and preview the inverse plan
before applying it.
For an interrupted operation, retain the named-root topology, make every
participant present and reachable, and run peipkg recover from the same
anchor, using the original --root TARGET if one was selected. The command
discovers reachable roots; it does not establish that missing or unregistered
participants have been found. An ordinary single-root operation refuses
pending cross-root work in isolation.
Recovery rolls pending participants back if none committed. If a participant has committed, it rolls pending participants forward using their persisted completion data. Missing or malformed data causes refusal; further recovery can also fail after another participant has been reconciled. Do not rely on an interruption leaving all roots at the same version.
In the reviewed source, the CLI can print rolled back cross-root
transaction for either recovery direction. That wording is not proof of
rollback. Inspect per-root history and verify files, and treat refused or
unreachable roots as unresolved. Roll-forward recovery also does not invoke
the normal post-commit side-effect runner: do not assume depmod or man-db
was retried. Follow Transactions and recovery
for the pinned recovery sources, outcome checks and maintenance limits.
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | Failure — an unregistered name, a dangling or otherwise invalid reference, a resolution cycle, a failed preparation or rollback, or a cross-root commit or recovery failure. |
2 | Usage error — a missing or unknown root subcommand, a malformed option, or the wrong number of positional arguments. |
Composing a root
Peios / Using Peios / Package management
Use the separate peipkg-compose binary when you need a new package-owned
root directory for an image. Use peipkg to maintain a root that already
exists. Compose does not produce a bootable image by itself or run feature
setup scripts.
Build and check a new tree #
-
Prepare a manifest with the intended architecture, timestamp, repositories, package set and any named roots. Verify the repository anchors through a trusted channel. The manifest reference gives every field and a complete example.
-
Choose an output directory that does not exist. Compose refuses an existing tree; do not point it at a live installation.
-
Resolve and record the package set, then build from that lock:
peipkg-compose lock image.toml peipkg-compose build image.toml --out ./root --locked -
Read the build outcome and warnings. If the build fails or is interrupted, the staging directory is left for inspection; it is not the finished root.
-
Inspect the successful output's package records and files, for example with
peipkg --root ./root listandpeipkg --root ./root verifywhere peipkg is available. Check security metadata required by the image separately.
A fresh tree includes package files, a package database and repository
configuration. It does not include recorded repository trust from the build.
On the booted system, check that configuration and deliberately run
peipkg repo add <name> for each source you choose to trust. Configuration is
not a completed trust ceremony.
The mental model #
lock resolves the package set; build assembles it. Keeping the same manifest,
lock and package bytes makes the selected inputs repeatable. For the resolution,
verification and assembly internals, see the
composition reference.
peipkg-compose lock #
peipkg-compose lock <manifest> [-o <lock>]
Creates a lock without building a tree. image.toml defaults to
image.lock.toml. Only -o selects another lock path; there is no --out
for this verb. See the full lock interface.
peipkg-compose build #
peipkg-compose build <manifest> --out <dir> [--locked | --update]
[--dangerously-bypass-path-restrictions]
[--record-xattrs <file>] [--no-dependencies]
- Use
--lockedto require an existing matching lock and do no metadata resolution. Package bytes must still be available. - Use
--updateto resolve again and overwrite the lock before building. - With neither flag, a missing lock is created; an existing lock must match
the manifest.
--lockedand--updatecannot be combined. --outis required, and its directory must not already exist.
The complete build options explain the explicit
path-restriction grant, recording security attributes, and --no-dependencies.
That last option deliberately creates a tree that is not self-sufficient;
it leaves signature and hash verification enabled and requires a lock made
in the same mode. Do not add it merely to get past a missing dependency.
The manifest #
The manifest is strict TOML: unknown keys, an empty package set, undeclared
repository or root references, and duplicate (root, name) requests are errors.
The complete schema and sample are in the manifest reference.
When preparing the manifest:
- Check the top-level fields for architecture, timestamp and the requested package set.
- Repository entries
also become
.repofiles in the image; those files do not establish trust. - Package entries
choose versions, sources and target roots. There is no manifest-level
default_root; packages can carry their own default placement. - Named-root paths
must be relative, must not escape through
.., and cannot be..
The lock file #
The generated lock pins the package set. Do not hand-edit it or use --update
just to suppress a mismatch without reviewing the changed inputs. See the
lock contract for digest, architecture and timestamp checks.
Elevated actions that would prompt in a live install are emitted as warnings
during unattended composition, so read them before using the image.
Named roots and claims in a composed image #
All roots are built under one output tree. The composer records named roots and selects claim holders for the fresh package set; no installed incumbents exist to preserve. See the detailed rules for deterministic holder selection and relocatable links.
What ends up in the root #
The output contains payloads, var/lib/peipkg/db.sqlite and repository files
under lcl/conf/peipkg/. A successful build renames the whole staged tree into
place. See the output contract before treating
an interrupted build's staging directory as a usable artifact.
What compose does not do #
The outer image builder is responsible for boot integration, runtime caches and feature setup. Compose runs no post-install maintenance steps and emits no audit events. It does not build or sign the packages it consumes.
The references disagree on security-descriptor materialisation; the CLI
interface documents --record-xattrs, while an implementation summary says
no descriptors are materialised. Inspect the attributes or recorded output
needed by your image. The full limitations and interface differences
retain those details rather than assuming either behaviour is universal.
Exit status #
| Code | Meaning |
|---|---|
0 | Success, or help output (-h, --help, help) |
1 | Resolution, verification, fetch, lock or filesystem failure |
2 | Usage error, including a missing manifest or --out, or conflicting flags |
For exact cases and the absence of environment-variable and --version
interfaces, see the complete command reference.
Features
Peios / Using Peios / Features
Installing a package makes its files available. A feature performs additional setup, such as defining a service or changing a setting, through scripts you explicitly run. Installing its package does not run those scripts or turn the feature on. Package-manager maintenance steps are a separate, closed set, not feature scripts.
Start with feat list and feat info <name> to see what is available, its
state and what it says it will change. Use Using feat
for the commands, or open Feature Manager
from the launcher by typing feature.
| What you want | Tool |
|---|---|
| Install or remove the files that supply a feature | peipkg or Package Manager |
| Set up a feature without turning it on | feat install <name> |
| Set it up and turn it on | feat add <name> |
| Turn it off but keep its setup | feat disable <name> |
| Undo its setup before removing its package | feat remove <name> |
What a feature can do #
A feature's scripts run as whoever asked, with no authority of their
own. feat grants nothing: a script can do exactly what the person
running it could do by hand. An Administrator's request can define a
service; the same request from someone else is refused by the system, at
whatever step needs the authority.
Its lifecycle #
A feature is in one of three settled states, and moves between them a step at a time:
| State | Means |
|---|---|
| Not installed | Nothing it would set up is in place. |
| Installed | Its install script has run: what it sets up is in place, but not in use. |
| On (enabled) | Its enable script has run as well: what it set up is in use. |
Turning on a feature that isn't installed installs it first. Removing
one that is on turns it off first. Each step runs one script: install.sh,
enable.sh, disable.sh or uninstall.sh. A feature that has nothing to do
at a step ships no script for it, and the step only records the new state.
What a feature's own words say about when it takes effect matters: dynamic boot's services, for example, start at the next boot after it is turned on.
Interrupted #
Before each step's script runs, the step is recorded as begun; once the script succeeds, the new state is recorded. A script that fails or is stopped therefore leaves the feature interrupted — installing, turning on, turning off or removing — rather than claiming a state it never reached.
An interrupted feature is fixed by asking again: running the same step reruns its script from the start, which every feature's scripts are written to allow. Or it can be taken the other way: an interrupted turn-on can be turned off, an interrupted install removed.
Where its state is kept #
Changing a feature requires write access to its registry state. As shipped,
Administrators can change it and everyone can read it. Use feat or Feature
Manager rather than editing numeric state to make a failed step look complete:
the scripts' work may still be unfinished.
The client reference keeps the definition path, registry key and complete numeric state mapping.
When its package is removed #
Removing the package that brought a feature takes its scripts away, but
not what they set up, since package removal does not run the feature's
teardown scripts. The registry
still holds the feature's state, and both feat list and Feature Manager
show it as having no definition. Installing the package again brings the
scripts back, so the feature can be turned off or removed properly.
Remove a feature before removing its package. Verify the feature's state
with feat list or feat info <name> after the removal. If its definition
is already missing, install the supplying package again, inspect the feature,
then use feat remove <name> while the scripts are available. Package undo
or recover is not a substitute for running the feature's teardown scripts.
Features in an image #
An image can install or turn on features as it is built, with
[[feature]]
in its spec. They are set up at
the image's first boot, before its services start.
Where to go next #
- Using feat, the command.
- Feature Manager, the window.
- Writing a feature, to ship one with your own package.
Using feat
Peios / Using Peios / Features
feat runs a feature's scripts and records its state. It runs them as
you: what you may change is what you could change by hand.
Inspect, change and check a feature #
Start by reading the feature's own description; it tells you what setup does and when it takes effect. For example:
feat list
feat info dynamic-boot
feat add dynamic-boot
feat info dynamic-boot
add installs and enables the feature. Use install instead if you want
setup only, or enable when it is already installed. For dynamic-boot, the
services start at the next boot; an enabled state does not mean they have
already started. Read the script output as well as the final state. For the
watchers, output destination and rebuild evidence, use the update-activation
checks;
enabled or running alone is not proof of boot readiness.
The scripts run with your rights. Permission to update the feature's state does not guarantee permission for every action its scripts attempt.
Turn off or remove setup #
feat disable <name>
feat info <name>
This leaves the feature installed but off. To remove what it set up:
feat remove <name>
feat info <name>
remove turns an enabled feature off first. It does not remove the package
that supplies the scripts, so the feature remains available to set up again.
Remove setup before removing its package with peipkg.
Recover an interrupted feature #
- Read the failed script's output and inspect
feat info <name>orfeat listto identify the interrupted phase. - Address the reported cause, such as missing authority or a resource the script needs. Updating the recorded state alone does not finish the work.
- Run the same command again to retry the step from the start. Feature scripts are required to tolerate that retry.
- Inspect the result again. If you want to back out, an interrupted enable can be disabled, and an interrupted install can be removed.
An interruption is recorded rather than automatically rolled back as a package transaction. If the definition is missing, restore the supplying package first so the feature's scripts can perform the retry or teardown. See Interrupted and When its package is removed.
Commands #
| Command | Does |
|---|---|
feat list | Every feature and its state, one a line: its name, a tab, and not-installed, installed, enabled, or an interrupted installing, enabling, disabling or uninstalling. A feature whose definition is gone is listed with a third column, no definition. |
feat info <name> | What the feature says it is (its title and description), its state, and which scripts it has. |
feat install <name> | Runs its install script. It isn't turned on. |
feat enable <name> | Runs the enable script of a feature that is installed. |
feat disable <name> | Runs the disable script of a feature that is on. |
feat add <name> | Installs it if it isn't, then turns it on. |
feat remove <name> | Turns it off if it is on, then runs its uninstall script. feat uninstall is the same. |
Asking for what is already so does nothing and says so:
feat enable on a feature that is on, feat disable on one that is off.
Asking again after an interruption reruns the step's script (see
Interrupted).
$ feat add dynamic-boot
applied 1 keys
applied 1 keys
dynamic-boot: created mkirf-watch and mkuki-watch (disabled)
feat: installed dynamic-boot
set Machine\System\Services\mkirf-watch Disabled = REG_DWORD 0 (layer: base)
set Machine\System\Services\mkuki-watch Disabled = REG_DWORD 0 (layer: base)
dynamic-boot: enabled mkirf-watch and mkuki-watch (start on next boot)
feat: enabled dynamic-boot
$ feat enable dynamic-boot
feat: dynamic-boot already enabled
The scripts' own output, and that of the commands they run, appears as
they run, between the lines feat writes.
Exit codes #
| Code | Means |
|---|---|
| 0 | Done, or nothing needed doing. |
| 1 | The command was wrong. |
| 2 | No such feature. |
| 3 | Refused: you may not change the feature's state. |
| 4 | Not possible in the feature's state: turning on one that isn't installed. |
| 5 | A script failed, or reading or writing the state failed. A failed script leaves the feature interrupted. |
For a program #
Human commands and their exit codes are above. Programs can query JSON or run a change in driven mode; the complete schemas and error codes are in the feat client reference.
--json #
feat list --json returns an array and feat info <name> --json an object.
See query members and permission checks
for state, defined, may_change and the other fields. Permission to write
state says nothing about whether a script can complete its work.
--driven #
feat --driven <command> <name> emits JSON Lines for a change. It asks no
questions and gives scripts empty standard input. See
events and terminal outcomes.
A client going away does not stop the change; inspect its eventual state.
Use --json for list and info, which driven mode refuses.
Feature Manager
Peios / Using Peios / Features
Feature Manager is the desktop's window on
features. Open it from the launcher (type
feature). It has no authority of its own: it runs feat as you, so what
you may change is exactly what you could with the command.
The cards #
Each feature on this machine is a card: its title and name, its state, and what it says it does. The state is one of:
| State | Means |
|---|---|
| Not Installed | Nothing it sets up is in place. |
| Installed, Off | What it sets up is in place, but not in use. |
| On | It is set up and in use. |
| Interrupted | One of its scripts didn't finish. The card says which, and what each way on does. |
| Definition Missing | Its package was removed while it was set up. What it set up is still in place; installing the package again lets it be turned off or removed. |
A feature comes in a package: one installed with Package Manager appears here.
The buttons #
| Button | Does | The command |
|---|---|---|
| Turn On | On a feature that isn't installed, installs it and turns it on. On one that is, turns it on. | feat add, feat enable |
| Install Only | Installs it without turning it on. | feat install |
| Turn Off | Turns it off, leaving it installed. | feat disable |
| Remove… | Asks first, then turns it off if it is on and removes it. The feature stays available to install again; its package isn't removed. | feat remove |
| Try Again | On an interrupted feature, runs the step that didn't finish again. | the same command again |
While a change is being made, its card shows which script is running and the last thing it said. Once it is done, the line along the bottom says so.
If a script fails, the card says which, shows everything the script said, and offers Try Again. The feature is left interrupted until it is tried again or taken the other way.
A change is made to the end even if you close the window: the window stays until then, saying so, and then closes.
What you may do #
Whether you may change a feature is asked of the system, by feat. As
shipped, only Administrators may. Anyone else sees every feature and its
state, with the reason said once at the top, and no buttons.
Being allowed to change a feature's state doesn't make every script able to do its work: a script that needs more than you have fails, and says so on the card.
Where to go next #
- Using feat, the same from a terminal, and for a program of your own.
- Writing a feature.
The registry
Peios / Using Peios / Concepts
Use the registry to inspect and change Peios configuration. Start with the current value, look up its meaning, and check the program that consumes it after a change. A successful write alone does not show that the new setting is in use.
Start with a read #
These commands only read settings and documentation:
reg lsshows the values and their types.reg get -Lshows the effective value, its winning layer and sequence. It does not list all the entries hidden by that winner.regmanexplains the expected type, default, valid range and when the setting applies. It reads installed documentation, not live state.
If you do not know the path, try regman -k buffer.
Find where a setting lives maps
common locations. You can also browse with
Registry Editor.
The editor shows the same manual beside each key and value.
Make one controlled change #
- Read the setting's
regmanentry. Check Type, Valid and Applies, including any restart or reboot requirement. - Record the old value and winning layer. For recovery that must retain permissions and layer entries, take a backup.
- Choose the destination layer deliberately. A write needs permission on both the key and the layer; higher-precedence policy can keep a local write from becoming effective.
- Change the value with its explicit type, then read it back.
- Check the consuming component's status and events after the documented application point. It may reject the stored value and retain an earlier one.
Change a setting and verify it
walks through this sequence. For the desktop controls, use
Registry Editor; for all
command options, use reg.
Know what you are addressing #
| Part | Operator meaning |
|---|---|
| Hive | A top-level namespace. Machine holds system-wide configuration; Users holds users' keys. |
| Key | A container with its own permissions, such as Machine\System\KMES. |
| Value | A named, typed setting inside that key, such as BufferCapacity. |
CurrentUser is an alias for the caller's own Users\<SID> key. Paths
are case-insensitive; quote backslash paths in a shell or use forward
slashes with reg. The value name is a separate argument, not part of the
key path. See Keys, values, and types.
If the result is unexpected #
| Symptom | Check next |
|---|---|
| The write fails with access denied | Key rights and layer rights; parent access is not enough. |
| The old value still wins | Layer precedence and write order. |
| The new value is stored but behavior is unchanged | Consumer validation and application timing. |
| A deleted value reappears | Deletion removes one layer's entry. |
| A write times out or the source fails | Read state back before retrying. |
| A change needs to be undone | Layer recovery or backup and restore. |
When you need implementation detail #
Applications and tools reach the registry through the kernel's Layered
Configuration Subsystem (LCS). Userspace sources persist its data;
loregd supplies the default registryd role. You do not edit their
backing databases to change a setting.
The source operations guide covers availability and trust. The kernel LCS TRM covers the data model, syscall ABI, watch implementation and source protocol.
Keys, values, and types
Peios / Using Peios / Concepts
A registry command usually takes a key path and a separate value name. Keeping them separate avoids changing the wrong setting:
Keys and values in a command #
Keys contain subkeys and values; values do not contain other values.
reg get KEY lists the key's effective values. reg get KEY NAME reads
one value. The literal @ in the value-name position addresses the
unnamed default value, if the key has one.
A key need not have a default value. A default value is also different from a setting's documented default: a consumer may use a compiled-in default when no registry value is present.
Keys #
Machine\System\KMES names a key. Its permissions cover the values it
contains. To give two values different permissions, put them in different
keys; there are no per-value permission lists.
Key-name components cannot contain \, / or a null byte. Empty
components, doubled separators and trailing separators are invalid.
Values #
A value has a name, a type tag and data. Unlike a key-name component, a
value name may contain / or \: it is not a path. The null byte is
forbidden; the empty name is reserved for the default value.
The value types #
Use the type the owning component documents in regman. For a command-line
write, an explicit prefix avoids unintended type inference:
| Type | Meaning to a reader | reg set prefix |
|---|---|---|
REG_DWORD | 32-bit integer | dword: |
REG_QWORD | 64-bit integer | qword: |
REG_DWORD_BIG_ENDIAN | Big-endian 32-bit integer | dword-be: |
REG_SZ | Text | sz: |
REG_EXPAND_SZ | Text whose references the consumer expands | expand: |
REG_MULTI_SZ | List of strings | multi: |
REG_BINARY | Bytes | hex: or bin: |
REG_LINK | A link target on a link key | link: |
REG_NONE | No type | none: |
For example, sz:007 preserves the text 007. Without the prefix,
reg infers an integer and loses the leading zeros. See
reg value literals
for encoding lists, bytes and numbers.
The hardware-resource types are retained for format fidelity and have no Peios-specific meaning. Byte encodings, including Peios UTF-8 strings and link targets, are documented in the LCS value representation.
Typed, but opaque #
The registry does not check whether ordinary value data is sensible for its consumer. A known type tag can accompany malformed data, such as a number with the wrong length. Registry Editor shows such data in red as bytes. Unknown type codes and invalid requests can still be rejected; "opaque" does not mean every write succeeds.
REG_LINK on a link key is the exception: the kernel interprets it to
resolve the target. For other settings, follow
Change a setting and verify it.
Names, case, and paths #
reg accepts forward slashes and backslashes as path separators. In a
shell, use forward slashes or quote a backslash path:
Names are case-insensitive and retain their display case. Case matching is locale-independent, but there is no Unicode normalization: visually identical names with different Unicode representations can be distinct.
Volatile keys #
A volatile key is held in memory and disappears on reboot or when its
store unloads. Its children must also be volatile. reg info KEY shows
whether a key is volatile; Registry Editor also shows this in the key pane.
Volatility is about persistence. It does not tell you when a setting
applies. A persistent setting may still require a reboot; check the
regman Applies field.
Where to go next #
Find where a setting lives
Peios / Using Peios / Concepts
Start with the setting's purpose and the program that reads it. Search the installed registry manual, find the key, then inspect its live values. A path alone does not tell you whether a value is a setting to change, a fact published by a service, or private to one user.
Search before changing anything #
These commands only read documentation and current state:
- Search installed documentation with
regman -k, using a word from the setting's name or purpose. - Look up the returned key and value; check type, valid values, default and application timing. A key-only lookup lists its documented values.
- Inspect live values with
reg.-Lshows the winning layer and sequence, not every hidden entry.
For graphical browsing, Registry Editor
shows the same manual beside live values. regman reads documentation,
not live state: a documented entry need not be set. Full command syntax
is in regman and reg.
Common locations #
These common locations are backed by pinned source revisions, not an exhaustive schema or a guarantee about your installed release. Check your installed manual before changing a field.
| What you are looking for | Start here | Who gives it meaning |
|---|---|---|
| A service definition | Machine\System\Services\<name> | peinit reads service definitions. See the service key reference. |
| Init-wide settings | Machine\System\Init | peinit reads global control limits, environment and boot-time path provisioning. |
| Network configuration and reported state | Machine\System\Network | netd, resolvd and policy consumers use distinct subtrees. See the network registry layout for details. |
| Shared authentication policy | Machine\Generic\Authn\Policy | Peios policy about a principal's authority, shared across authentication-authority implementations. |
| Shared security-descriptor suggestions | Machine\Common\SecurityDescriptorBuilder | A claim catalogue for programs that build security descriptors. Common holds stable shared formats that are not part of the Peios specifications. |
| authd's implementation configuration | Machine\Software\Authd | authd's Sources subkeys say which principal sources may register and what they are trusted to assert. |
| A particular user's settings | Users\<SID>; CurrentUser for the caller | Each consumer defines its per-user keys. Check the identity notes below. |
The service and init locations come from peinit's key constants;
its init manual
explains the global settings.
The distinction between shared authentication policy
and authd configuration
is explicit in its manual. The descriptor editor's catalogue documentation
explains the Common example.
Check whose user settings you are reading #
Users\<SID> identifies a principal by SID. authd creates a private user root
at authenticated sign-in, with access for that user, SYSTEM and Administrators;
it does not reset an existing root's permissions.
CurrentUser is an alias, not a separate hive. For a caller-supplied
absolute path, LCS resolves its leading CurrentUser component using the
calling thread's effective token. A service or impersonating thread may
therefore address a different user's settings from your interactive shell.
See the alias's exact rules.
Machine-to-user fallback is consumer-specific. For example, session locale combines machine and user settings for new sessions' environment, under its own precedence rules. Those rules do not apply automatically to other programs.
Separate settings from reported facts #
The registry also contains state written by running components. Under
Machine\System\Network:
- Profiles and interface rules are operator configuration.
- netd publishes
Statusand readiness facts. ItsStatuskeys have SYSTEM-only writes and Everyone reads. Inspect them; use the documented configuration keys to request changes. - A network's status can include remembered DNS server observations.
These differ from both active resolver state and the fallbacks and
overrides in resolvd's configuration
under
Machine\System\Network\Dns.
Use Configuring profiles and Name resolution to make and verify a network change; a stored value alone does not prove active use.
If you cannot find or read it #
- No manual entry: check the owning component's documentation. The installed manual is only as complete as the documentation shipped with it.
- No live value: check the consumer's missing-value behavior. Do not create the key merely because the map or manual mentions it.
- Access denied: check the exact key's permissions. Listing a parent and reading a child are separate operations.
There is no universal Everyone-read rule. The default hive descriptor
grants read access to Authenticated Users and full access to SYSTEM and
Administrators; individual keys can use different descriptors, including
the network Status keys above. Knowing a path does not grant access.
Once you have identified the setting and its consumer, follow Change a setting and verify it for a controlled edit, read-back and consumer verification.
Change a setting and verify it
Peios / Using Peios / Concepts
A registry write changes stored configuration. The program that reads it still decides whether it is valid and when to use it. Treat a change as finished only after checking both the stored result and the consumer.
1. Look up the setting #
If you do not know its key, start with Find where a setting lives. Confirm that it is operator configuration rather than a service's reported state.
Read the expected Type, Default, Valid range and Applies
field. In this example, the documented type is REG_QWORD; the value is a
power-of-two buffer capacity and applies live. Other settings may apply
only on service restart or reboot.
regman reads package documentation, not the live registry. If it has no
entry, find the owning component's documentation before changing the
value. An undocumented setting is not evidence that any type or value is safe.
2. Inspect and prepare recovery #
Record the effective value, type and winning layer. -L shows only the
winner, not every shadowed entry. If the setting is absent, record that
fact too; writing the documented default is not the same as leaving it unset.
Choose the layer to write into, check permissions, and decide how you will recover. Use a backup when you need the subtree's permissions and layered entries retained. A JSON export is useful for review but is not an equivalent backup.
3. Make the smallest intended change #
For example, after deciding an 8 MiB buffer is appropriate for this machine:
The type is explicit and the destination is explicit. A higher-precedence layer can still win, so read-back may show a different value. See Layers before rewriting the setting repeatedly.
For a guarded update, reg set --expected-seq N can reject a concurrent
change with exit status 6. The check is against the destination layer's
entry: use the sequence from reg get -L only when that winning entry is
in the layer you intend to write. If it is not, the effective sequence is
not a sequence for your target layer. See reg
and the transaction notes.
4. Verify that the consumer accepted it #
- Confirm the effective value and type with
regor Registry Editor. - Follow Applies: allow the live consumer to process the change, or arrange its documented restart or reboot if required.
- Inspect that component's status and relevant events. A watch notification
or a successful
reg setis not an acceptance acknowledgement. - Test the behavior the setting was intended to change.
Use the event and audit guides for reading records. Do not treat silence in the log alone as proof of acceptance.
If the stored value is rejected #
Consumers validate their configuration. Under reject-or-keep, a rejected setting remains stored while the consumer keeps its last known-good value and logs the rejection. It does not clamp or silently repair the invalid value.
Check the event for the rejected value and the retained value. Correct the type or data according to the component's manual, then repeat both checks. The consumer's documented fallback matters: for example, SdDefaults uses a compiled descriptor when an override is invalid.
Registry settings follow the same check #
LCS reads its own settings under Machine\System\Registry. It validates
changes before using them and logs rejections. Deleting one of these
values does not necessarily reset the running parameter: the documented
LCS behavior is to retain the last active value when a parameter is
missing. See Registry startup and configuration.
If the write fails or times out #
A write can fail because of access, input, capacity or source errors.
Use the reg exit-status reference
to distinguish them. After a timeout or source failure, read the affected
state before retrying: a dispatched write or commit may still have succeeded.
Recover deliberately #
- For an isolated edit, restore the intended value in the intended layer and verify the consumer again.
- To withdraw a layer's entry, use per-layer deletion.
- To remove a bundle of configuration, review layer recovery.
- To replace a damaged subtree from a snapshot, follow backup and restore.
Removing configuration does not reverse changes to a key's security descriptor or undo effects already performed by a consumer.
Watching for changes
Peios / Using Peios / Concepts
Use a watch to notice that configuration changed. Use a fresh read to learn the current state, and the consuming component's status and events to learn whether it accepted that state.
Watch a key from a terminal #
This remains running until interrupted. --subtree includes descendant
keys; omit it for only the named key. The filters are value, subkey
and sd, as a comma-separated list; the default is all three. --count N
limits the run to N events. See reg watch.
Arming a watch requires KEY_NOTIFY. Reading values separately requires
KEY_QUERY_VALUE; a watch is not permission to read everything beneath it.
Read after a notification #
reg watch reports that something under its filter changed. It does not
decode the detailed change records. In another terminal, inspect the
current state:
Watches describe effective-state changes, not a history of every layer write. A write hidden by another layer need not change the effective answer. Layer operations may require a full re-read rather than provide one notification for every affected value.
A notification is not consumer acceptance #
Uncommitted transaction changes are not published to watchers. A committed change can notify a consumer, but the consumer still has to read, validate and apply it. Follow Change a setting and verify it instead of treating a watch line as confirmation that the service changed.
Recover after missed events #
Watch delivery is best-effort, with bounded queues. An overflow means the record is incomplete; re-read the key and, for a subtree watch, its subtree. Restore, layer deletion and a source re-registration can also trigger this recovery path. A watch is not a backup or a complete change journal.
After a source outage, wait for reads to work again and resynchronise
from current values. After restarting reg watch, take a fresh read
rather than assume the new stream includes changes made while it was stopped.
When the same path names a different key #
A watch follows the key object opened at registration, not the path text. If another key replaces it at the same path, the old watch does not move to the replacement. Reopen the path and arm a new watch when you need to observe the replacement. Merely hiding a key does not destroy its watch; the object and path distinction is explained in the LCS watch dispatch reference.
Where to go next #
- Access rights for watching and reading
- Source interruptions and safe retry
- LCS watch model, record layout, and queue behavior for client implementers
Layers
Peios / Using Peios / Layers
If a successful write does not change the value you read, inspect the winning layer before writing again:
Registry Editor shows the winning layer in the value pane and the layer list under Layers…. Neither interface currently shows the complete set of shadowed entries for a value.
What a layer retains #
A layer groups registry entries so they can be enabled, disabled or removed together. Each layer retains at most one entry for a particular value name on a key. Another write in the same layer replaces that entry; layers are not a history of every edit.
The default write destination is base. Use --layer NAME or Registry
Editor's Writes go to control to select a different destination.
Check the destination before changing or deleting anything.
How the winner is chosen #
For each value separately, the registry selects among active entries:
- Highest precedence wins.
- At equal precedence, the latest write sequence wins.
A read returns that effective winner. A winning tombstone instead makes the value absent. The same ordering also governs layered key names.
Equal-precedence layers can each win different values #
Base and role layers normally use precedence 0. Neither is permanently above the other. If base writes one value last and a role writes another last, each wins its own value. Layer creation order does not establish a fixed order for all their settings.
This also means a later role update can supersede an earlier local edit at the same precedence. Record the winning layer for the specific setting rather than assuming all settings come from one layer.
When precedence really does differ #
A higher-precedence entry wins even over a newer lower-precedence entry. Rewriting base does not defeat policy above precedence 0.
Creating or raising a layer above 0 requires SeTcbPrivilege in addition
to the relevant key rights. Administrator membership alone does not
supply that privilege. Do not change layer precedence as a shortcut around
a policy decision; use the responsible policy or role's management path.
Sequence numbers are ordering information #
Write order is a monotonic sequence, not the key's wall-clock last-write time. Changing the clock does not make an older entry win.
reg get -L exposes the winning sequence. Conditional writes check the
destination layer's entry, so a winner from a different layer is not a
usable version of your own layer's value. See Transactions.
It is not only values #
Layers can create or hide key names and mask individual or all values on a key. Removing a marker lets the remaining entries compete again; it need not leave the key or value absent. Read Deleting keys and values before choosing between delete, mask and hide.
Where to go next #
- Disable or remove a layer and verify recovery
- Key and layer permissions
- LCS resolution algorithm and sequence counter for implementation detail
What layers are for
Peios / Using Peios / Layers
Use layers to keep a set of configuration entries removable as a group. Before disabling or deleting one, identify what manages it and take a backup of the affected configuration. Removing a layer changes every entry tagged with that name, not just the key you were inspecting.
Recover from a layer change #
-
Run
reg layer ls -land inspect important values withreg get KEY NAME -L. -
Confirm the layer is the intended one. For a managed role or policy, use its management workflow so registry state and its owner remain in sync.
-
If appropriate, disable the layer to stop its entries participating in ordinary resolution while retaining them:
-
Re-read the affected keys. The next winning entries may become visible, or values and keys may become absent. Verify the consuming services again.
-
Re-enable with
reg layer set NAME --enableif that is the intended recovery. Delete withreg layer del NAMEonly when its entries should be removed permanently.
Replace NAME with the layer you reviewed. Disabling is not a security
boundary: a disabled layer can still participate for threads whose
credentials name it as a private layer.
reg layer del asks for confirmation on a terminal. With non-terminal
input it proceeds without that prompt. Do not rely on a prompt to protect
an unattended script.
The base layer #
base always exists, is enabled at precedence 0 and cannot be disabled
or deleted. Writes go there unless you choose another layer. Repeating a
write in base replaces base's previous entry, so deleting another layer
cannot recover an older base edit.
Absence can also come from a layer #
A tombstone competes as "no value here". A blanket tombstone does this for all values on a key; a hidden-key entry masks a key name. Removing their layer lifts those markers, so configuration previously hidden can reappear. See delete, mask and hide.
Roles #
A role can put its registry configuration into a layer. Removing that layer withdraws its entries and leaves the remaining entries to resolve. This does not restore a historical snapshot of the entire machine: other writers may have changed configuration since the role was installed. A transaction is the separate mechanism for applying related registry changes together.
Group Policy #
Higher-precedence policy continues to win over local edits, regardless of write order. Lifting a policy layer lets remaining configuration resurface. Confirm the intended policy through its management workflow; do not try to defeat it with repeated base-layer writes.
What layer removal cannot recover #
Layer removal also does not undo actions already taken by a service, nor make a consumer accept the values that resurface. It is not a transaction, a version-history browser or a substitute for a backup.
A backup of an application's subtree preserves its tagged entries but
may not include the layer definitions under Machine\System\Registry\Layers.
After layer deletion, such a backup alone may leave the restored entries
inactive. Read backup scope and layers
before relying on that recovery path.
Where to go next #
Deleting keys and values
Peios / Using Peios / Layers
Before deleting, inspect the current value and its winning layer. A registry deletion normally removes an entry in the selected layer; a remaining entry can become visible afterward.
Choose the operation you mean #
| Intent | Operation | Result |
|---|---|---|
| Withdraw one layer's value | reg del KEY NAME --layer LAYER | Removes that layer's entry; another may surface. |
| Keep a value absent through a layer | reg mask KEY NAME --layer LAYER | Adds a tombstone that competes with other entries. |
| Withdraw one layer's key name | reg del KEY --layer LAYER | Removes that layer's claim; another may remain. |
| Mask a key name through a layer | reg hide KEY --layer LAYER | Adds a hidden-key entry. |
| Withdraw a whole configuration layer | reg layer del LAYER | Removes its entries across the registry. |
KEY, NAME and LAYER are placeholders. Masking and hiding still obey
precedence and write order; they do not
ignore stronger entries. reg unmask and reg unhide remove the selected
layer's markers. reg mask KEY --all applies a blanket tombstone to values
on that key. See the reg reference.
Hive roots cannot be deleted or hidden.
Deleting values #
- Read
reg get KEY NAME -Land confirm the destination layer. - Remove only the intended layer's entry.
- Read the value again. It may be absent or replaced by another winner.
- Verify the consumer's response. Absence may select a default or retain a prior runtime setting, depending on that component's documented behavior.
Do not assume deleting the effective value restores its compiled default. Do not use deletion to probe for hidden entries on a live system.
Deleting a populated key #
The kernel's single-key delete refuses visible child keys, but the tools provide recursive deletion:
reg del KEY -r --layer LAYERwalks the subtree and deletes in a transaction. It deletes links without following their targets.- Registry Editor's Delete key… deletes the shown key and its subtree after confirmation. Check Writes go to first.
Inspect the subtree with reg tree KEY --values and save an appropriate
backup before proceeding.
The command opens the keys before deleting; file-descriptor and transaction
limits can cause the whole operation to be refused. See
reg del for the documented limits.
reg del -r prompts only when input is a terminal, unless the prompt is
explicitly skipped. Non-terminal input proceeds without confirmation.
A failure before commit aborts the batch. After a commit timeout or source
failure, inspect state before retrying; an error does not prove rollback.
Deleting out from under an open handle #
An open handle refers to a key object, not just its name. Removing its last name does not revoke existing handles: their reads, writes and watches can continue while the object remains open. New opens by the removed path fail, or resolve another key if layers provide one.
If a service must use a replacement key, it may need to reopen it through its documented reload or restart procedure. The LCS deletion reference covers object lifetime.
Permission #
Deleting or hiding a key requires DELETE on that key. Deleting or
masking a value requires KEY_SET_VALUE on its key. Layer-targeted
operations also require permission to write into the layer. Parent
permissions alone do not establish access to descendants.
Where to go next #
Access control on keys
Peios / Using Peios / Security
A registry operation can need rights on both the target key and the layer being changed. Start with the operation's error and the exact key; do not assume that administrator membership or access to a parent grants every registry operation.
Inspect the key's permissions #
Or open Permissions… on the key in Registry Editor. A key's security descriptor contains its owner, DACL and, optionally, SACL. It protects all values in that key; values do not have separate access controls.
reg sd normally shows owner, group and DACL. Reading or changing a SACL
requires the privileged ACCESS_SYSTEM_SECURITY right. If inspection is
denied, ask the key's administrator to review the needed operation rather
than broaden access to the entire registry.
The registry-specific rights #
| Intended operation | Required right on the key |
|---|---|
| Read values | KEY_QUERY_VALUE |
| Write or delete values | KEY_SET_VALUE |
| Create a child key | KEY_CREATE_SUB_KEY on the parent |
| List child keys | KEY_ENUMERATE_SUB_KEYS |
| Watch changes | KEY_NOTIFY |
| Create a link | KEY_CREATE_SUB_KEY and KEY_CREATE_LINK on the parent, plus SeTcbPrivilege or Administrator membership |
| Delete or hide the key | DELETE |
| Read its descriptor and metadata | READ_CONTROL |
| Change its DACL or owner | WRITE_DAC or WRITE_OWNER, respectively |
| Read or change its SACL | ACCESS_SYSTEM_SECURITY |
The common bundles are KEY_READ (query, enumerate, notify and read the
SD), KEY_WRITE (set values and create subkeys) and KEY_ALL_ACCESS.
Ordinary reads and writes use AccessCheck
against the key; privileges do not grant blanket ordinary registry access.
Backup and restore
have their own privilege-gated path.
Also check the destination layer #
A layer-targeted write needs KEY_SET_VALUE on that layer's metadata key
under Machine\System\Registry\Layers\<LayerName>, as well as the
operation's right on the target key. The layer's displayed owner SID is
informational; its metadata key's security descriptor controls access.
On a stock system, base-layer authorization falls back to a descriptor that allows Authenticated Users to write into base, while each target key still controls what they may change.
Creating a layer above precedence 0, or raising a layer's precedence
above 0, additionally requires SeTcbPrivilege. Ordinary writes into an
existing layer use the target-key and layer permissions described above.
Deleting a layer requires DELETE on its metadata key. See the
layer rights reference.
No traversal check #
Opening a key checks that key's descriptor, not every ancestor's. If you know a path, you can sometimes open it even when you cannot list its parent. Conversely, restricting a parent does not automatically protect every existing key below it. Inspect the keys containing the data you need to protect.
Where a key's SD comes from #
Inheritance is computed when a key is created. Changing a parent's permissions later does not automatically rewrite existing descendants; reapplying inheritance requires a deliberate administrative operation.
The standard root defaults are:
Machine: SYSTEM and Administrators have full control; Authenticated Users can read. These grants are inheritable.Users\<SID>: that user, SYSTEM and Administrators have full control.
A component can set tighter permissions on its own subtree. Verify the actual descriptor instead of relying on the default description.
The handle model #
Rights are granted when a key is opened and retained on that handle. Tightening the descriptor affects future opens; it does not revoke an already-open handle. If an access change must affect a running service, arrange for it to reopen the key through its documented reload or restart.
Watching and reading are separate rights #
KEY_NOTIFY permits watching; KEY_QUERY_VALUE permits reading values.
A subtree watcher can learn that a descendant appeared or disappeared
without being allowed to read that descendant's contents. It still must
open the descendant and pass its access check to read it.
The sharp edge: security is not layered #
Before a security change, save an appropriate backup and inspect the current descriptor. Afterward, check the descriptor and test future opens under the relevant identity. A value that contains a descriptor, such as an SdDefaults override, is a different case: it is layered data, but changing it does not retroactively rewrite objects already stamped from it.
Where to go next #
Default security descriptors
Peios / Using Peios / Security
An SdDefaults value tells a component which security descriptor to stamp on an object it creates. Changing it affects future stamping; it does not repair the permissions of objects already on disk or in memory.
Find the component's default #
The convention is a named REG_SZ value containing SDDL under:
Machine\Software\<Software>\SdDefaults
The value name identifies the object, such as SpoolDirectory, StateFile
or Run. These are examples, not names every component provides. Each
component keeps its own names beside its own configuration.
Use regman for the exact key and value before editing it. Read its
Applies field: the relevant object may be stamped at startup, on a
mount, or only when it is first created. Inspect the current override
with reg get or Registry Editor, and record whether it was absent.
Compiled default, registry override #
| Override state | Descriptor the convention selects |
|---|---|
| Absent | The component's compiled-in default. The key need not exist. |
| Present and valid SDDL | The registry override. |
| Invalid | The compiled-in default, with an event identifying the rejection and descriptor in force. |
An invalid descriptor is not repaired or broadened automatically. This fallback does not prove that the policy is the one you intended: inspect the rejection event and verify the actual descriptor on the created object. A syntactically valid descriptor can also grant more access than intended, so validity alone is not a security review.
When a change takes effect #
- Read the value's manual and understand when its component stamps the object.
- Save the old value and the relevant recovery state.
- Make the intended SDDL change with the correct type and destination layer.
- Let the component create or stamp an object through its documented workflow.
- Inspect that object's actual permissions and the component's events.
Changing the default does not rewrite existing objects. Children inherit from their parent at creation, so existing descendants are not updated either. Do not delete a live object merely to force restamping without understanding that component's recovery procedure.
Layer removal can remove the override value, but it cannot undo descriptors already stamped onto objects while the override was effective. This is separate from the rule that a registry key's own descriptor is not layered.
The values are access policy — protect them #
Whoever can write these values controls access to objects the component
will create. The convention protects the SdDefaults key so only SYSTEM
and Administrators can write. Check its actual permissions and the
write destination layer.
Do not confuse the key's Permissions… with the descriptor stored as a value: the first governs who may change configuration, while the second is consumed as policy for another object.
What the convention does not cover #
- Bootstrap seeds: descriptors needed before a registry source exists are compiled into the tools that stamp them.
- Kernel fallback templates: fixed fallbacks used when mount policy has
no template are not SdDefaults settings. An object created without a
parent instead uses the creating token's default DACL;
authdobtains that from the principal'sDefaultDaclpolicy value. A token with none leaves such an object with a null DACL. See Assigning privileges. - Package payload files: these inherit at their destination or use a package-manifest descriptor declaration; see PSPU §5.20.
For component authors #
The convention requires a reviewed compiled default for each object-named
value, fallback to that default with a recorded rejection for invalid
SDDL, and a regman entry specifying when it is stamped. Do not substitute
a broader policy on parse failure.
Where to go next #
Registry startup and configuration
Peios / Using Peios / Administration
LCS uses compiled-in defaults at startup, then reads its settings from
Machine\System\Registry when the Machine hive becomes available.
For routine tuning, inspect and document the specific parameter before
changing it:
Use the normal change-and-verification workflow. These parameters affect the registry itself, so keep changes narrow and record a recovery value before modifying them.
What startup defaults do and do not mean #
The kernel's registry machinery and base layer can operate before there is stored configuration. That does not make hives available before their source registers. Until a source supplies a hive, a path naming it cannot be opened.
The registryd role, supplied by loregd by default, starts early so the
rest of the system can read configuration. Its backing database paths
come from startup arguments rather than registry settings. See
LCS and sources.
When a registry parameter change applies #
LCS watches its configuration and validates changes before publishing new operational parameters; the normal transition needs no LCS restart. Use the parameter's manual and the LCS operational-parameters reference for type, range and effect. That reference also describes the limits of in-flight parameter snapshots; do not assume every ongoing operation changes at exactly the same instant.
It applies reject-or-keep to itself #
A valid parameter is adopted. An invalid value, wrong type or missing
parameter leaves the previously active value in force, which may be the
compiled default or a previously accepted value. Values are not clamped.
The documented rejection event is lcs.config.value.rejected.
Read-back shows stored data, so inspect the registry's events too. A missing whole configuration key during first boot differs from individual missing parameters in an existing key; the TRM describes that distinction.
First boot, with no data #
The source creates hive roots with their initial security descriptors. LCS keeps its defaults while configuration is absent. The init system then restores a seed containing the initial configuration; LCS notices it, reads the settings and validates them.
For a machine that does not reach normal startup, examine the source's
early diagnostic output and the init-system failure information. With
loregd, peinit relays output to the console if the source fails before
readiness; after logging starts, its buffered output reaches eventd.
Do not assume missing configuration alone proves that the databases need
to be replaced or seeded again.
Where to go next #
Backup and restore
Peios / Using Peios / Administration
Use a backup when recovery must preserve a subtree's security descriptors and tagged layer entries. Use a JSON export for a reviewable configuration document. They serve different purposes.
Choose the recovery format #
| Need | Tool | Important limit |
|---|---|---|
| Review or transfer visible keys and values | reg export --json KEY FILE and reg apply FILE | Export does not save key security descriptors or the complete layered store. |
| Recover a subtree with its descriptors and layer entries | reg backup KEY FILE and reg restore KEY FILE | Privileged; restore replaces the target contents and descendants. |
| Recover a managed configuration bundle | The role or policy workflow, or a reviewed layer operation | Layer removal does not restore key permissions or reverse consumer side effects. |
Registry Editor offers these under Files as Export…, Import…,
Back up… and Restore…. Its export is a JSON registry document.
Text-format reg export is for review; text input to reg apply is not
implemented, so use JSON for reapplication.
Take a backup before a risky change #
For example, to capture the KMES configuration subtree:
Confirm the command succeeded and retain the file together with its original key path and the change it precedes. Choose a protected file location: backup privilege can include configuration you could not read through ordinary key access. A failed or interrupted output is not a verified recovery copy.
Backup is a point-in-time snapshot #
A backup captures a consistent subtree while other writes can continue. It includes key descriptors, layer-tagged entries and absence markers, rather than just effective values. The stream has integrity checks to reject truncation or corruption on restore. Integrity does not establish that an unknown backup is trustworthy.
If you restore entries for a layer that no longer exists, and the backup does not restore that layer's metadata, those entries remain inactive until real metadata exists. This matters when recovering after layer deletion or moving configuration to another machine. Review which layer definitions are needed before relying on a narrow subtree snapshot. See the LCS backup reference and restored-layer rules.
Restore is replace, not merge #
Before restoring:
- Confirm the backup is trusted and corresponds to the intended target.
- Save the current target if you need a way back from the restore itself.
- Review the target subtree, affected consumers and restored permissions.
- Confirm needed layer metadata exists or is included, and arrange any required privileges or consumer restart/reload.
- Run restore only after approving the replacement scope.
For the earlier example:
The target key object remains, but its mutable fields, values and contents are restored and its descendants are replaced. Its immutable volatile/link flags must match the backup root. Do not assume a backup can be restored onto any arbitrary key.
The operation uses one transaction. A rejected stream or a failure before commit leaves no partial restored subtree. If the commit times out or reports a source failure, its result can be uncertain: inspect the live state before retrying or reporting that nothing changed.
reg restore prompts on terminal input; with non-terminal input it
proceeds without that prompt. Do not use a script's lack of a prompt as
evidence that the operation is non-destructive.
Both bypass per-key permissions — by design #
Backup requires SeBackupPrivilege; restore requires SeRestorePrivilege.
They bypass ordinary per-key checks and are audited independently of each
key's audit settings.
SeRestorePrivilege alone is not enough for every snapshot. A manifest
layer with precedence above 0, or a matching existing layer above 0,
requires SeTcbPrivilege. Restored layer metadata is also subject to
its positive-precedence check. See the
restore precedence gate.
Verify the recovery #
- Inspect the subtree and representative effective values with
reg tree,reg get -Land Registry Editor. - Inspect key descriptors and
reg layer ls -l; missing or different layer definitions can change what is effective. - Check relevant consumer status and events after the setting's documented application point. Restore notifications require watchers to re-read; they are not consumer acceptance acknowledgements.
- Check fresh opens when restored permissions matter; existing handles retain the rights granted when they were opened.
Where to go next #
LCS and sources
Peios / Using Peios / Administration
The registry depends on both the kernel's LCS subsystem and a userspace
source that persists hive data. On a standard installation, loregd
provides the registryd role and serves Machine and Users.
For ordinary configuration, use reg or Registry Editor. Do not edit a
source's database to bypass the registry's access checks, layer handling
or notifications.
Which component to investigate #
| Symptom or question | Start with |
|---|---|
| What is a setting and when does it apply? | regman and its owning component. |
| Why does a stored value lose to another? | Layers. |
| Why is one operation denied? | Key and layer access. |
| Why are a hive's reads and writes unavailable? | Its source's startup and failure diagnostics. |
| Why did a source response get rejected? | LCS events and the validation reference. |
LCS owns routing, access decisions, effective layer resolution, watches and transaction coordination. Sources persist and return entries; each hive has one source, and one source may serve several hives. Programs reach the registry through LCS rather than talking directly to a source.
The base source: registryd #
registryd is a replaceable role, not a second daemon alongside loregd.
peinit starts its provider early because later services need the registry.
The default provider takes one HiveName=DatabasePath argument per hive;
its own startup does not depend on reading registry configuration.
The reviewed peinit bootstrap passes
Machine=/var/state/loregd/Machine.hive and
Users=/var/state/loregd/Users.hive to the source. These paths are peinit's
boot policy, not defaults chosen by loregd; see the pinned
service definition.
The /var/state/registry/*.regdb paths in generic loregd examples do not
establish the deployed locations. Inspect the installed startup configuration
before assuming these paths apply to a particular image or release. Do not
launch another source against the same databases as an exploratory repair step.
The loregd command-line reference covers arguments and validation. The loregd overview covers packaging and its boot role. Alternative source implementations may have different storage diagnostics.
When a source goes away #
Its hives become unavailable. Open key handles remain, but operations requiring source access fail until it returns. Watches stay armed and receive overflow recovery on re-registration, so consumers must re-read.
To investigate:
- Save the exact failed operation, key and error.
regexit status3means access denied;5includes source and syscall failures. Avoid treating every failure as a permissions problem. - Check the source's diagnostic output and init-system status. For
loregd, peinit relays failures before readiness to the console and carries buffered output into eventd once logging starts. - Establish why the source failed before restarting it or replacing data. When reads work again, re-read the affected keys and verify consumers.
- After a timeout or source failure during a mutation, check state before retrying. The source may have applied it even though the caller did not receive success.
A transaction commit timeout is especially important: neither the timeout nor the transaction's timed-out status proves the writes were rolled back. See Transactions and the LCS late-response reference.
The trust boundary #
A source is part of the trusted computing base. LCS checks access using the descriptors the source returns. It validates response structure, but cannot detect well-formed false data, such as fabricated permissions or layer metadata. Protect the source's service definition, privileges, process and storage as critical system components.
Do not interpret "sources make no access decisions" as "sources need no trust". The LCS source model explains that boundary.
Two operations the kernel coordinates #
- Transactions group changes to one hive into a commit. A source must support the needed transaction mode.
- Backup and restore use the source's storage operations, with LCS enforcing privileges and the backup format. Restore replaces a subtree, including descriptors.
These are administrative interfaces; the private source protocol is not a troubleshooting command surface.
Where to go next #
Registry Editor
Peios / Using Peios / Administration
Registry Editor shows the registry: its keys as a tree, and the values of the key you pick, which you can change. A key is a container in the registry, like a folder, and a value is a named, typed piece of data in a key. It reads the registry as you, so it shows what you may read, and it says what you may not.
Before changing anything #
- Browse to the exact key and inspect its current values. Select a value to see its winning layer and the registry manual beside it.
- Check the manual's expected type, valid values and application timing. A saved value can still be rejected by the component that reads it.
- Check Writes go to in the bar. Your change goes to that layer, which may differ from the layer currently winning the value.
- Record the original state. Use Files → Back up… if recovery needs key permissions and layered entries; Export… is a reviewable configuration document with different coverage.
- Save one intended change, then verify it.
Opening it #
- From the launcher: start Registry Editor. It opens on
Machine. - From a program:
gxwi-registry-editor --key PATHopens on the key atPATH, such asgxwi-registry-editor --key 'Machine\System\Services'.
The window's title names the key shown, such as Registry Editor: Services.
The keys #
On the left is the tree. It starts from three keys:
- Machine holds the machine's own settings.
- Users holds each person's settings, under a key named for their SID, the number that identifies them. The tree shows whose each one is, such as dana, before the SID.
- CurrentUser is your own key under Users, by a shorter name.
Select ▸ beside a key to show the keys under it, and ▾ to hide them. Select a key's name to show its values.
To go straight to a key, type its path in the box at the top and press
Enter or select Go. Write the path with \ between the names, as
in Machine\System\Services; / works too. The tree opens down to the
key. A path with no key at it is refused, with the reason.
The values #
The middle shows the values of the key shown, each with its name, its type and its data. The default value comes first, as (Default): it is the value a key can hold without a name.
Types are given in words:
| It says | Type | Shown as |
|---|---|---|
| Text | REG_SZ | the text |
| Text with variables | REG_EXPAND_SZ | the text, with any %NAME% left as written |
| Link | REG_LINK | the path the link points to |
| List of text | REG_MULTI_SZ | the items, separated by · |
| Number | REG_DWORD | the number, then in hexadecimal |
| Number, big-endian | REG_DWORD_BIG_ENDIAN | the number, then in hexadecimal |
| Large number | REG_QWORD | the number, then in hexadecimal |
| Bytes | REG_BINARY | the bytes, in hexadecimal |
| No type | REG_NONE | nothing |
The registry doesn't check that a value's data fits its type. Data that doesn't fit, such as a number of the wrong length or text that isn't valid UTF-8, is shown in red, as bytes.
A value or a key in full #
Select a value to see it in full in the pane on the right: its type, its size, the layer its data came from, and all of its data. A layer is a set of registry entries that can override another. Bytes are shown eight to a row, after the offset of the first.
With no value selected, the pane describes the key: when it last changed, how many keys and values it holds, and whether it is a link or is kept only until the machine restarts. Copy path copies the key's path.
What the registry manual says #
Packages document their keys and values in the registry manual, which
regman shows in a terminal. Registry
Editor shows the same, at the foot of the pane:
- For a key, what it is for.
- For a value, the type the manual expects, its default, the values it may take, and when a change applies: at once, when the program that reads it next starts, or when the machine next starts. Then what it is for.
Values the manual documents that aren't set on the key are listed after the values that are, greyed, with Not set and the default the manual gives, as in Not set · default 30. Check the manual for the consumer's missing-value behavior; some retain a previously active value. These manual-only entries aren't stored values, so they aren't counted or exported. Select one to see what the manual says of it, and double-click it or choose Set… to start a new value with its name, the type the manual gives it and, where the default is a plain value, the default.
If a value's type isn't the one the manual expects, the pane says so in red, since whatever reads it may not accept it. A key or value the manual doesn't mention says so too.
Changing values #
To change a value, double-click it, or select it and choose Edit… in the pane or on its right-click menu. The pane shows a form for its type:
- Text and Link take one line.
- List of text takes one item to a line. Empty lines are left out.
- Numbers take a whole number, typed in Decimal or Hexadecimal. Changing between them rewrites the number in the other form.
- Bytes take two hexadecimal digits to a byte. Spaces and new lines are ignored.
Data that doesn't fit its type is edited as bytes, and keeps its type. Save writes it, and Cancel or Esc leaves it as it was.
If Save reports a concurrent change, it refuses the edit and refreshes the list. Review the current value and destination layer before choosing Save again; submitting your edit again can replace another writer's entry in that layer. Conditional-write checks are scoped to the destination layer, so do not assume they guard every change to another layer's effective winner. See Transactions.
New value… adds a value to the key shown. Give it a name, or leave the name empty for the key's default value, and pick its type: text, text with variables, a list of text, a number, a large number or bytes. A name the key already uses is refused.
To delete a value, select it and choose Delete…, then confirm.
Changing keys #
- New key… creates a key under the one shown, and shows it.
- Delete key… deletes the key shown and everything under it, keys and values, after asking. It is all or nothing: if any part can't be deleted, nothing is. Links under it are deleted; the keys they point to are not. Large trees can exceed file-descriptor or transaction limits and be refused. Review the deletion limits and agree a smaller deletion scope before splitting the operation: separate deletions are no longer one atomic change.
The root keys can't be deleted. Changes go to the base layer, the one every change goes to unless it names another.
The key shown has these on its right-click menu in the tree too, with Copy path.
Layers #
A layer is a set of registry entries that can override another. Layers… in the bar opens them in a window of their own, each with its precedence, whether it is enabled, and who owns it. Where entries for the same value are in several layers, the one in the layer of highest precedence wins; at equal precedence, the most recent write to that value wins. Different values can have different winning layers.
- New layer… creates one. A layer may be disabled from the start: it then takes part only for programs that name it.
- Disable and Enable switch a layer off and on.
- Set gives a layer a new precedence.
- Delete… deletes a layer after asking, and every entry written into it goes with it.
A precedence above 0 needs the privilege to act as part of the operating system (SeTcbPrivilege), which administrators don't hold, so for them only 0 may be set, and the window says so. The base layer is always there, at precedence 0, and can't be changed.
Writes go to, in the bar, picks the layer your changes are written into, base unless you choose another. You need to be allowed to write into it as well as to change the key: if you may not, the key is shown read-only, with the reason. Deleting a value deletes only that layer's entry for it; another surviving entry can then show. Deletion does not necessarily leave the value absent or select its documented default.
The pane says which layer a value's data came from. The registry can't yet list a value's entries in the layers beneath the one that wins, so only the winning entry is shown. If your changes go to a lower layer than the one a value comes from, the pane warns that they won't show while that entry is there.
Permissions #
Each key has its own permissions, which say who may read it, change it and so on. Permissions… in the pane, or on the key's right-click menu, opens them in the permissions editor. Read covers reading the key's values and listing the keys under it, and Write covers changing values and creating keys. The editor adds inheritable grants for children. Inheritance is computed at creation; changing the parent does not by itself rewrite keys that already exist. Inspect the actual descriptors on existing descendants.
Some values hold a security descriptor: a list of who may do what, kept
for a program that reads it, such as a service's ServiceSecurity.
Registry Editor recognises one by its bytes, and shows it as Security
descriptor, written in SDDL, a short text form, rather than as bytes.
Permissions… beside it opens it in the permissions editor. What its
rights mean is the reading program's to say, so only the general ones
are named there: Full control, Read, Write and Execute.
Anything else it grants is shown as special and kept as it is. To change
one, you need to be allowed to change the key's values; otherwise it
opens read-only and says why. For a service's own permissions, Services
Manager names its rights properly: see Who can manage a
service.
Files #
Files, in the key's pane, takes the key to a file and back. Each opens a file dialog, where you choose where to save or what to open:
- Export… writes the key and everything under it to a registry
document, a JSON file of keys and values that you can read and change
by hand, and that
reg applyreads too. It keeps every value exactly. - Import… reads a registry document and, after showing where it will write, writes it into the registry, into the layer chosen in the bar. It is all or nothing: if any part can't be written, none is.
- Back up… copies the key and everything under it, permissions and layers included, to a backup file.
- Restore… replaces the key's contents and subtree and restores its security descriptor from a backup, after asking. It does not merge with the current contents.
Backing up needs the privilege to back up files and keys
(SeBackupPrivilege), and restoring the privilege to restore them
(SeRestorePrivilege). Administrators hold both. Without them, the
buttons are shown disabled, with the reason. A backup containing or
matching positive-precedence layers additionally needs SeTcbPrivilege
for restore; having the restore privilege alone is not enough.
An export does not include key permissions or the full set of layer entries. A backup preserves tagged entries, but its layer manifest does not recreate missing layer definitions. Before recovery after layer deletion, check whether the needed metadata was included. See Backup and restore.
Review the target before importing or restoring. A restore can rewrite owners and permissions throughout that subtree. If a write or commit times out or reports a source failure, refresh and inspect state before retrying; the error does not prove that it was rolled back.
The file dialog shows one folder at a time, starting in your home folder. Open a folder to go into it; Up and Home move about, and you can type a path. It can also make a folder, rename and delete, as you, and says which of those you may not do, and which folders you may not list. Saving over a file that is there asks first.
Changes made elsewhere #
The window watches the keys it shows. A value or key added, changed or removed by a program or in a terminal appears within a moment, with no need to refresh. Refresh, or F5, reads everything again.
What you may not see #
Each key has its own permissions, and the registry checks only the key being opened, not the keys above it. So:
- A key you may not list shows You may not list this key's subkeys. under it in the tree. A key you may not read at all shows You may not read this key. where its values would be.
- You may still go to a key under one you may not list, by typing its path. The tree then shows it, in italics, under the key that couldn't be listed.
Some keys let you list their subkeys but not read their values, or the other way round. Registry Editor shows whichever you may, and says which you may not.
What you may change of a key is its own permissions' to say: changing its values, creating keys under it and deleting it are each a separate right. Registry Editor asks the registry which of them you hold. What you may not do is still shown, greyed out, and the pane says why, such as You may read this key but not change it.
In a terminal #
reg does the same in a terminal. The tree is reg ls and reg tree, the
values are reg get on a key, and a value in full is reg get KEY NAME.
Saving a value is reg set, a new key is reg new, and deleting is
reg del, with -r for a key and everything under it. Export and
import are reg export --json and reg apply, back up and restore are
reg backup and reg restore, and layers are reg layer. See
reg.
Verify a saved change #
After saving, inspect the displayed value, type and winning layer. Use Refresh or F5 for a fresh read. If a different layer still wins, check Layers rather than saving repeatedly.
Then follow the manual's application timing and inspect the consuming component's status and events. An updated row proves what is stored, not that a service accepted it. Follow Change a setting and verify it for rejected settings and recovery choices.
The registry manual (regman)
Peios / Using Peios / Administration
Configuration, not storage established that the registry holds values but not their meaning: it keeps a type tag and some bytes, and never knows that BufferCapacity must be a power of two or what it is for. That knowledge has to live somewhere a person can look it up. It lives in regman, the registry's manual.
regman is the man of the registry. Give it a path and it tells you what a key or value actually is. It is the everyday tool for configuring a Peios system — before you touch a knob, regman is how you find out what it does and what changing it will cost.
regman, in one sentence #
regman <path> [value] documents what a registry key or value means — its type, default, valid values, when a change takes effect, and a prose description — drawn from shipped documentation, never from the live registry.
That last clause is the one to hold onto; there is a section on it below.
Looking up a value #
Give regman a key path and a value name and it prints a knob-card — an identity line, an aligned block of facts, and a description:
$ regman 'Machine\System\KMES' BufferCapacity
Machine\System\KMES BufferCapacity documented by kmes
Type REG_QWORD
Default 4194304 (4 MB)
Valid 65536–268435456 bytes (64 KB–256 MB), power of two
Applies live — ring-buffer swap
Per-CPU ring buffer capacity, in bytes. Must be a power of two; values
that are not are treated as invalid and ignored.
Every registry value is a knob, and every knob has the same few facts worth knowing, so the card is uniform rather than free-form:
| Field | Tells you |
|---|---|
| Type | The value's registry type (REG_QWORD, REG_SZ, …). |
| Default | The value used when nothing is set. |
| Valid | The range, set, or constraint a sensible value must satisfy. |
| Applies | When a change takes effect — live, on restart, or on reboot. |
The Applies field is the one operators reach for most: it is the difference between "edit it and you're done" and "edit it and schedule a reboot". Only the fields that make sense for a given item are shown, and a knob that is being retired carries a prominent deprecation notice at the top of its card.
Looking up a key #
Give regman just a key — no value — and it does double duty as an index: the key's own description, then a list of every value documented under it, one summary line each.
$ regman 'Machine\System\KMES'
Machine\System\KMES documented by kmes
The KMES event subsystem reads its tuning parameters from the values
under this key...
Values
BufferCapacity Per-CPU ring buffer capacity in bytes.
MaxEventSize Max total size of a userspace-emitted event.
MaxNestingDepth Max nesting depth for userspace event payloads.
MaxEmitRatePerProcess Max events per second a single process may emit.
So you can start at a subtree, see what is configurable there, and drill into any single value.
Keys that come in families #
Much of the registry is not at a fixed path. Every service has a key under Machine\System\Services\, every interface one under Machine\System\Network\Interfaces\, every principal one under Machine\Generic\Authn\Policy\. You ask about a real one — sshd, an interface id — and the documentation is written once for the whole family.
regman resolves that for you. Ask about the concrete path and you get the family's page:
$ regman 'Machine\System\Services\sshd' ImagePath
Machine\System\Services\<name> ImagePath documented by peinit
Type REG_SZ
Default (required)
Valid a non-empty absolute path
Applies restart
Absolute path to the service binary.
The heading keeps the <name> so you can see you have been handed the page for the family rather than one written about sshd specifically. Everything on the card applies to the key you asked about.
A heading with a trailing ... — Machine\System\Network\Rules\<Layer>\<rule...> — means the family is a tree and the page covers it at any depth. That is what firewall rules are: an exception is a subkey of a rule, and a rule nested five deep has the same values meaning the same things as one at the top, so one page answers for all of them.
If a package has documented your exact key — its own service's knob, a particular principal's privileges — that page wins instead, and the heading shows the concrete path. The more specific documentation always answers.
Searching, when you do not know the path #
If you don't know where a setting lives, regman -k searches names and summaries — the registry's apropos:
$ regman -k buffer
Machine\System\KMES BufferCapacity Per-CPU ring buffer capacity in bytes.
regman documents intent, not current state #
This is the boundary that matters most. regman tells you what a setting should be — what it means and which values are legal — not what it is currently set to on this machine. It reads shipped documentation, never the live registry; it does not talk to LCS at all. Ask regman about BufferCapacity and you learn it must be a power of two and defaults to 4 MB; you do not learn that this particular box has it set to 8 MB right now. Reading the current value is a separate, live query against the registry — that is reg's job. regman tells you a knob should be a power of two; reg get tells you what it is set to, and reg set changes it. Reach for regman to decide what to write, and reg to write it.
Put regman next to the other two things from Configuration, not storage and a clean division of labour appears:
| To learn… | Look at… |
|---|---|
| What a setting means, and what is valid | regman — the manual |
| What the value is set to | reg — a live query against the store |
| What the subsystem is actually running on | the event log |
regman is the one you consult first, and the only one that works before you have changed anything — because documentation ships with the software, not with the machine's state. It is the natural complement to reject-or-keep: regman tells you the valid range up front, so you set a good value the first time instead of writing one the owning subsystem will quietly refuse.
Where the documentation comes from #
regman has no built-in database of every setting. Each package ships its own documentation as files dropped into a well-known directory (/usr/share/regman/), one fragment per package documenting that package's whole configuration surface. Install a package and its registry documentation appears; remove the package and it goes away. The package that owns a set of keys is the package that documents them — the only arrangement that stays correct as the system changes.
A consequence worth knowing: regman can only describe settings that some installed package has documented. An undocumented key is invisible to it — regman is exactly as complete as the packages on the machine make it. If two packages happen to document the same item, regman shows both and flags the overlap rather than silently picking a winner.
(For the people who write that documentation, regman fmt and regman lint prepare and check fragments, and regman index keeps lookups fast on large corpora — the lookup is always correct without an index, which is purely an accelerator. These are packaging concerns rather than everyday operator ones.)
Where to go next #
For the idea regman exists to serve — why the registry holds values without holding their meaning, and what reject-or-keep means — read Configuration, not storage.
For the other half of the picture — reading what a value is actually set to, which is a live query against the store rather than a documentation lookup — read LCS and sources.
reg
Peios / Using Peios / Administration
reg is the command-line interface to the live registry — the running LCS store, reached through the registry system calls. Where regman reads shipped documentation and tells you what a key means, reg talks to the kernel and reads or writes what a key is. It is the everyday tool for scripting configuration: querying effective values, writing to layers, masking and hiding, managing security descriptors, watching for change, and taking backups.
reg <command> [options] <key> [value] [data]
reg is the read/write half of the registry toolset and regman is the lookup half. The division is exact:
| To… | Use… |
|---|---|
| Read or change what a value is set to, on this machine, right now | reg |
| Look up what a value means — its type, default, valid range, when it applies | regman |
Consult regman first to learn the legal range for a knob, then use reg set to write a value inside it. reg never checks a value's meaning and regman never touches the live store; they are complementary.
How reg treats access and privilege #
Every reg operation goes through the kernel's access check against the single key it opens — the same access-control path as any other protected object, with no traversal check on the parent keys. reg performs no identity or membership checks of its own: it does not refuse a privileged operation up front. It attempts the operation and reports faithfully whatever the kernel returns. An operation that needs a privilege you lack (creating a link, a positive-precedence layer, a backup or restore, reading a SACL) simply fails with access denied (exit 3) rather than being blocked by the tool. A single reg command may legitimately span privilege tiers.
Addressing model #
Almost every subcommand shares one addressing scheme: a key path positional, and an optional value name positional. Understanding the split is most of understanding reg.
Key paths #
The key is a positional path argument. Both / and \ are accepted as separators — neither character is legal inside an LCS key component, so there is no ambiguity, and you may use whichever your shell quotes most comfortably:
reg get Machine/System/KMES
reg get 'Machine\System\KMES' # identical
A leading separator is optional and ignored (/Machine/X is the same as Machine/X). Paths are compared case-insensitively. The first component is the hive:
| Path | Meaning |
|---|---|
Machine\… | The machine hive — system-wide configuration. |
Users\<SID>\… | A specific principal's hive. |
CurrentUser\… | A kernel alias, rewritten to the caller's own Users\<SID>\ at the syscall boundary. |
On output, reg displays paths with a backslash by default. --sep=/ (or REG_SEP=/) switches display to forward slash for that invocation; this affects display only, never how a path is parsed.
The value-name positional #
The value name is a separate positional argument — it is never folded into the key path. This is deliberate: an LCS value name may itself contain / or \ (which a key component may not), so keeping them apart removes all ambiguity.
reg get Machine/System/KMES BufferCapacity
# └──── key path ─────┘ └── value ──┘
The presence or absence of the value argument selects what the command targets:
- No value argument ⇒ the command targets the key itself — its metadata, its values as a set, its security descriptor, its children.
- A value argument ⇒ the command targets that one named value.
- The default value (the empty-name value) is addressed by the literal token
@in the value-name position, mirroring.regconvention:reg get Machine/App @.
This split applies uniformly to get, set, del, mask, and unmask.
Value literals and types #
On set (and in a batch), the value's data is a single token whose registry type is either forced by a type: prefix or inferred from its shape. The registry value types and their literal syntax:
| Prefix | Registry type | Data syntax |
|---|---|---|
sz: | REG_SZ | UTF-8 string (rest of token, verbatim). |
expand: | REG_EXPAND_SZ | UTF-8 string, %VAR% left unexpanded on disk. |
dword: | REG_DWORD | 42 or 0x2A; must fit u32. |
dword-be: | REG_DWORD_BIG_ENDIAN | 42 or 0x2A; must fit u32. |
qword: | REG_QWORD | 42 or 0x2A; must fit u64. |
multi: | REG_MULTI_SZ | Comma-separated; \, escapes a literal comma. |
hex: / bin: | REG_BINARY | Hex bytes; :, -, and space separators are ignored. |
link: | REG_LINK | An absolute key path (a symlink target). |
none: | REG_NONE | No data (token must be empty after the prefix). |
When the substring before the first : is not a recognised keyword (for example http://host), the token is not treated as typed and inference applies:
| Token shape | Inferred type |
|---|---|
all decimal digits, fits u32 | REG_DWORD |
all decimal digits, fits u64 but not u32 | REG_QWORD |
0x… hex, ≤ 8 significant hex digits | REG_DWORD |
0x… hex, ≤ 16 significant hex digits | REG_QWORD |
| anything else (including empty) | REG_SZ |
Inference is intentionally broad, so any all-digit token becomes a number — a PIN, a zip code, or 007 becomes a REG_DWORD and loses its textual form. To force a string, prefix it with sz: (sz:007 stores the string 007; sz:dword:42 stores the literal string dword:42). Because the coercion is a known trap, reg set always echoes the resolved type on success, so a surprising conversion is never silent even under --quiet.
Global options #
Every subcommand accepts the following. Each behavioural toggle also has an environment-variable equivalent, so it can be set per-invocation (the flag) or once per shell or script (the variable). The flag wins over the variable, which wins over the built-in default.
| Option | Env var | Effect |
|---|---|---|
--json | REG_JSON=1 | Emit structured JSON instead of human text. watch emits JSON-lines. |
-v, --verbose | REG_VERBOSE=1 | More detailed output. |
-q, --quiet | — | Suppress non-essential output. (A surprising type coercion is still reported.) |
--sep=CHAR | REG_SEP | Path display separator: \ (default) or /. |
--help | — | Print help for the command and exit 0. |
--version | — | Print the version and exit 0. |
Mutating subcommands additionally accept:
| Option | Env var | Effect |
|---|---|---|
--layer NAME | REG_LAYER | Target layer for the write (default: the base layer). |
-y, --yes | REG_ASSUME_YES=1 | Skip the confirmation prompt on a destructive command. |
Destructive commands (del -r, restore, layer del) prompt for confirmation when standard input is a terminal, and auto-proceed when it is not — so interactive use is guarded while scripts are never blocked. Set -y/--yes or REG_ASSUME_YES=1 to skip the prompt explicitly.
Reading #
reg get <key> [value] #
Read the effective (resolved) value. With a value, prints that value's data; with no value, lists the key's effective values (the default @ value first, then named values sorted case-insensitively) — a shorthand for ls --values-only.
| Option | Effect |
|---|---|
-L, --layers | Annotate the value with the layer it resolved from and its sequence number. |
--raw | Write the value's raw bytes to standard output verbatim — for piping REG_BINARY data. |
--no-follow | Operate on a symlink key itself rather than following it to its target. |
The single-value default form prints data bare (no quotes; one element per line for REG_MULTI_SZ) so it pipes cleanly. -L shows the winning layer's provenance:
$ reg get Machine/System/KMES BufferCapacity
4194304
$ reg get Machine/App Theme -L
Theme = REG_SZ "dark" (layer: policy, seq 7)
Only the winning value is shown; the full shadowed stack beneath it is not yet exposed by any client ABI (the -L flag will render the whole stack unchanged once that primitive lands). See Layers for what "effective" resolves against.
reg ls <key> #
One-level listing of a key's immediate subkeys and values.
| Option | Effect |
|---|---|
-l, --long | Long form: for subkeys, child and value counts; for values, type and byte size. |
--keys-only | List subkeys only. |
--values-only | List values only. |
$ reg ls Machine/System/KMES -l
BufferCapacity = REG_QWORD 4194304 (8 bytes)
MaxEventSize = REG_DWORD 65536 (4 bytes)
reg tree <key> #
Recursively list the subkey tree rooted at key.
| Option | Effect |
|---|---|
--depth N | Limit recursion to N levels. |
--values | Include each node's values, not just its keys. |
$ reg tree Machine/System --depth 2
reg info <key> #
Print a key's metadata: leaf name, subkey and value counts, last-write time, hive generation, security-descriptor size, and the volatile and symlink flags.
| Option | Effect |
|---|---|
--no-follow | Inspect a symlink key itself rather than its target. |
$ reg info Machine/System/KMES
path Machine\System\KMES
name KMES
subkeys 0
values 4
last_write_ns 1717243800000000000
hive_generation 42
sd_size 164
volatile false
symlink false
Writing #
reg set <key> <value> <data> #
Create or update a value. The value name (or @) and the data token are both required. Writes are tagged with --layer (default: base). See value literals for the data syntax.
| Option | Effect |
|---|---|
--layer NAME | Write into layer NAME instead of the base layer. |
-p, --parents | Create any missing ancestor keys. |
--expected-seq N | Compare-and-swap guard: apply only if the value's current sequence is N; a mismatch exits 6 without writing. |
$ reg set Machine/App Build 4096
set Machine\App Build = REG_DWORD 4096 (layer: base)
$ reg set Machine/App Servers multi:alpha,beta --layer policy
--expected-seq supports lock-free read-modify-write: read the value with -L to learn its sequence, then write back with --expected-seq set to that number; if another writer changed it in between, your write fails cleanly instead of clobbering theirs.
reg new <key> #
Create a key with no values. Reports whether the key was created or already existed.
| Option | Effect |
|---|---|
--layer NAME | Create the key in layer NAME. |
-p, --parents | Create any missing ancestor keys. |
--volatile | Create a volatile (RAM-only) key that does not survive a reboot. |
$ reg new Machine/App/Cache --volatile
created Machine\App\Cache (layer: base)
reg del <key> [value] #
Delete a value, or a key. (Alias: delete.)
With a value, deletes this layer's entry for that value only — lower-layer entries resurface, because a per-layer delete is not a tombstone (use mask for that). With no value, deletes the key, which must be empty unless -r is given.
| Option | Effect |
|---|---|
--layer NAME | Delete from layer NAME. |
-r, --recursive | Delete the key and all its descendants, in one transaction: all of them go, or, on any failure, none do. Links under the key are deleted, not followed. Every key in the tree is opened before any is deleted, so a tree larger than reg may hold open at once (1,024 files by default) is refused whole, as is one of more than 4,096 keys, which is all a transaction holds. |
-y, --yes | Skip the confirmation prompt (recursive deletes prompt on a terminal). |
$ reg del Machine/App Legacy
deleted value Legacy from Machine\App
$ reg del Machine/App/Temp -r
Recursively delete key Machine\App\Temp and all its contents? [y/N] y
deleted Machine\App\Temp (3 keys)
See Deleting keys and values for how a per-layer delete differs from a tombstone.
Masking and hiding #
These commands expose LCS's tombstone and hide primitives, which mask lower-precedence state rather than removing a layer's own entry — the distinction that makes layers revertible. The layer is chosen with --layer (default: base).
reg mask <key> [value] / reg unmask <key> [value] #
mask sets a tombstone: a per-value marker in the target layer that hides that value from all layers below it. unmask clears it.
| Option | Effect |
|---|---|
--layer NAME | The layer that carries the tombstone. |
--all | Blanket tombstone: mask all lower-layer values on the key (no value argument). |
A value is required unless --all is given.
$ reg mask Machine/App ApiKey --layer policy
set tombstone on Machine\App ApiKey (layer: policy)
$ reg mask Machine/App --all --layer policy
set blanket tombstone on Machine\App (layer: policy)
reg hide <key> / reg unhide <key> #
hide installs a HIDDEN path entry in the target layer, masking the key's existence in that layer and below. unhide removes that layer's path entry, letting the key reappear.
| Option | Effect |
|---|---|
--layer NAME | The layer that hides (or unhides) the key. |
$ reg hide Machine/App/Debug --layer policy
hid Machine\App\Debug (layer: policy)
Layers #
reg layer ls #
List layers with name, precedence, enabled state, and (informational) owner SID, ordered by descending precedence. The base layer is always listed. A layer whose metadata values have the wrong type is marked (malformed metadata): the registry goes by what it last knew of that layer, not by what is shown.
| Option | Effect |
|---|---|
-l, --long | Long form. |
$ reg layer ls
policy prec=100 enabled owner=S-1-5-32-544
base prec=0 enabled
reg layer new <name> #
Create a layer by writing its metadata key under Machine\System\Registry\Layers\, with its precedence and enabled state, in one transaction.
| Option | Effect |
|---|---|
--precedence N | Precedence (higher wins). A precedence above 0 requires SeTcbPrivilege; the tool attempts it and reports any denial. |
--owner SID | Record an informational owner SID, stored as a binary SID. Without it, the owner is whoever creates the layer. |
--disabled | Create the layer disabled. |
$ reg layer new policy --precedence 100 --owner S-1-5-32-544
created layer policy (precedence 100, enabled)
reg layer set <name> #
Modify an existing layer's metadata.
| Option | Effect |
|---|---|
--precedence N | Change the precedence. |
--enable / --disable | Enable or disable the layer. |
--owner SID | Change the informational owner SID. |
reg layer del <name> #
Delete a layer — removes its metadata key; LCS tears down the layer's entries. Prompts for confirmation on a terminal.
$ reg layer del policy
Delete layer policy and all its entries? [y/N] y
deleted layer policy
Managing per-process private layer views is out of scope for reg; see private hives and layers.
Security descriptors #
reg sd <key> #
Show or set a key's security descriptor as SDDL, using the same codec as the sd tool, so its output is consistent. With no scope flags, the default is owner + group + DACL (a SACL requires the privileged ACCESS_SYSTEM_SECURITY right).
| Option | Effect |
|---|---|
--set SDDL | Apply the given SDDL. Without it, sd prints the current descriptor. |
--owner | Scope to the owner. |
--group | Scope to the group. |
--dacl | Scope to the DACL. |
--sacl | Scope to the SACL (needs ACCESS_SYSTEM_SECURITY). |
$ reg sd Machine/App
O:BAG:BAD:(A;;KA;;;BA)(A;;KR;;;WD)
$ reg sd Machine/App --set 'O:BAG:BAD:(A;;KA;;;BA)' --owner --dacl
set security descriptor on Machine\App
A security-descriptor change takes effect on future opens only (existing handles keep the access mask they were granted). Security changes are not layered and are not undone by layer removal — they mutate the key object directly. See Access control on keys.
Symlinks #
reg link <key> <target> #
Create a symlink key pointing at the absolute target key path. This creates a key with the immutable symlink flag and a default REG_LINK value holding the target. Requires KEY_CREATE_LINK and SeTcbPrivilege/Administrator; the tool attempts it and reports any denial.
| Option | Effect |
|---|---|
--layer NAME | Create the link key in layer NAME. |
$ reg link Machine/App/CurrentConfig Machine/App/Config/v3
linked Machine\App\CurrentConfig -> Machine\App\Config\v3 (layer: base)
get and info follow symlinks to their target by default; pass --no-follow to inspect the link key itself. See Advanced: registry links.
Watching #
reg watch <key> #
Arm a change watch and stream one event per change until interrupted (or until --count events). Each event faithfully means "something under the filter changed — re-read"; reg does not decode the change records' contents. With --json, emits one JSON object per line.
| Option | Effect |
|---|---|
--subtree | Watch descendant keys too, not just this key. |
--filter LIST | Comma-separated subset of value, subkey, sd (default: all). |
--count N | Exit after N events. |
$ reg watch Machine/System/KMES --subtree --filter value
changed: Machine\System\KMES
changed: Machine\System\KMES
See Watching for changes for why a service watches instead of polling.
Batch, export, backup, and restore #
Two paths move a subtree around: export/apply are the portable, reviewable path (a diffable text or JSON document); backup/restore are the exact, privileged path (an opaque kernel snapshot).
reg apply <file> #
Apply a batch of operations to one hive in a single transaction — all-or-nothing. All operations must target one hive (a cross-hive batch fails with exit 4). - reads standard input.
| Option | Effect |
|---|---|
--dir DIR | Apply every batch file in DIR (sorted), each as its own transaction. A missing or empty directory applies nothing and succeeds. |
--once-delete | Delete each batch file after it applies successfully (a drain). |
-y, --yes | Skip confirmation. |
The batch format is JSON (canonical and exact) or a line-oriented text format; apply auto-detects (a leading { or [ means JSON). Text-format input is not yet implemented — supply JSON, or generate one with reg export --json. Text export works for review.
$ reg export --json Machine/App - | reg apply -
applied 4 keys
A JSON batch is an object with a keys array, parents before the keys under them. Each entry has a path, an optional values array (each value {name, type, data}, or hex in place of data, as in the output formats below), and an optional descriptor: security descriptor parts in SDDL, set on the key in the same transaction as its values. Only the parts the SDDL gives are set, so "descriptor": "S:(AL;CI;0x10002;;;WD)" sets the SACL and leaves the owner, group and DACL the key inherited. Setting a SACL needs SeSecurityPrivilege, as reg sd --sacl does; a descriptor that does not parse, or that gives no part at all, fails the batch before anything is written. export never writes a descriptor.
A key entry with a member apply does not know is refused rather than ignored. Versions of reg before descriptor existed ignore it and report success without setting anything, so a batch that carries one needs a reg that knows it.
reg export <key> [file] #
Dump a subtree to the batch format. Omit file (or pass -) to write to standard output. Human-readable text by default; --json emits the canonical exact form.
| Option | Effect |
|---|---|
--layer NAME | Export one layer's view (default: the effective state). |
$ reg export Machine/App app-config.reg
reg backup <key> <file> #
Write an opaque, exact, binary kernel snapshot of a key and its subtree to file. Requires SeBackupPrivilege.
$ reg backup Machine/System/KMES kmes.snap
backed up Machine\System\KMES -> kmes.snap
reg restore <key> <file> #
Replace a key and its entire subtree from a snapshot, in one transaction. Requires SeRestorePrivilege. Prompts for confirmation on a terminal.
$ reg restore Machine/System/KMES kmes.snap
Replace key Machine\System\KMES and its entire subtree from kmes.snap? [y/N] y
restored Machine\System\KMES from kmes.snap
See Backup and restore for when to reach for each path.
Output formats #
Default output is terse, human-readable, and grep-friendly. --json switches every command to structured output — a single self-contained JSON document, except watch, which emits JSON-lines (one object per event). In any value listing, the default (@) value is emitted first, then named values sorted case-insensitively.
Type formatting on read:
| Registry type | Human form | JSON form |
|---|---|---|
REG_SZ / REG_EXPAND_SZ | the string | {"type":"sz","data":"…"} |
REG_DWORD / REG_QWORD | decimal | {"type":"dword","data":42} |
REG_MULTI_SZ | one element per line | {"type":"multi","data":["a","b"]} |
REG_BINARY | hex | {"type":"binary","data":"deadbeef"} |
REG_LINK | the target path | {"type":"link","data":"…"} |
The registry doesn't check that a value's bytes fit its type. Bytes that don't are shown as hex, in both forms, under the value's own type. Examples are a string that is not UTF-8, a number of the wrong length, and REG_NONE with data. In the JSON form such a value has a hex member, its bytes exactly, in place of data, as {"type":"sz","hex":"ff00"}: it survives export --json and apply unchanged.
Exit status #
| Code | Meaning | Typical cause |
|---|---|---|
0 | Success. | — |
1 | Usage error. | Bad arguments; a required option missing. |
2 | Key or value not found. | ENOENT. |
3 | Access denied. | EACCES, EPERM — the kernel refused the operation. |
4 | Invalid specification. | Bad path, type, or literal; a cross-hive batch; a name too long (EINVAL, EXDEV, ENAMETOOLONG). |
5 | Syscall or source failure. | EIO, ETIMEDOUT, ENOSPC, ENOMEM. |
6 | Compare-and-swap conflict. | EAGAIN — an --expected-seq guard did not match; the value changed under you. |
A denial (3) reports the operation, the target, and, where relevant, the access that was required — in the style of the sd and token tools.
See also #
regman— the registry manual: what a key or value means, as opposed to what it is set to. Consult it before youreg set.- Keys, values, and types — the data model
regaddresses. - Layers — what "effective" resolves against, and what
--layer,mask, andhideoperate on. - Access control on keys — the check every
regoperation goes through.
Private hives and layers
Peios / Using Peios / Advanced
A service or sandbox can see different registry data from an administrator looking up the same path. Before concluding that the store is inconsistent, check whether the workload uses a private hive or private layer through its effective thread credentials.
This page explains the diagnostic distinction. It does not provide a
command for creating private views; reg does not manage their attachment.
Use the workload's documented isolation mechanism and the technical
references below.
Private hives #
A private hive is visible to threads whose tokens carry its scope identity.
Private hives are considered before global hives, so a private Machine
can shadow the global Machine for that thread without changing the path.
An operator reading the global path therefore has not necessarily inspected the workload's configuration. Identify which credentials and scope the workload actually uses before comparing values or planning recovery. Do not assume a global-hive backup captures a separate private hive.
Private layers #
A disabled layer can be active for a thread whose credentials name it. It then competes using the normal precedence-and-write-order rules. Disabling it globally does not remove it from those private views.
This is per thread, not necessarily per process: different impersonation tokens can give threads in one process different views. Check the relevant request or service identity rather than only the process's nominal identity.
Typical uses are session-specific overrides, test settings and sandbox configuration. Those uses do not establish who may attach a private view.
Authorisation lives in KACS #
The token mechanism governs attachment. The kernel TRM documents scope
GUIDs and private-layer names entering credentials at token creation,
gated by SeCreateTokenPrivilege. That is a broad token-creation gate,
not per-scope ownership authorization.
With the PEI-268 kernel fix, attaching a layer above precedence zero also
requires enabled SeTcbPrivilege on the token's creator. That authorization
stays with the attachment, so an ordinary process can use a view deliberately
delegated by a trusted service. An attachment made without that authorization
cannot gain high-precedence access through a later layer promotion: registry
operations using that attachment fail with EPERM while the layer is above
zero. Older kernels lack this check.
Use the private-hive and private-layer TRM sections for the credential model and its documented limitations. Configured limits can also reject a registry operation when the token carries too many scopes or private layers; this is not automatically a key-permission problem.
Where to go next #
Transactions
Peios / Using Peios / Advanced
Use a transaction when several registry changes must become visible
together. reg apply and Registry Editor's Import… provide an
operator-facing way to apply a registry document as one batch.
Apply one reviewed document #
Prepare or review a JSON registry document, then apply it with:
reviewed.json is your reviewed file. All its operations must target one
hive; cross-hive batches are rejected even if both hives use the same
source. JSON is supported input; text-format batch input is not implemented.
reg export --json KEY FILE creates a reviewable starting point containing
keys and effective values. It does not export key descriptors. A manually
prepared document can include descriptor changes, which must be reviewed
as security changes and need the relevant rights. See
reg apply for its schema and
version caveat for descriptor support.
A successful transaction publishes its changes together. Other readers do not see intermediate writes; reads inside a bound transaction see their own pending writes. This is registry atomicity, not a guarantee that all consuming services will apply the new configuration simultaneously.
Limits worth knowing #
- One hive per batch. Cross-hive atomicity is not available.
- Directory imports are separate batches.
reg apply --dir DIRapplies each file in its own transaction; the directory is not one atomic unit. - No nesting or savepoints. Keep each document's recovery scope clear.
- Bounded time and size. A transaction can time out or exceed resource
limits.
reg applyerrors need investigation, not an assumption that splitting the file preserves its consistency requirements. - Source support is required. A source can reject the needed transaction
mode; the default
loregdsupports transactions.
Atomic changes and removable changes are different #
A role can use a transaction to apply related registry entries together and a layer to keep those entries removable later. A transaction does not select the winning layer or create a history of previous states. Layer removal also does not undo a key's descriptor changes.
Review layer recovery separately from whether the original write was atomic.
Avoid overwriting another writer #
Transactions are not value-level conflict detection. If your change depends on a value remaining unchanged since you read it, use the conditional-write mechanism rather than assuming the transaction protects that read.
reg set --expected-seq N checks the destination layer's own entry. A
mismatch returns exit status 6 without the write. reg get -L shows only
the effective winner, so its sequence is suitable only when that winner
is in your intended destination layer. It does not expose a shadowed
layer's sequence.
If a commit times out #
A source failure returned during commit can also follow a successful storage commit. Check current state before resubmitting a batch after such an error.
A late successful response can publish the committed change and notify watchers even after the caller was told it timed out. Closing an uncommitted transaction normally aborts it, as does process exit, but that is not a way to undo an already-dispatched successful commit.
The LCS commit and failure reference describes these outcomes. After a successful batch, use the same consumer verification as for a single write.
Where to go next #
Registry links
Peios / Using Peios / Advanced
A registry link redirects a key path to another key. A normal open follows it, so an inspection may show the target's values rather than the link's own stored target. Check which object you are operating on before changing or deleting a link.
Inspect the link without following it #
For a suspected link at KEY, use:
Replace KEY with the actual path. The first shows the key's metadata,
including its link flag; the second reads its unnamed target value.
Add -L to the value read to see the winning target's layer and sequence.
Omitting --no-follow follows the link by default.
These are inspection commands. Do not assume an option documented for
get and info is available on every mutating subcommand. Check the
reg command reference before changing
a link itself.
What makes a key a link #
A link has both a fixed link flag set at creation and an unnamed default
value of type REG_LINK containing an absolute registry target path.
A REG_LINK value alone does not turn an ordinary key into a link.
This is the exception to the registry's ordinary opaque-data behavior: LCS reads the target during path resolution. The handle returned by a normal open refers to the resolved target.
Creating a link is privileged #
The command form is reg link KEY TARGET. Creation requires
KEY_CREATE_SUB_KEY and KEY_CREATE_LINK on the parent, plus either
SeTcbPrivilege or Administrator membership, in addition to applicable
layer rights.
Use it only when redirecting that namespace is intended.
Targets are followed literally. CurrentUser is not expanded inside a
link target, so do not use it expecting a caller-specific user redirect.
Resolution has a hop limit; cycles fail rather than loop indefinitely.
Layers can redirect a link #
The default target value is layered. A higher-precedence or later same-precedence write can change where the link resolves. Removing that entry lets the next winner determine the target; it need not be the historical target you expected.
A winning default value whose type is not REG_LINK can make resolution
through the still-flagged link fail. Inspect the link with --no-follow
and check the target's winning layer before attempting a repair. Verify
both the stored target and the intended resolved key after recovery.
Where to go next #
- Layer selection
- Deletion, including recursive tools that do not follow links
- LCS symlink implementation and ABI for programmatic link handling
Files and directories
Peios / Using Peios / Peiosutils / Files and directories
This topic covers commands for creating, copying, moving, removing, and resizing files and directories, as well as reporting disk usage. Commands in other topics can also write output files.
This page names every command, and then explains the one theme that runs through the whole topic on Peios: what happens to a file's security descriptor when the file is created or copied.
The commands #
| Command | Purpose |
|---|---|
cp | Copy files and directories. |
mv | Move or rename files and directories. |
rm | Remove files, and with -r, directories. |
rmdir | Remove directories, but only empty ones. |
mkdir | Create directories. |
mkfifo | Create named pipes (FIFOs). |
mknod | Create special files — device nodes and FIFOs. |
touch | Create empty files, or update a file's timestamps. |
ln | Create hard links and symbolic links. |
link and unlink | The single-purpose, low-level versions of "make a hard link" and "remove a file". |
shred | Destroy a file's contents by overwriting them, so they cannot be recovered. |
dd | Copy data block by block, converting it on the way. |
df | Report how much space each file system has, and how much is free. |
du | Report how much space files and directories use. |
truncate | Shrink or extend a file to an exact size. |
mktemp | Create a temporary file or directory with a safely unique name. |
sd | Inspect selected file security components and change file security descriptors. |
mkexec | Mark a regular file as a program, so the kernel is willing to run it. |
Every file has a security descriptor #
On Peios, every file and directory carries a security descriptor — the record of who owns it and who may do what to it. Access to a file is decided from its descriptor; see Security descriptors for the full picture.
That fact shapes two groups of commands in this topic.
Creating a file gives it a descriptor #
When mkdir, mkfifo, mknod, or touch creates a new object, that object needs a security descriptor. By default it gets one inherited from the directory it is created in — the new object picks up the access rules its parent directory hands down.
All four of these commands share one set of options — the creation flags — that let you set the new object's descriptor explicitly instead of taking the inherited default: name an owner, set a different group, lock the object so it does not inherit, give it an integrity label, or supply a complete descriptor. The flags are documented in full on the mkdir page; mkfifo, mknod, and touch each link back to it.
Copying a file makes a fresh descriptor — unless you preserve #
cp creates new files, and a cross-file-system mv does too. A brand-new copy gets a brand-new descriptor inherited from its destination directory — it does not carry the source file's security across by default.
When you do want the copy to keep the source's owner, access rules, timestamps, or other attributes, that is the job of the --preserve family of options. cp and mv share an identical --preserve surface; it is documented on the cp page.
Removing a file checks whether you may write it #
rm and shred both make a decision based on whether you can write a file. rm prompts before removing a file you cannot write to; shred's --force overrides a file's protection so it can be destroyed. Both judge "can you write this?" by a live access check against the file's security descriptor — not by any decorative metadata on the inode. Each command's page explains exactly where that check is made.
Where to start #
For copying — and for the whole --preserve model — read cp.
For creating files and directories, and the shared creation flags, read mkdir.
For removing things safely, read rm.
cp
Peios / Using Peios / Peiosutils / Files and directories
cp copies files and directories.
cp [options] source dest
cp [options] source... directory
cp [options] -t directory source...
In the first form, cp copies one source to one dest. In the other two, it copies any number of sources into a directory, keeping their names.
$ cp report.txt report-backup.txt
$ cp report.txt notes.txt /home/jack/archive/
Copying directories #
cp refuses to copy a directory unless you ask it to descend into one:
| Option | Effect |
|---|---|
-r, -R, --recursive | Copy directories, and everything inside them, recursively. |
-a, --archive | Copy recursively, follow no symbolic links, and request all supported preservation attributes listed below. Equivalent to -d -R --preserve-all. |
-a requests the tree structure and the supported preservation set, including owner, DACL, SACL, timestamps, hard links, executable-ness, and extended attributes. It does not guarantee a complete security-descriptor duplicate; see the component limits below. A plain -r copies the contents but gives newly created files fresh security of their own.
A recursive copy never follows a symbolic link inside the destination. If a directory being copied corresponds to an entry in the destination tree that is a symbolic link to a directory, cp refuses with "cannot overwrite non-directory with directory" rather than writing the source tree through the link. Naming a symbolic link to a directory as the destination operand itself is still allowed, as it is with GNU cp.
What a copy's security descriptor is #
This is the part of cp that is specific to Peios, so it is worth being precise.
Every file carries a security descriptor — the record of who owns it and who may do what to it. When cp creates a new destination file, that new file needs a descriptor. The defaults here describe newly created files, not a promise to replace the descriptor of an existing destination.
By default, a copy gets a fresh descriptor — it does not inherit the source's. Specifically, a plain cp:
- gives the copy a descriptor inherited from the destination directory, exactly as if you had created a new file there;
- makes you the owner of the copy;
- gives the copy fresh timestamps — the copy's modification time is "now".
This is usually what you want. A file copied into your home directory should be governed by your home directory's rules and owned by you, regardless of where it came from. But when you need the copy to keep something from the source, you ask for it explicitly — with the --preserve family.
The --preserve family #
--preserve carries chosen attributes from the source onto the copy. The attributes you can preserve:
| Attribute | What it carries from the source |
|---|---|
owner | The owner SID. |
dacl | The whole DACL — every access rule on the file, inherited rules included. |
sacl | The whole SACL — the audit rules and the integrity label. |
daclni | The DACL with inherited rules stripped out — only the rules set explicitly on the source. |
saclni | The SACL with inherited rules stripped out. |
timestamps | The access and modification times, not creation or change time. |
links | Hard-link relationships among source entries selected in this command. Links to names outside that set are not recreated. |
exec | Executable-ness: if the source has any POSIX execute bit, set all three execute bits on the destination; otherwise clear them. Symbolic links are skipped. This does not preserve each Unix mode bit separately. |
security | Extended attributes whose names begin with security., except security.peios.sd, which is handled through the descriptor components above. |
xattrs | Extended attributes outside the security.* namespace. xattr is an alias. |
You rarely name those individually. These options select sensible sets:
| Option | Preserves |
|---|---|
-p | timestamps and exec, both required. |
--preserve | timestamps only. This differs from -p. |
--preserve=LIST | The comma-separated attributes you name, with no implicit attributes. Other preservation options can add or remove attributes. |
--preserve-all | Every supported attribute in the table above, including exec. The full DACL/SACL selections take precedence over their inherited-rule-stripping variants. |
--sd | owner, dacl, sacl, including inherited ACL entries. The descriptor's group component is not selected. |
--sd-explicit | owner, daclni, saclni — the owner and the source's explicitly set ACL rules. The group component is not selected. |
--no-preserve=LIST | Turns the named attributes back off — useful to subtract from a broader option. |
With no explicit preservation list, a plain copy preserves exec best-effort: failure to carry executable-ness does not fail the copy. An explicit --preserve=LIST starts with no implicit attributes, so include exec if you need it. Bare --preserve uses the list timestamps and likewise does not select exec. In contrast, -p, -a, and an explicit --preserve=exec require it. These distinctions come from the option defaults and cp's bare-option parsing; the exec handler implements the execute-bit conversion.
Preservation options combine in command-line order: requests add attributes, --no-preserve removes them, and -a or --preserve-all selects the full supported set again. For example, -a --no-preserve=exec removes executable-ness from the archive request. See the resolver.
The security selection is broader than security.peios.*: it can include compatibility attributes such as security.capability if present and readable. xattrs does not include that namespace. The attribute filters exclude security.peios.sd from both classes, so copying extended attributes cannot fill in descriptor components omitted by the SD copy. The xattr copy loop sets source attributes but does not remove destination-only attributes. Copying over an existing destination is not an exact attribute synchronization.
--sd versus --sd-explicit #
Both select the source's owner, DACL, and SACL; they differ in how they treat inherited rules.
--sdpasses the owner and the whole source DACL and SACL, including inherited entries, to the destination's descriptor-setting operation.--sd-explicitcarries the owner but strips inherited entries from the source ACLs before applying them, letting the destination directory supply inheritance. Use it when copying a file into a different directory and you want the file's own tailored rules to come along while still picking up the new location's inherited rules.
Neither option selects the descriptor's GROUP component. The copy implementation selects only OWNER, DACL, and SACL; it does not request replacement of the destination's group from the source. There is no group preservation attribute. The same limit applies to -a and --preserve-all. These options are not a promise of a byte-identical descriptor or preservation of every descriptor control bit, and do not establish a complete security backup/restore procedure. When the destination already has a valid descriptor, the kernel merges selected components and their selected control bits; it retains unselected group and resource-manager state. Missing or invalid destination descriptors use a separate privileged replacement path. See the set-security contract.
For what "inherited" versus "explicit" means, see Inheritance.
When a preserve cannot be honoured #
Required preservation failures are errors in the copy operation, including required exec preservation. This differs from the best-effort executable-ness of a plain copy. Descriptor writes must pass the component's access and validation checks; a full SACL write also needs enabled SeRelabelPrivilege if its label exceeds the caller's integrity. SeSecurityPrivilege or SeRestorePrivilege alone does not guarantee that every label can be copied. See integrity-label constraints.
A recursive link-preserving copy can suppress a returned error for a source symbolic link. This includes -a: the recursive caller does not propagate that error when hard-link preservation is selected. An underlying operation may still print a diagnostic, so this is not a claim that every such failure is silent. A successful exit status does not certify that every link and its required metadata were copied.
Failure is not a rollback. Destination files or a partially copied tree may already exist when a preserve fails; the regular-file error path even attempts to truncate the destination. Treat that output as incomplete; do not delete the source or replace a known-good backup on the strength of a partial copy. Check the exit status and verify the content and each attribute you require before relying on the result.
Source-side unsupported xattr enumeration is a separate accepted case. security and xattrs name extended attributes, and some filesystems have no extended attributes at all — FAT, 9p, and a squashfs image built without an attribute table are examples. If source enumeration reports that it is unsupported, the xattr handler returns success for that step without copying attributes. This does not establish that every other copy step succeeded.
That exception applies only to source enumeration. If an attribute is read from the source but cannot be set on the destination, the attribute handler returns an error. The recursive source-symlink exception above still applies. A diagnostic can look like:
# cp -a /home/jack/notes /mnt/usb-fat/
cp: failed to set extended attributes on '/mnt/usb-fat/notes': Operation not supported
The distinction matters for -a, which requests both attribute classes: unsupported source enumeration is accepted, while a destination set failure remains a preservation error.
Overwriting an existing destination #
By default, if the destination already exists, cp overwrites it. These options change that.
| Option | Effect |
|---|---|
-i, --interactive | Ask before overwriting an existing file. |
-n, --no-clobber | Never overwrite an existing file; skip it silently. |
-u, --update[=WHICH] | Overwrite only when the source is newer than the destination. WHICH can be all, none, or older. |
-f, --force | If an existing destination cannot be opened for writing, remove it and try the copy again. |
--remove-destination | Remove the destination before opening it, always — contrast --force, which only removes on failure. |
-b, --backup[=CONTROL] | Before overwriting, make a backup of the destination. |
A backup is refused when making it would destroy the source: if the source is the file the destination's backup would be written over (for example cp --backup=simple a~ a), cp stops with an error instead of overwriting it. The check compares file identity, not spelling.
Symbolic links in the source #
When a source is a symbolic link, cp can copy the link itself or the file it points to.
| Option | Effect |
|---|---|
-L, --dereference | Always follow symbolic links — copy the file they point to. |
-P, --no-dereference | Never follow symbolic links — copy the link itself. |
-H | Follow only the symbolic links named directly on the command line, not links found while recursing. |
-d | Copy symbolic links as links, and preserve hard-link structure. |
Other ways to copy #
cp can also produce something other than a plain duplicate.
| Option | Effect |
|---|---|
-l, --link | Create a hard link to the source instead of copying its data. |
-s, --symbolic-link | Create a symbolic link to the source instead of copying. |
--reflink[=WHEN] | Make a lightweight copy-on-write clone where the file system supports it. WHEN is always, auto, or never. |
--sparse[=WHEN] | Control whether runs of zero bytes are stored as holes rather than written out. WHEN is always, auto, or never. |
--attributes-only | Create the destination and copy its attributes, but not its data. |
Other options #
| Option | Effect |
|---|---|
-t, --target-directory=DIR | Copy every source into DIR. Useful when the directory is not the last argument. |
-T, --no-target-directory | Treat dest as a plain file even if it is a directory. |
--parents | Recreate the source's leading directories under the destination. |
-x, --one-file-system | Do not cross into a different file system while recursing. |
--strip-trailing-slashes | Strip any trailing slashes from each source argument before using it. |
-v, --verbose | Print each file as it is copied. |
--debug | Explain in detail how each file was copied. Implies -v. |
--progress | Show a progress bar. |
Exit status #
| Code | Meaning |
|---|---|
0 | No failure reached the command's exit status. Skipped entries and the recursive source-symlink error case above mean this is not proof that every file or attribute was copied. |
1 | A copy failure was reported through the command's error path, including propagated required-preservation failures. |
Source and version scope #
The preservation corrections on this page are checked against peiosutils 0.8.18, commit 3344d469. The cited implementation establishes the option selections and copy behavior at that revision. It does not establish when each behavior first shipped, prove the version installed on a particular machine, or substitute for runtime validation of a copy or restore.
mv
Peios / Using Peios / Peiosutils / Files and directories
mv moves files and directories from one place to another. Moving a file within the same directory, under a new name, is how you rename it.
mv [options] source dest
mv [options] source... directory
mv [options] -t directory source...
$ mv draft.txt report.txt # rename
$ mv report.txt notes.txt /home/jack/done/ # move into a directory
A move is either a rename or a copy-and-delete #
mv does its job in one of two ways, and which one decides what happens to the file's security.
Within one file system, a move is normally a rename. Nothing is copied: the file stays exactly where it physically is, and only its name and directory entry change. The file keeps its inode, and so it keeps its security descriptor unchanged — its owner, access rules, and audit rules stay the same. A rename does not re-evaluate inheritance; a file does not pick up new rules just because it landed in a new directory.
Across file systems, a move is a copy followed by a delete. When the destination is on a different file system, mv cannot just rename — it copies the data to a new file on the destination, then removes the original. That new file is a genuinely new object, so, exactly as with cp, it gets a fresh security descriptor inherited from the destination directory unless you ask otherwise.
So: rename a file and its security is untouched; move it to another file system and the copy starts fresh.
The same applies to POSIX ownership: the copy is a new file owned by whoever ran mv, and the source's uid and gid are not carried. Because of that, a cross-file-system move also drops the setuid and setgid bits from the copy, as GNU mv does: a set-id file that changed hands would grant the mover's identity, not the original owner's. Data, timestamps and ordinary extended attributes do travel with the file.
When a directory is moved across file systems onto an existing destination, mv removes the destination and recreates it. If something else appears under that name in between, mv refuses with "destination was replaced while moving" rather than copying into it.
Selecting attributes forces a copy-and-delete #
mv accepts the same attribute-selection options as cp: --preserve, --no-preserve, --preserve-all, --sd, and --sd-explicit. Specifying any of these options forces a copy followed by deletion of the source, even within one file system, rather than a rename. The selected options control which attributes are carried onto the new file.
See cp attribute selection and security preservation for the attribute list and the meaning of these options.
Overwriting an existing destination #
By default mv overwrites an existing destination. These options change that. When more than one of -i, -f, -n is given, the last one wins.
| Option | Effect |
|---|---|
-i, --interactive | Ask before overwriting an existing file. |
-f, --force | Do not prompt before overwriting. |
-n, --no-clobber | Never overwrite an existing file. |
-u, --update[=WHICH] | Select the update policy below. -u and bare --update select older. |
-b, --backup[=CONTROL] | Make a backup of the destination before overwriting it. |
The update modes are:
all: no age restriction; this is the default when no update option is given. Other overwrite options still apply.older: move when the destination is missing or the source is newer; otherwise skip successfully.none: skip existing destinations successfully.none-fail: refuse to replace an existing destination and report failure.
A backup is refused when making it would destroy the source: if the source is the file the destination's backup would be written over (for example mv --backup=simple a~ a), mv stops with an error. The check compares file identity, not how the paths are spelled.
Other options #
| Option | Effect |
|---|---|
-t, --target-directory=DIR | Move every source into DIR. |
-T, --no-target-directory | Treat dest as a plain file even if it is a directory. |
--strip-trailing-slashes | Strip any trailing slashes from each source argument. |
-v, --verbose | Print each file as it is moved. |
--debug | Explain in detail how each file was moved. Implies -v. |
--progress | Show a progress bar. |
Exit status #
| Code | Meaning |
|---|---|
0 | No error was reported. This includes successful skips; it does not mean every source was moved. |
1 | A move failed, including refusal to replace an existing destination with --update=none-fail. |
rm
Peios / Using Peios / Peiosutils / Files and directories
rm removes files. With -r, it removes directories and everything inside them.
rm [options] file...
$ rm draft.txt old-notes.txt
$ rm -r build/
Removal is not reversible — once rm removes a file, its directory entry is gone. rm has a few safety behaviours, below, but the basic rule stands: think before you confirm.
Removing directories #
rm will not remove a directory unless you say so:
| Option | Effect |
|---|---|
-r, -R, --recursive | Remove directories and all of their contents, recursively. |
-d, --dir | Remove a directory, but only if it is already empty. |
The write-protected prompt #
By default — when you have not passed -f and rm is running interactively — rm prompts before removing a file you cannot write to:
$ rm policy.db
rm: remove write-protected regular file 'policy.db'?
The point of that prompt is to catch mistakes: a file you cannot write is one you most likely did not mean to delete.
"Write-protected" here means a real access check. rm decides it by asking the kernel whether your token may write the file — a live check against the file's security descriptor. It is not read off any decorative metadata on the inode; it is the same authority question that any write to the file would face. So the prompt is honest: if rm says a file is write-protected, it is a file the access model would genuinely stop you writing.
(The check is about write access to the file's contents. Whether you may actually remove the file is a separate question, decided by its own access right — so you can be prompted about a write-protected file and still be allowed to delete it. The prompt is a courtesy check, not the authorisation.)
Controlling the prompts #
| Option | Effect |
|---|---|
-f, --force | Never prompt. Also ignore files that do not exist instead of reporting them. |
-i | Prompt before every removal. |
-I | Prompt just once before removing more than three files or before a recursive removal. Less intrusive than -i, but still a check against a slip. |
--interactive[=WHEN] | Set the prompting explicitly: never, once, or always. |
The root failsafe #
rm refuses to recursively remove / — the root of the file system — because doing so by accident would be catastrophic.
| Option | Effect |
|---|---|
--preserve-root | Refuse to recurse on /. This is the default; you do not need to ask for it. |
--no-preserve-root | Disable the failsafe. Required, in full and unabbreviated, if you genuinely intend a recursive operation on /. |
The failsafe recognises the root by identity, not by spelling: a symbolic link to /, a path such as /.. that resolves to it, and a bind mount of the root file system are all refused.
A recursive removal also refuses to be redirected while it runs. rm descends through directory handles rather than paths, never follows a symbolic link it meets on the way down, and if the operand it started from is replaced by something else mid-way it stops with "File changed while removing".
Other options #
| Option | Effect |
|---|---|
-v, --verbose | Print each file as it is removed. |
--progress | Show a progress bar. |
rm and shred #
rm unlinks a file — it removes the name, and the space becomes free. The file's data may still be physically present on the device until something else overwrites it. When you need the contents to be genuinely unrecoverable, use shred, which overwrites the data before removing the file.
Exit status #
| Code | Meaning |
|---|---|
0 | Every requested removal succeeded (or, with -f, the file did not exist). |
1 | A file could not be removed. |
rmdir
Peios / Using Peios / Peiosutils / Files and directories
rmdir removes directories — but only empty ones. If a directory still has anything in it, rmdir leaves it alone and reports the failure.
rmdir [options] directory...
$ rmdir old-cache/
That refusal to touch a non-empty directory is the whole point of rmdir: it is the safe way to remove a directory you believe is empty. If it is not empty, you find out instead of losing the contents. To remove a directory together with everything inside it, use rm -r.
Options #
| Option | Effect |
|---|---|
-p, --parents | Remove the directory and then its ancestors, as long as each becomes empty in turn. rmdir -p a/b/c removes a/b/c, then a/b, then a. |
--ignore-fail-on-non-empty | Do not treat "directory not empty" as a failure. Other failures are still reported. Useful when clearing out whatever directories happen to be empty and leaving the rest. |
-v, --verbose | Print a line for each directory as it is removed. |
Exit status #
| Code | Meaning |
|---|---|
0 | Every directory was removed. |
1 | A directory could not be removed — it was not empty, did not exist, or removal was refused. |
mkdir
Peios / Using Peios / Peiosutils / Files and directories
mkdir creates directories.
mkdir [options] directory...
$ mkdir reports
$ mkdir -p projects/2026/q2
Creating parent directories #
By default mkdir creates exactly the directories you name, and fails if a parent is missing. -p removes that restriction:
| Option | Effect |
|---|---|
-p, --parents | Create any missing parent directories along the way. Also makes mkdir succeed quietly if the directory already exists. |
-v, --verbose | Print a line for each directory as it is created. |
The creation flags #
A new directory needs a security descriptor — the record of who owns it and who may do what to it. By default, a new directory inherits its descriptor from the directory it is created in: it picks up the access rules its parent hands down. For most directories that is exactly right, and you pass no extra options at all.
When you need the new directory to have a different descriptor, the creation flags set it explicitly. These five flags are shared, unchanged, by mkdir, mkfifo, mknod, and touch — whenever one of those commands creates a new object, it accepts this same set. This page is the full reference for them.
| Flag | Argument | Effect |
|---|---|---|
--owner | SID | Make this principal the owner, instead of you. |
--group | SID | Set the object's group. |
--label | level | Give the object a mandatory integrity label. |
--no-inherit | — | Do not inherit rules from the parent directory; lock the object to its owner. |
--sddl | SDDL string | Supply the entire security descriptor directly. |
A SID argument is either a literal S-1-… value or a well-known alias — BA for the built-in administrators, for example.
--label takes one of these levels, lowest to highest: untrusted, low, medium, medium-plus, high, system, protected. The label applies the standard rule for files and directories — a principal below the object's level cannot write to it.
Inherited, or explicit #
The default and --no-inherit are the two ends of a choice.
- Inherited (the default) — the new directory tracks its parent. Rules the parent hands down apply to it, and as the parent's rules change the directory follows along. This keeps a tree of directories consistent without per-directory effort.
--no-inherit— the new directory takes nothing from its parent. Its access is locked to its owner, and it does not follow the parent's rules. Use it to carve out a directory whose security stands apart from the tree around it.
See Inheritance for the full model.
--sddl for a complete descriptor #
The four flags above are shortcuts for common adjustments. When you need to specify the whole descriptor — a particular set of access rules, audit rules, owner, and group all at once — --sddl takes it as a single string in the security-descriptor definition language:
$ mkdir --sddl 'O:BAG:BAD:(A;OICI;FA;;;BA)' secure-area
--sddl is mutually exclusive with --owner, --group, --label, and --no-inherit — it already says everything those flags would say, so combining them is rejected.
How the descriptor is applied #
mkdir creates the directory first and then applies the descriptor to it. The two steps are not separately visible — mkdir either finishes with the directory created and secured as asked, or fails as a whole. If applying the descriptor fails, the half-created directory is not left behind.
Exit status #
| Code | Meaning |
|---|---|
0 | Every directory was created. |
1 | A directory could not be created, or a creation flag could not be applied. |
mkfifo
Peios / Using Peios / Peiosutils / Files and directories
mkfifo creates named pipes, also called FIFOs.
mkfifo [options] name...
$ mkfifo events
A named pipe is a file that two programs use to pass a stream of data: one writes into it, the other reads from it, and the data flows through in order — first in, first out, which is what "FIFO" stands for. Unlike an ordinary pipe between two commands, a named pipe has a name on the file system, so the two programs do not have to be started together or be related to each other.
Setting the new pipe's security #
A FIFO is a file, and a new file needs a security descriptor. By default a new FIFO inherits its descriptor from the directory it is created in.
mkfifo accepts the shared creation flags — --owner, --group, --label, --no-inherit, and --sddl — to set that descriptor explicitly instead. They behave exactly as they do for mkdir; the full reference is on the mkdir page.
Exit status #
| Code | Meaning |
|---|---|
0 | Every FIFO was created. |
1 | A FIFO could not be created — for example, the name already exists. |
mknod
Peios / Using Peios / Peiosutils / Files and directories
mknod creates special files — files that are not data but an interface to something else: a device, or a named pipe.
mknod [options] name type [major minor]
$ mknod /dev/mydisk b 8 0
$ mknod backlog p
The type argument #
The type says what kind of special file to create:
| Type | Creates |
|---|---|
b | A block device — a device addressed in fixed-size blocks, such as a disk. |
c or u | A character device — a device addressed as a stream of bytes, such as a terminal. |
p | A named pipe (FIFO). |
For a block or character device (b, c, u) you must also give a major and a minor number. The major number selects which driver handles the device; the minor number tells that driver which specific device is meant. For a pipe (p), the major and minor numbers are omitted — a pipe has no driver behind it.
A major or minor number is read as hexadecimal if it begins with 0x, as octal if it begins with 0, and as decimal otherwise.
mknod NAME p and mkfifo NAME do the same thing; mkfifo exists as the clearer way to ask for just a pipe.
Setting the new file's security #
A special file needs a security descriptor like any other file, and by default a new one inherits its descriptor from the directory it is created in.
mknod accepts the shared creation flags — --owner, --group, --label, --no-inherit, and --sddl — to set that descriptor explicitly. They behave exactly as they do for mkdir; the full reference is on the mkdir page.
Exit status #
| Code | Meaning |
|---|---|
0 | The special file was created. |
1 | It could not be created — a missing or invalid type, missing device numbers, or a name that already exists. |
touch
Peios / Using Peios / Peiosutils / Files and directories
touch does one of two things, depending on whether the file already exists:
- if the file does not exist,
touchcreates it, empty; - if the file does exist,
touchupdates its timestamps to the current time.
touch [options] [file...]
$ touch notes.txt # create notes.txt if absent, else bump its times
The "create an empty file" behaviour is the everyday use. The "update timestamps" behaviour matters to tools that decide what work to do by comparing file times — touch is how you tell such a tool that a file should be considered freshly changed.
Which timestamps, and to what #
By default touch sets both the access time and the modification time to the current moment. These options narrow or redirect that.
| Option | Effect |
|---|---|
-a | Change only the access time. |
-m | Change only the modification time. |
--time=WORD | Choose which time to change by name: access, atime, or use for the access time; modify or mtime for the modification time. |
-d, --date=STRING | Use the time described by STRING instead of the current time. |
-t STAMP | Use the time given as [[CC]YY]MMDDhhmm[.ss] instead of the current time. |
-r, --reference=FILE | Use the timestamps of FILE instead of the current time. |
-h, --no-dereference | If the named file is a symbolic link, change the link's own timestamps rather than those of the file it points to. |
Not creating a file #
To use touch purely to update timestamps, and never to create anything:
| Option | Effect |
|---|---|
-c, --no-create | Do not create a file that does not exist. A named file that is absent is silently skipped. |
Setting a created file's security #
When touch creates a file, that new file needs a security descriptor, and by default it inherits one from the directory it is created in.
touch accepts the shared creation flags — --owner, --group, --label, --no-inherit, and --sddl — to set that descriptor explicitly. They behave exactly as they do for mkdir; the full reference is on the mkdir page.
These flags apply only on the create path. If touch is updating the timestamps of a file that already exists, there is no new descriptor to set, and the creation flags have nothing to do — they do not alter an existing file's security. Pair them with -c and they never take effect at all, since -c means nothing is ever created.
Exit status #
| Code | Meaning |
|---|---|
0 | Every file was created or updated as requested. |
1 | A file could not be created or updated. |
ln
Peios / Using Peios / Peiosutils / Files and directories
ln creates links — extra ways to reach a file.
ln [options] target link_name
ln [options] target
ln [options] target... directory
ln [options] -t directory target...
$ ln -s /opt/app/bin/app /lcl/bin/app # a symbolic link
$ ln data.csv data-archive.csv # a hard link
Hard links and symbolic links #
There are two kinds of link, and they are genuinely different things.
A hard link is another name for the same file. The file's data exists once; a hard link is an additional directory entry pointing at it. Every hard link to a file is equal — none is "the original" — and the file's data stays as long as at least one hard link remains. Hard links cannot span file systems, and cannot be made to directories.
A symbolic link is a small file that simply contains a path. When something opens a symbolic link, it is redirected to whatever that path names. The symbolic link and its target are separate files: if the target is moved or removed, the link still exists but now points at nothing — it "dangles". Symbolic links can point anywhere, across file systems and at directories.
ln creates hard links by default, and symbolic links with -s. When in doubt, -s — a symbolic link's behaviour is the easier one to reason about.
The two also differ in how they are secured. A hard link is not a new file, so creating one involves no new security descriptor — every name for the file shares the file's existing one. A symbolic link is a new file, and like any new file it inherits its own descriptor from the directory it is created in.
The forms #
ln target link_name— create one link namedlink_name.ln target— create a link totargetin the current directory, with the same final name.ln target... directory— create a link to eachtargetinsidedirectory.ln -t directory target...— the same, with the directory named first.
Options #
| Option | Effect |
|---|---|
-s, --symbolic | Make symbolic links instead of hard links. |
-f, --force | Remove an existing destination file so the link can be created. |
-i, --interactive | Ask before removing an existing destination. |
-r, --relative | For a symbolic link, write the target as a path relative to the link's own location, rather than an absolute path. |
-n, --no-dereference | If link_name is itself a symbolic link to a directory, treat it as an ordinary file — replace the link rather than create something inside the directory it points to. |
-L, --logical | When the target is a symbolic link, link to what it points to. |
-P, --physical | Make a hard link to a symbolic link itself, rather than to its target. |
-t, --target-directory=DIR | Create all links inside DIR. |
-T, --no-target-directory | Treat link_name as a plain file, never as a directory to create links inside. |
-b, --backup[=CONTROL] | Back up an existing destination before replacing it. |
-S, --suffix=SUFFIX | Use SUFFIX for backup file names. |
-v, --verbose | Print the name of each link as it is created. |
Inspecting links afterwards #
readlink prints where a symbolic link points. The low-level link command makes a single hard link with no options, for scripts that want exactly that and nothing else.
Exit status #
| Code | Meaning |
|---|---|
0 | Every link was created. |
1 | A link could not be created. |
link and unlink
Peios / Using Peios / Peiosutils / Files and directories
link and unlink are the bare, single-purpose versions of two operations that ln and rm also perform. Each does exactly one thing, takes exactly its operands, and has no options. They exist for scripts that want one precise operation with no surrounding behaviour — no prompting, no link kinds to choose, no recursion.
link #
link file1 file2
link creates one hard link: a new name, file2, for the existing file file1. file1 must exist; file2 must not.
$ link data.csv data-archive.csv
That is the whole command. It performs the link operation directly and reports whether it succeeded. For anything more — symbolic links, replacing an existing destination, linking into a directory — use ln.
unlink #
unlink file
unlink removes one file: it deletes the directory entry file. It takes a single name and removes it directly.
$ unlink stale.lock
unlink does not prompt, does not recurse, and does not remove directories. For removing several files, removing directories, or any of the safety prompts, use rm.
Why they exist #
ln and rm are the commands to use day to day — they are flexible and have the safety behaviours. link and unlink are deliberately rigid: a script that calls unlink will only ever remove a single file, and a reviewer can see that at a glance. The narrowness is the feature.
Exit status #
Both commands:
| Code | Meaning |
|---|---|
0 | The operation succeeded. |
1 | The operation failed — a missing or already-existing operand, or a refused request. |
shred
Peios / Using Peios / Peiosutils / Files and directories
shred destroys a file's contents. It does not just unlink the file — it overwrites the data, repeatedly, so that what was there cannot be read back even with effort and specialised equipment.
shred [options] file...
$ shred -u -v secret-keys.txt
rm removes a file's name; the data may sit on the device, recoverable, until something else happens to overwrite it. shred overwrites the data on purpose. Use it when a file held something that must not be recoverable.
By default shred overwrites a file three times and then leaves the file in place — empty of its old contents but still present. That default exists because shred is often pointed at device files, which usually should not be removed. To remove the file after shredding it, pass -u.
When shredding actually works #
shred relies on one assumption: that writing to the file overwrites the same physical storage the old data occupied. On a traditional file system that holds. Several common file system designs break it — anything that writes new data to a fresh location instead of in place, keeps snapshots, journals file data, caches copies elsewhere, or compresses. On those, shred cannot guarantee the old data is gone.
Backups and remote mirrors are a separate matter entirely: shred cannot reach a copy of the file that lives somewhere else.
Treat shred as effective on a plain local file system and uncertain otherwise.
Options #
| Option | Effect |
|---|---|
-n, --iterations=N | Overwrite N times instead of the default of 3. |
-u, --remove[=HOW] | Remove the file after overwriting it. HOW controls how thoroughly the name itself is obscured before the file is unlinked. |
-z, --zero | Add a final pass that writes zeros, so the file does not visibly look shredded afterwards. |
-s, --size=N | Shred N bytes, rather than the whole file. Accepts size suffixes such as K, M, G. |
-x, --exact | Do not round the shredded size up to the next full block. Without it, shred rounds up so the block's slack space is overwritten too. |
--random-source=FILE | Take the random bytes for the overwrite passes from FILE. |
-v, --verbose | Show progress as each pass runs. |
-f, --force | Override a file's protection: when a live access check shows you cannot write the file, rewrite its security descriptor so the overwrite can proceed (see below). |
--force and a file's protection #
To overwrite a file, shred has to be able to write it. If the file's security descriptor does not grant you write access, an ordinary shred cannot proceed.
-f (--force) handles that case. When --force is given and a live access check shows you cannot currently write the file, shred rewrites the file's security descriptor to one that grants full access, and then overwrites the file. The reasoning is deliberate: the file is about to be destroyed anyway, and --force is you saying "override this file's protection."
--force can do this only when you are allowed to change the file's security in the first place — that is, when you own the file or hold the right to rewrite its access rules. If you cannot change the descriptor, --force cannot grant itself write access, and the shred still fails. --force overrides a file's protection; it does not manufacture authority you do not have.
Exit status #
| Code | Meaning |
|---|---|
0 | Every file was shredded successfully. |
1 | A file could not be shredded. |
dd
Peios / Using Peios / Peiosutils / Files and directories
dd copies data from one place to another, a block at a time, and can transform the data as it goes. It is the tool for low-level copying — writing a disk image to a device, extracting an exact byte range out of a file, copying from or to a raw device.
dd [operand]...
$ dd if=disk.img of=/dev/mydisk bs=4M status=progress
How dd is told what to do #
dd does not take options in the usual -x form. It takes operands, each written name=value, in any order. With no if= operand it reads from standard input; with no of= operand it writes to standard output.
The operands #
| Operand | Meaning |
|---|---|
if=FILE | Read input from FILE. |
of=FILE | Write output to FILE. |
bs=BYTES | Read and write BYTES at a time. Sets both the input and output block size at once. |
ibs=BYTES | Input block size (default 512). |
obs=BYTES | Output block size (default 512). |
cbs=BYTES | Conversion block size — used by the block and unblock conversions. |
count=N | Stop after N input blocks, rather than reading to the end. |
skip=N | Skip N input blocks before copying. Also spelled iseek=N. |
seek=N | Skip N output blocks before writing. Also spelled oseek=N. |
conv=LIST | Apply the comma-separated conversions below. |
iflag=LIST | Comma-separated input flags — how the input is opened and read. |
oflag=LIST | Comma-separated output flags — how the output is opened and written. |
status=LEVEL | How much to report: progress (periodic stats while copying), noxfer (final counts only), none (silent). |
A size value accepts a unit suffix (K, M, G, …). So bs=4M is four mebibytes per block.
Conversions (conv=) #
| Value | Effect |
|---|---|
ucase / lcase | Convert the data to upper-case / lower-case. |
swab | Swap every adjacent pair of bytes. |
sync | Pad each input block out to its full size — with zeros, or with spaces alongside block/unblock. |
block / unblock | Convert between newline-terminated lines and fixed-size cbs records. |
sparse | Where an output block is all zeros, seek past it instead of writing it. |
excl | Fail if the output file already exists. |
nocreat | Fail if the output file does not already exist. |
notrunc | Do not truncate the output file when opening it. |
noerror | Continue past read errors instead of stopping. |
fdatasync / fsync | Flush the data — or the data and metadata — to storage before finishing. |
When dd creates the output file — an of= file that does not already exist — the new file needs a security descriptor, and it inherits one from the directory it is created in.
Input and output flags (iflag=, oflag=) #
| Flag | Applies to | Effect |
|---|---|---|
count_bytes | input | Interpret count=N as a number of bytes, not blocks. |
skip_bytes | input | Interpret skip=N as a number of bytes. |
fullblock | input | Wait for a full ibs of data on each read. |
seek_bytes | output | Interpret seek=N as a number of bytes. |
append | output | Open the output in append mode. |
direct | both | Use direct I/O, bypassing the cache. |
dsync / sync | both | Use synchronised I/O — for data, or for data and metadata. |
nonblock | both | Use non-blocking I/O. |
noatime | both | Do not update the file's access time. |
nocache | both | Ask the system to drop the file's cached pages. |
directory | both | Fail unless the file is a directory. |
What dd prints #
Unless status=none is set, dd prints a summary when it finishes:
16+0 records in
16+0 records out
67108864 bytes (67 MB, 64 MiB) copied, 1.234 s, 54.4 MB/s
The records in / records out counts are written complete+partial — full-sized blocks plus any short final block. status=progress prints the last line periodically during a long copy.
Exit status #
| Code | Meaning |
|---|---|
0 | The copy completed. |
1 | The copy failed — a bad operand, an I/O error, a file that could not be opened, or a regular output file that could not be truncated to its final length. |
df
Peios / Using Peios / Peiosutils / Files and directories
df — "disk free" — reports, for each file system, how much space it has and how much is still available.
df [options] [file...]
With no arguments, df reports on every mounted file system. Given a file, it reports on the one file system that file lives on.
$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/sda2 456G 198G 235G 46% /
/dev/sda1 511M 12M 499M 3% /boot
The default columns #
Each line is one file system:
- Filesystem — the device or source backing it.
- Size — its total capacity.
- Used — space in use.
- Avail — space still available.
- Use% — used space as a percentage.
- Mounted on — where it is attached in the directory tree.
Choosing the units #
By default sizes are shown in blocks. These options change that.
| Option | Effect |
|---|---|
-h, --human-readable | Show sizes with unit suffixes — 456G, 12M — using powers of 1024. |
-H, --si | Likewise, but using powers of 1000. |
-B, --block-size=SIZE | Show sizes scaled to SIZE — -BM reports in megabytes. |
-k | Use 1024-byte blocks. |
Choosing what to report #
| Option | Effect |
|---|---|
-a, --all | Include dummy and otherwise-hidden file systems that df would normally skip. |
-l, --local | Report only local file systems, skipping network-mounted ones. |
-t, --type=TYPE | Report only file systems of type TYPE. |
-x, --exclude-type=TYPE | Report everything except file systems of type TYPE. |
--total | Add a final line giving the grand total across everything reported. |
Changing the output #
| Option | Effect |
|---|---|
-i, --inodes | Report inode counts — total, used, and free — instead of block space. A file system can run out of inodes while it still has space. |
-T, --print-type | Add a column showing each file system's type. |
--output[=LIST] | Replace the default columns with exactly the fields named in LIST. With no list, print every available field. |
-P, --portability | Use a fixed, simple column layout that does not wrap a long device name onto its own line. |
Sync behaviour #
| Option | Effect |
|---|---|
--sync | Flush pending writes before reading the usage figures, for the most up-to-date numbers. |
--no-sync | Do not flush first. This is the default and is faster. |
Exit status #
| Code | Meaning |
|---|---|
0 | The report was produced. |
1 | A failure — for example, a named file does not exist, or the table of file systems could not be read. |
du
Peios / Using Peios / Peiosutils / Files and directories
du — "disk usage" — reports how much space files and directories occupy. Where df tells you about a whole file system, du tells you where the space has actually gone.
du [options] [file...]
$ du -sh /home/jack/projects
4.2G /home/jack/projects
With no arguments, du works on the current directory. By default it walks the directory tree, prints the cumulative size of each directory (a directory's size includes everything under it), and ends with the total for the argument itself. Individual files are not listed unless you ask for them.
What to count and how deep #
| Option | Effect |
|---|---|
-a, --all | List every file, not only directories. |
-s, --summarize | Print only a single total for each argument — no per-subdirectory breakdown. |
-d, --max-depth=N | Show totals only down to N levels below each argument. -d 0 is the same as -s. |
-S, --separate-dirs | Give each directory's own size, excluding its subdirectories. |
-c, --total | Add a final grand-total line. |
--inodes | Count inodes used rather than space used. |
What "size" means #
| Option | Effect |
|---|---|
-h, --human-readable | Sizes with unit suffixes — 4.2G — using powers of 1024. |
--si | Likewise, using powers of 1000. |
-B, --block-size=SIZE | Scale sizes to SIZE. |
-k / -m | Use 1024-byte / 1024×1024-byte units. |
--apparent-size | Report the apparent size of files — how much data they contain — rather than the disk space they occupy. The two differ for sparse files and because of block rounding. |
-b, --bytes | Report plain byte counts. Equivalent to --apparent-size --block-size=1. |
Following symbolic links #
By default du does not follow symbolic links.
| Option | Effect |
|---|---|
-L, --dereference | Follow all symbolic links and count what they point to. |
-D, -H, --dereference-args | Follow only the symbolic links named directly on the command line. |
-P, --no-dereference | Follow no symbolic links. This is the default. |
-x, --one-file-system | Do not cross into a different file system while walking. |
-l, --count-links | Count a hard-linked file every time it is encountered, instead of once. |
Limiting the output #
| Option | Effect |
|---|---|
--exclude=PATTERN | Skip files and directories whose name matches PATTERN. |
--exclude-from=FILE | Skip everything matching any pattern listed in FILE. |
-t, --threshold=SIZE | Show only entries at least SIZE (or, with a negative SIZE, at most that big). |
--files0-from=F | Take the list of files to measure from F, NUL-separated. - means standard input. |
--time[=WORD] | Also show a timestamp — by default the latest modification time anywhere in the directory. |
-0, --null | End each output line with a NUL character instead of a newline. |
-v, --verbose | Report extra detail about what is being processed. |
Exit status #
| Code | Meaning |
|---|---|
0 | The report was produced. |
1 | A file or directory could not be accessed. |
truncate
Peios / Using Peios / Peiosutils / Files and directories
truncate sets a file's size to an exact figure — shrinking it, or extending it.
truncate [options] file...
$ truncate -s 0 logfile # empty the file
$ truncate -s 1G disk.img # make a 1 GiB file
Shrinking a file discards everything past the new size. Extending it adds space that reads back as zero bytes; that added space is a hole — it costs no actual storage until something writes real data into it, which is how truncate -s 1G can create a "1 GiB file" instantly.
By default, truncate creates a file that does not yet exist (as an empty file, then sized as asked). A file created this way needs a security descriptor, and it inherits one from the directory it is created in.
Specifying the size #
-s (--size) takes the size. A plain number sets the size outright. A unit suffix scales it: K, M, G, T, … where the bare letter is a power of 1024 (K = 1024) and the letter with B is a power of 1000 (KB = 1000).
A size may also begin with a prefix, which makes it relative to the file's current size:
| Prefix | Meaning |
|---|---|
+ | Extend by this much. |
- | Reduce by this much. |
< | Shrink to this size only if the file is currently larger ("at most"). |
> | Extend to this size only if the file is currently smaller ("at least"). |
/ | Round the size down to a multiple of this number. |
% | Round the size up to a multiple of this number. |
$ truncate -s +4K notes.txt # 4 KiB larger than it is now
$ truncate -s '<1M' notes.txt # cap it at 1 MiB, leave smaller files alone
Options #
| Option | Effect |
|---|---|
-s, --size=SIZE | Set or adjust the size to SIZE, as described above. |
-r, --reference=RFILE | Base the size on the current size of RFILE instead of giving a number. Combine with a relative -s to mean "the size of RFILE, adjusted". |
-c, --no-create | Do not create a file that does not exist; skip it instead. |
Exit status #
| Code | Meaning |
|---|---|
0 | Every file was resized. |
1 | A file could not be resized — a bad size, or a file that could not be opened. |
mktemp
Peios / Using Peios / Peiosutils / Files and directories
mktemp creates a temporary file — or, with -d, a temporary directory — gives it a name that is not already taken, and prints that name.
mktemp [options] [template]
$ mktemp
/tmp/tmp.QV8x3kZ1aR
$ mktemp -d
/tmp/tmp.7bNc0wPe2L
The reason to use mktemp rather than inventing a name yourself is safety. A script that picks a fixed name like /tmp/work has two problems: two copies of the script collide, and a name an attacker can predict is a name an attacker can interfere with before the script gets to it. mktemp chooses an unpredictable name and creates the file in the same step, so nothing can slip in between. It prints the name it used, for the script to capture:
work=$(mktemp)
# ...use "$work"...
rm "$work"
The temporary file or directory mktemp creates is a new object, and a new object needs a security descriptor; it inherits one from the directory it is created in.
Templates #
The name comes from a template — a name with a run of trailing X characters, each of which mktemp replaces with a random character. More Xs means a wider range of possible names.
$ mktemp report-XXXXXX.txt
report-a8Kp2Q.txt
With no template, mktemp uses a built-in default that places the file in the temporary directory. A template needs at least three Xs.
Options #
| Option | Effect |
|---|---|
-d, --directory | Create a directory instead of a file. |
-p, --tmpdir[=DIR] | Create the temporary object inside DIR. With no DIR, use the directory named by the TMPDIR environment variable, or the system temporary directory. The template is then just the final name component. |
--suffix=SUFFIX | Append SUFFIX after the random part of the name — for giving the file an extension. |
-u, --dry-run | Do not create anything; just print a name that would be free. This reintroduces the race the command exists to avoid — use it only when you genuinely cannot create the object yet. |
-q, --quiet | Print no error message if creation fails; rely on the exit status. |
Exit status #
| Code | Meaning |
|---|---|
0 | The temporary file or directory was created; its name was printed. |
1 | Creation failed — a bad template, or a directory that does not exist. |
sd
Peios / Using Peios / Peiosutils / Files and directories
sd is the command-line tool for working with security descriptors on files. Its subcommands edit owners, DACLs, SACLs, integrity labels and inheritance. The show subcommand queries only owner, group, DACL and the integrity-label view; it does not read the full SACL.
sd subcommand path [arguments]
$ sd show ./report.txt
$ sd allow ./report.txt alice:read
$ sd owner ./report.txt BA
Where ls -l shows a file's owner and a summary, and cp --sd carries selected descriptor components (owner, DACL, and SACL), sd is the tool that edits the descriptor directly.
The subcommands #
| Group | Subcommand | Does |
|---|---|---|
| Inspect | show | Show owner, group, DACL and the integrity-label view on a path. |
check | Simulate an access check against the path. | |
| DACL | allow | Add an allow rule for one or more principals. |
deny | Add a deny rule for one or more principals. | |
remove | Drop every DACL rule for the named principals. | |
| Auditing | audit | Add an audit rule to the SACL. |
unaudit | Drop every SACL rule for the named principals. | |
| Ownership | owner | Set the descriptor's owner. |
group | Set the descriptor's group. | |
| Integrity | integrity | Set the mandatory integrity label. |
| Inheritance | inherit | Turn inheritance protection on or off. |
reset | Drop the file's own rules and re-inherit from the parent. | |
propagate | Push inheritance down to descendants. | |
| Wholesale | set | Write the supplied descriptor components; kernel merge and validation rules apply. |
Naming a principal #
Wherever a subcommand takes a PRINCIPAL, it accepts any of:
| Form | Example | Meaning |
|---|---|---|
@self | @self | The user SID of the token running sd. |
@owner | @owner | A placeholder that the access check substitutes with the file's own owner. |
| A well-known label | Everyone, Administrators, LocalSystem | A named built-in principal. |
| A two-letter alias | WD, BA, SY | The short alias for a well-known principal. |
| A raw SID | S-1-5-32-544 | Any SID, written out in full. |
See SIDs for what these are.
Naming permissions #
Wherever a subcommand takes PERMS, several notations are accepted, and may be mixed:
| Form | Example | Meaning |
|---|---|---|
| Single letters | rwx, r, m | r read, w write, x execute, d delete, m modify, f full, c change-permissions, o take-ownership. |
| Words | read,write, modify | The same set, spelled out. |
| Fine-grained names | read-data,append,traverse | Individual low-level rights, for precise rules. |
| Raw hex | 0x1F01FF | An access mask written directly. |
Run letters together (rwx) or separate names with commas (read,write,execute).
Inspecting #
sd show #
Queries the owner, group, DACL and mandatory integrity-label subset on a path. It selects OWNER | GROUP | DACL | LABEL, not the full SACL. Audit ACEs and other non-label SACL entries are outside this view, even when the caller has authority to read them.
$ sd show ./report.txt
| Flag | Effect |
|---|---|
--sddl | Render the queried components as SDDL. |
--raw | Render SIDs in raw S-1-… form only; this is not raw descriptor export. |
--label | Render SIDs as their labels where known. |
--all | Add the queried byte length and, in human output, the raw control value. |
--json | Emit JSON for the selected view. |
These flags change formatting, not the component selection. A SACL heading or JSON key contains the selected label view, not the audit SACL. --all does not request additional components, and there is no full-SACL/export selector for show.
Human and ordinary JSON output are summaries: callback condition payloads are not serialized, and other ACE bodies are represented by their length. Matching output does not prove complete descriptor equality. --sddl still formats only the queried subset; do not use it as a full backup to replay with sd set. If audit entries or complete metadata verification are required, this command does not provide that evidence.
sd check #
Simulates an access decision: "would this access be allowed?" — without performing it.
$ sd check ./report.txt write
$ sd check ./report.txt read --pid 4821 --explain
| Argument / flag | Effect |
|---|---|
PERMS | The access to test for. |
--pid PID | Check against process PID's token instead of your own. |
--explain | Show why the decision came out as it did — the rule-by-rule walk. |
sd check is the first thing to reach for when an access is denied and you do not know why. --explain walks the descriptor the same way the kernel does.
Changing the DACL #
The DACL is the list of allow and deny rules. These three subcommands edit it.
sd allow and sd deny #
Add an allow (or deny) rule for one or more PRINCIPAL:PERMS pairs.
$ sd allow ./report.txt alice:read bob:rw
$ sd deny ./report.txt Everyone:write
| Flag | Effect |
|---|---|
--flags LIST | ACE inheritance flags — CI container-inherit, OI object-inherit, NP no-propagate, IO inherit-only; none clears them. |
--if EXPR | Make it a conditional rule, applied only when EXPR is true. |
--replace | Drop any existing rules for this principal and kind first, instead of appending. |
--recursive, -r | Apply to every descendant of the path. |
The DACL is evaluated in order: the first matching ACE that decides a right wins for that right. A later deny does not undo an earlier allow, and a later allow does not undo an earlier deny. See DACL evaluation.
sd remove #
Drops every DACL rule — allow and deny — for the named principals.
$ sd remove ./report.txt bob carol
| Flag | Effect |
|---|---|
--allow-empty | Permit the result to be a present-but-empty DACL, which denies everyone. Without this, sd refuses to produce one. |
--recursive, -r | Apply to every descendant. |
Auditing #
sd audit and sd unaudit #
The SACL holds audit rules — see The SACL. sd audit adds one; sd unaudit drops every SACL rule for the named principals.
$ sd audit ./secrets.db Everyone:write:failure
An audit spec is PRINCIPAL:PERMS:WHEN, where WHEN is success, failure, or both — which outcomes to log. sd audit takes the same --flags, --if, --replace, and -r options as sd allow.
Ownership #
sd owner and sd group #
Set the descriptor's owner or group SID.
$ sd owner ./report.txt alice
$ sd group ./report.txt Administrators
Both take a single PRINCIPAL and accept -r. Changing an owner is itself an access-controlled act — see Ownership.
Integrity #
sd integrity #
Sets the file's mandatory integrity label.
$ sd integrity ./report.txt high
The level is one of untrusted, low, medium, medium-plus, high, system, protected. The standard catalog is five levels (untrusted, low, medium, high, system); medium-plus (RID 8448) and protected (RID 20480) are non-standard Windows-compatibility levels — the kernel compares any S-1-16-<rid> numerically.
| Flag | Effect |
|---|---|
--policy BITS | The label's policy bits, comma-separated: NW no write-up, NR no read-up, NX no execute-up. |
--recursive, -r | Apply to every descendant. |
For what the label does, see Mandatory integrity control.
Inheritance #
sd inherit #
Turns inheritance protection on or off — the + mark ls -l shows.
$ sd inherit off ./report.txt # lock the file; stop inheriting
$ sd inherit on ./report.txt # let it inherit from its parent again
inherit on permits inheritance from the parent directory; inherit off protects the file while keeping its current rules. Neither creates a live link to the parent: the child stores its own descriptor, and later parent changes require explicit propagation to existing children. See Inheritance.
| Flag | Effect |
|---|---|
--strip-inherited | When turning protection off, also drop the inherited rules already on the file. |
--recursive, -r | Apply to every descendant. |
sd reset #
Drops the file's own explicit rules and its protection, and rebuilds its DACL purely from what the parent directory hands down — returning the file to "inherits everything".
sd propagate #
Pushes this directory's inheritable rules down into its descendants, refreshing what they inherit. Use it after changing a directory's rules so the children pick up the change. Each descendant gets what it would get if it were created now: a CREATOR OWNER rule gives that descendant's own owner, and generic rights are translated into file rights.
A descendant that protects its rules (sd inherit off) is left as it is and counted in the summary, though the files below it are still refreshed from it. A descendant that can't be changed is reported, and the walk carries on.
| Flag | Effect |
|---|---|
--sacl | Push the audit rules (SACL) down as well. Needs SeSecurityPrivilege. |
--json | Report what was pushed, protected and refused as JSON. |
See Inheritance for the full model.
Replacing the whole descriptor #
sd set #
Writes the supplied descriptor components. On a file with a valid descriptor, the kernel merges selected components and selected control bits rather than replacing every descriptor byte. Missing or invalid file security descriptors use a separate privileged replacement path; see the kernel contract. Do not treat the selected output of sd show --sddl as a complete input backup.
$ sd set ./report.txt 'O:BAG:BAD:P(A;;FA;;;BA)(A;;0x1200a9;;;BU)'
| Argument / flag | Effect |
|---|---|
SDDL | The new descriptor as an SDDL string. - reads it from standard input. |
--binary FILE | Instead of SDDL, read the raw descriptor bytes from FILE (- for standard input). |
--components LIST | Override which parts of the descriptor (owner, group, DACL, SACL) the operation writes. |
Common flags #
These apply across the subcommands:
| Flag | Effect |
|---|---|
--recursive, -r | Apply the change to every descendant of the path. Implies --no-follow-symlinks. |
--no-follow-symlinks, -P | Operate on a symbolic link itself, not the file it points to. |
--json | Emit JSON instead of human-readable output. |
Recursion and symbolic links #
A recursive run stamps a tree, and a symbolic link is a node in that tree rather than a route out of it. So -r operates on link inodes and does not descend through them: the links themselves get the descriptor you asked for, and nothing outside the tree is touched. This matters on Peios more than it might elsewhere, because /usr/bin is around 170 links into a single multi-call binary — a recursive grant that followed them would stamp that one binary repeatedly and leave every command unreachable.
Name a path without -r and sd follows it, because naming a symbolic link is how you say you mean its target. If you want the link itself, pass -P.
seed-sd stamps trees the same way, so the two tools agree about what a symbolic link is.
Exit status #
| Code | Meaning |
|---|---|
0 | The operation succeeded — or, for sd check, the access would be allowed. |
| non-zero | The operation failed, the path was unreachable, or — for sd check — the access would be denied. |
Source and version scope #
The show selection and rendering described here are checked against peiosutils 0.8.18, commit 3344d469. This source review does not identify the version installed on a machine or establish a runtime-tested metadata export and restore procedure.
mkexec
Peios / Using Peios / Peiosutils / Files and directories
mkexec marks a regular file as a program, so that the kernel is willing to run it.
mkexec file...
$ mkexec ./report-tool
Why the mark exists #
On Peios, who may do what to a file is decided by the file's security descriptor. POSIX mode bits carry none of that authority, which is why Peios ships no chmod.
One part of the mode survives, with a different meaning. Before any access decision is made, the kernel refuses outright to execute a file that carries no execute bit anywhere. That refusal happens below the access-control layer, and it is not waived for administrators: an unmarked file is unrunnable for everybody, including Local System.
So on Peios the execute bit is not a permission. It is an intrinsic property of the file — a statement that the file is a program rather than data — and mkexec is how you set it.
What it does not do #
Marking a file executable grants nobody the right to execute it. That right comes from the file's security descriptor, and sd is the tool that grants it.
The two are separate steps, and a program needs both: the mark says the file is a program, the descriptor says who may run it. A file that is marked but not permitted fails the access check; a file that is permitted but not marked fails before the check is reached.
When a file arrives unmarked #
A file gets the mark when something deliberately sets it. A file written by a program that simply created it, or moved onto the system by a transport that did not carry the mark across, arrives without one — and refuses to run, usually reporting only "permission denied". mkexec is the fix.
$ ./deploy
sh: ./deploy: Permission denied
$ mkexec ./deploy
$ ./deploy
Details #
mkexec writes the mark to all three execute slots. Which slot carries it has no meaning under the Peios security model — the mark is a single property, and any slot satisfies it — so no arbitrary choice is made between them. Every other mode bit is left alone.
Marking a file that is already marked succeeds and changes nothing, so a script may mark the same file twice.
Symbolic links are followed. Marking a link marks the file it points at, which is the file that would actually be executed.
Only regular files can be marked. Directories, device nodes, and sockets are rejected: the mark means "this is a program", and nothing else can be one.
You can see the mark in the second column of ls -l, which shows x for a marked file and - for an unmarked one.
Exit status #
| Code | Meaning |
|---|---|
0 | Every file was marked. |
1 | A file could not be marked — it does not exist, it is not a regular file, or its security descriptor does not permit the change. |
Listing and paths
Peios / Using Peios / Peiosutils / Listing and paths
Before you can do anything with a file you usually have to find it — see what a directory holds, learn one file's details, turn a relative name into an absolute one. This topic covers the commands for that: listing directories, inspecting a single file, and resolving path names.
This page names every command in the topic, says in one line what each is for, and points you at the page that covers it in full.
For a worked example of choosing what must exist, start with Choose how to resolve a path.
The commands #
| Command | Purpose |
|---|---|
ls | List the contents of a directory. The everyday command for "what's in here?". |
dir and vdir | ls with a fixed output style. dir lists in columns; vdir lists in long format. |
stat | Show the detailed status of one file — its type, size, timestamps, and raw inode metadata. |
pwd | Print the directory you are currently working in. |
readlink | Print where a symbolic link points. |
realpath | Resolve a path to its canonical, absolute form — every symlink followed, every . and .. removed. |
basename | Strip the directory part off a path, leaving just the final name. |
dirname | The opposite of basename — strip the final name, leaving the directory. |
pathchk | Check whether a path name is valid and portable before you try to create it. |
dircolors | Produce the colour settings that ls uses when it colours its output. |
These commands and file security #
basename and dirname split path strings without consulting the file system. realpath normally resolves links through the file system, with options for missing components or text-only handling. pathchk checks names; its default checks include file-system length limits and whether leading directories can be searched.
ls and stat report on real files, and they relate to file security differently:
ls -lsurfaces security-descriptor information directly. On Peios a file's access is governed by its security descriptor — the record of who owns the file and who may do what to it. The long listing shows the owner and the parts of the descriptor that fit in a one-line-per-file format.statis a low-level diagnostic. It dumps the raw contents of a file's inode. The inode carries some numeric fields — an owner id, a group id, a mode value — that the Peios access model does not consult.statreports them because it reports the inode verbatim, but they are not the file's access policy.
Neither command prints the full security descriptor; inspecting that is a job for the dedicated security tooling. If a line of ls -l looks unfamiliar, or you want to know why stat shows fields that "don't count", the explanation is in Security descriptors.
Where to start #
If you want the everyday "what's in this directory" command and the meaning of every column it prints, read ls.
If you need the full detail of one specific file, read stat.
If you are writing a script, use basename or dirname to split a path as text. Use realpath to resolve it with the existence and link-handling options your task needs.
Choose how to resolve a path
Peios / Using Peios / Peiosutils / Listing and paths
When a script receives a path, first decide what must exist. Resolving an existing input, naming a new output file, and planning a directory tree need different checks.
Start with one working directory #
Assume the physical working directory is /home/jack/project, with no symbolic links in that directory path:
notes.txtis an existing ordinary file.latestis a symbolic link whose stored target isnotes.txt.report.txtdoes not exist yet.plannedand its child directorydraftsdo not exist yet.
The examples use those files and directories and show the expected output. Relative paths are interpreted from the working directory, which you can inspect with pwd:
$ pwd -P
/home/jack/project
In a script, check the command's exit status before using its output. The examples omit realpath -q: affected builds can report success for invalid operands in quiet mode. See the precise compatibility warning and workaround. Nonempty output alone does not establish success.
Read a link's immediate target #
Use plain readlink when you want the path stored in the link:
$ readlink "latest"
notes.txt
That is the immediate target, not an absolute path or a check that the target exists. Plain readlink "notes.txt" would fail because notes.txt is an ordinary file; that failure does not prove the file is absent.
Require an existing input #
Use realpath -e when every component, including the final file, must exist:
$ realpath -e "latest"
/home/jack/project/notes.txt
Unlike plain readlink, this resolves the whole path, following symbolic links and removing . and ... It also accepts an ordinary file. Under the example assumptions, realpath -e "report.txt" would fail because that final file is absent.
Name a new file under existing directories #
Default realpath allows the final component to be absent, but requires its parents to exist:
$ realpath "report.txt"
/home/jack/project/report.txt
Choose this when preparing an output filename in an existing directory. Resolving the name does not create the file or establish that a later write will succeed.
Plan a path with missing parents #
Use -m when parent directories may be absent too:
$ realpath -m "planned/drafts/report.txt"
/home/jack/project/planned/drafts/report.txt
Where the path cannot be walked, it is canonicalised as text. This gives you a planned spelling; it does not create directories or establish access. Default realpath would fail for this example because the parent directories are absent.
Split a name or check it #
If you only need part of a path, basename and dirname operate on text without consulting the file system:
$ basename "planned/drafts/report.txt"
report.txt
$ dirname "planned/drafts/report.txt"
planned/drafts
pathchk checks names instead: its default checks include file-system length limits and whether leading directories can be searched. A successful check does not guarantee that a future write will succeed.
For relative output, other link-handling modes, and full option tables, use the realpath reference.
ls
Peios / Using Peios / Peiosutils / Listing and paths
ls lists the contents of a directory. It is the command you use to answer "what is in here?" — and, with the right options, "who owns these files, how big are they, and when were they last changed?".
ls [options] [file...]
Each file is a directory to list the contents of, or a single file to report on. With no file argument, ls lists the directory you are currently in.
$ ls
build.sh notes.txt projects
The default listing #
Run on its own, ls prints the names of the entries in a directory and nothing else. Three rules shape that default:
- Hidden entries are skipped. A name beginning with a dot (
.config,.cache) is hidden.lsleaves hidden entries out unless you ask for them with-aor-A. - Entries are sorted by name. Alphabetical order, case-sensitive. Other sort orders are available.
- The layout adapts to where output goes. When
lsis writing to a terminal it arranges names into columns sized to the terminal width. When output is redirected to a file or a pipe it prints one name per line, so the result is easy for another program to read.
Everything past this default is opt-in. The rest of this page is the options, grouped by what they do.
The long format #
-l (or --long) switches ls into the long format: one entry per line, each with its mode, owner, size, modification time, and name.
$ ls -l
total 28
d-- S-1-5-21-9f3a-1c4e-7b20-1001 4096 May 14 09:12 projects
-x- S-1-5-21-9f3a-1c4e-7b20-1001 8344 May 16 17:40 build.sh
--- S-1-5-21-9f3a-1c4e-7b20-1001 219 May 15 11:03 notes.txt
--+ S-1-5-18 12048 May 10 22:15 policy.db
The total line at the top reports the combined disk space used by the listed files, measured in blocks.
Each entry line has five columns: mode, owner, size, modification time, name. There is no permission-bits column and no group column — the reason is below.
The mode column #
The mode column is three characters: [type][executable][protected].
The first character is the file's type:
| Character | Type |
|---|---|
- | Regular file |
d | Directory |
l | Symbolic link |
p | Named pipe (FIFO) |
s | Socket |
c | Character device |
b | Block device |
The second character is the executable mark. It is x when the entry is a regular file that is marked as an executable, and - otherwise (it is always - for directories, links, and other non-regular entries).
The executable mark describes the file, not your relationship to it. x means "this file is runnable code." It does not mean that you, specifically, are allowed to run it — whether a given principal may execute a file is a separate access decision made against the file's security descriptor. A file can show x and still be one you cannot run, and the reverse. See Access decisions for how execution is actually authorised.
The third character is the protected mark. It is + when the file's access control is inheritance-protected, and - otherwise.
A - here means the file's access control is inherited from the directory it lives in: it tracks its parent. A + means the file's access control has been set deliberately and locked, so it no longer tracks the parent directory. The + is the signal that someone has tailored this file's security on purpose. See Inheritance.
So in the example above: projects is a directory (d--); build.sh is an executable file (-x-); notes.txt is an ordinary file inheriting its security from its directory (---); and policy.db is an ordinary file whose access control has been protected (--+).
Why there are no permission bits #
A long listing does not carry a fixed block of permission characters. On Peios, what a principal may do to a file is decided by the file's security descriptor — a record that can contain any number of entries granting or denying specific rights to specific principals. There is no small, fixed set of bits that captures that, so ls -l does not pretend there is.
The mode column tells you the file's type and two facts about it (is it executable, is its security protected). To see who may do what, look at the security descriptor itself with stat or the dedicated security tooling. The concepts are in Security descriptors.
The owner column #
The owner column is the SID of the file's owner — shown in full, in the S-1-… form. ls does not translate SIDs into account names; it prints the identifier exactly as the security descriptor stores it. A well-known owner such as the system itself appears as its fixed SID (S-1-5-18); an ordinary account appears as a longer domain-style SID.
For what a SID is and how to read one, see SIDs. For what "owner" means and what it grants, see Ownership.
If ls cannot read a file's descriptor — for a dangling symlink, say — the owner column shows ? and the mode column shows the type character followed by ??.
Choosing what to list #
By default ls shows non-hidden entries, and for a directory argument it shows the directory's contents. These options change that.
| Option | Effect |
|---|---|
-a, --all | Show hidden entries too, including . (the directory itself) and .. (its parent). |
-A, --almost-all | Show hidden entries, but leave out . and ... |
-d, --directory | List a directory as an entry in its own right, instead of listing its contents. Useful with -l to see a directory's own owner and mode. |
-R, --recursive | Descend into every subdirectory and list it too. |
-B, --ignore-backups | Skip entries whose name ends in ~. |
--ignore=PATTERN | Skip entries whose name matches the shell pattern. May be given more than once. |
--hide=PATTERN | Like --ignore, but overridden by -a or -A — so a hide rule can be cancelled by asking to see everything. |
Sorting #
ls sorts by name by default. These options change the sort key; -r reverses whatever order is in effect.
| Option | Sort order |
|---|---|
-t | By modification time, newest first. |
-S | By size, largest first. |
-X | By file extension, alphabetically. |
-v | By version: runs of digits in the name sort numerically, so f2 comes before f10. |
-U | No sorting at all — entries appear in the order the directory stores them. Fast for very large directories. |
-r, --reverse | Reverse the current sort. |
--sort=WORD | Choose the sort key by name: none, time, size, extension, version, or width. |
--group-directories-first | List directories before other entries, with each group sorted normally. |
Output layout #
When several entries fit on a line, ls has to decide how to arrange them.
| Option | Layout |
|---|---|
-C | Columns, filled top-to-bottom. The default when writing to a terminal. |
-x | Columns, filled left-to-right (across the rows) instead. |
-1 | One entry per line. The default when output is not a terminal. |
-m | All entries on as few lines as possible, separated by commas. |
-l, --long | The long format described above. |
--format=WORD | Choose the layout by name: across, commas, horizontal, long, single-column, or vertical. |
-w, --width=COLS | Assume the terminal is COLS columns wide instead of detecting it. 0 means unlimited. |
File-type indicators and colour #
These options make a listing easier to scan by marking entries with a symbol or a colour.
| Option | Effect |
|---|---|
-F, --classify | Append a type symbol to each name: / for a directory, @ for a symlink, | for a FIFO, = for a socket, and * for an executable file. |
--file-type | The same, but without the * on executables. |
-p | Append only the / on directories. |
--indicator-style=WORD | Choose the indicator set: none, slash, file-type, or classify. |
--color[=WHEN] | Colour entries by type. WHEN is auto (colour only when writing to a terminal — the usual choice), always, or never. |
The colours ls uses are configurable. dircolors generates the colour settings and explains how to install them.
File sizes #
In the long format, and with -s, sizes are printed in bytes by default. These options rescale them.
| Option | Effect |
|---|---|
-h, --human-readable | Print sizes with a unit suffix — 4.0K, 234M, 56G — using powers of 1024. |
--si | Like -h, but using powers of 1000, so the suffixes are decimal. |
-s, --size | Print the disk space each entry occupies, in blocks, before its name. |
-k, --kibibytes | Use 1024-byte blocks for the size figures and directory totals. |
--block-size=SIZE | Scale every size by SIZE (for example --block-size=1M to count in megabytes). |
Timestamps #
The long format shows the modification time by default. These options change which timestamp is shown — and, when sorting by time, which one is sorted on.
| Option | Timestamp |
|---|---|
-u | Access time — when the file was last read. |
-c | Status-change time — when the file's metadata last changed. |
--time=WORD | Choose the timestamp by name: atime/access, ctime/status, mtime/modification, or birth/creation. |
--time-style=STYLE | Choose the date format: full-iso, long-iso, iso, locale, or +FORMAT for a custom layout. |
--full-time | Shorthand for the long format with --time-style=full-iso — a complete, unabbreviated timestamp. |
Symbolic links #
By default ls reports on a symbolic link itself — its type is l, its size is the length of the link text. These options make it report on the link's target instead.
| Option | Effect |
|---|---|
-L, --dereference | Always report on the file a link points to, not the link. |
-H, --dereference-command-line | Dereference only the links named directly on the command line, not links found inside a listed directory. |
--dereference-command-line-symlink-to-dir | Dereference a command-line link only when it points to a directory. |
readlink and realpath are the dedicated tools for inspecting and resolving links.
Other options #
The long-tail options, each in one line.
| Option | Effect |
|---|---|
-i, --inode | Print each entry's index number (its inode number). |
-Z, --context | Print each entry's security context. Available only when the build enables it. |
-b, --escape | Print non-printable characters in names using C-style backslash escapes. |
-q, --hide-control-chars | Replace non-printable characters in names with ?. The default when writing to a terminal. |
--show-control-chars | Print names verbatim, control characters and all. |
-N, --literal | Print names exactly, with no quoting. |
-Q, --quote-name | Wrap each name in double quotes. |
--quoting-style=WORD | Choose the quoting scheme: literal, shell, shell-always, shell-escape, c, escape, and others. |
-T, --tabsize=COLS | Assume tab stops every COLS columns when laying out output. |
-f | List everything, unsorted, including hidden entries — equivalent to -aU. Also turns colour off unless --color is given explicitly. |
--hyperlink[=WHEN] | Emit terminal hyperlinks for file names, so a capable terminal can make them clickable. |
-D, --dired | Emit extra position markers designed for the Emacs dired editing mode. |
--zero | End each line with a NUL character instead of a newline, and list one entry per line. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success — everything requested was listed. |
1 | A minor problem — for example, a named file could not be accessed. The rest of the listing still printed. |
2 | A serious problem — for example, a directory could not be read, or an option was invalid. |
dir and vdir
Peios / Using Peios / Peiosutils / Listing and paths
dir and vdir list the contents of a directory. They are ls with one decision already made for you: the output style is fixed instead of adapting to where the output is going.
dir [options] [file...]
vdir [options] [file...]
What they fix #
ls chooses its layout based on whether it is writing to a terminal — columns for a terminal, one name per line for a pipe or file. dir and vdir each pick one layout and always use it:
| Command | Layout | Equivalent ls |
|---|---|---|
dir | Columns, regardless of where output goes. | ls -C |
vdir | Long format, regardless of where output goes. | ls -l |
Both also default to a quoting style that escapes unusual characters in names without wrapping every name in quotes.
That is the only difference. The fixed style is just a default — pass an explicit format option (-1, -m, -l, -C, …) and it overrides the built-in choice, so dir -l produces a long listing and vdir -C produces columns.
Everything else is ls #
dir and vdir accept every option ls accepts and behave identically in every other respect — sorting, filtering, the long-format columns, colour, indicators, sizes, timestamps. vdir's long format is the same Peios long format documented for ls, with the [type][executable][protected] mode column and the owner SID.
For the full option set and the meaning of every long-format column, see ls. This page exists only to explain how dir and vdir differ from it — and the answer is "they pin the output style."
Exit status #
The same as ls:
| Code | Meaning |
|---|---|
0 | Success. |
1 | A minor problem — a named file could not be accessed. |
2 | A serious problem — a directory could not be read, or an option was invalid. |
stat
Peios / Using Peios / Peiosutils / Listing and paths
stat displays the detailed status of a file. Where ls gives you a line per file, stat gives you everything the system records about one file: its type, its size, its timestamps, and the low-level metadata stored in its inode.
stat [options] file...
stat is a diagnostic tool. It dumps what a file's inode physically contains, verbatim. That makes it the right command for "tell me exactly what is recorded about this file" — and it means some of what it prints needs a word of explanation, below.
The default output #
With no options, stat prints a labelled, multi-line block per file:
$ stat notes.txt
File: notes.txt
Size: 219 Blocks: 8 IO Block: 4096 regular file
Device: 8,2 Inode: 1572931 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ jack) Gid: ( 1000/ jack)
Access: 2026-05-15 11:03:42.000000000 +0000
Modify: 2026-05-15 11:03:42.000000000 +0000
Change: 2026-05-15 11:03:18.000000000 +0000
Birth: 2026-05-15 11:03:18.000000000 +0000
Reading it line by line:
- File — the name, and for a symbolic link, what it points to.
- Size / Blocks / IO Block / type — the size in bytes, the number of allocated disk blocks, the preferred I/O block size, and the file type in words.
- Device / Inode / Links — the device the file lives on, its inode number (its unique index within that device), and the number of hard links to it.
- Access / Uid / Gid — a numeric owner id, a group id, and a mode value. See the caveat below — these are not the access policy.
- Access / Modify / Change / Birth — four timestamps: when the file was last read, last written, last had its metadata changed, and first created.
The Uid, Gid, and mode fields are decorative #
The Access: (0644/-rw-r--r--) Uid: (…) Gid: (…) line needs care.
A file's inode carries a numeric owner id, a numeric group id, and a mode value, and stat reports them because stat reports the inode verbatim. But the Peios access model does not consult these fields. What a principal may do to a file is decided by the file's security descriptor — see Security descriptors. The numbers on the Uid/Gid/mode line are inert metadata: they are stored, they are reported, and they have no effect on any access decision.
Do not read that line as a permission summary. A file showing 0644 is not "world-readable" in any meaningful sense — whether anyone can read it depends entirely on its security descriptor. stat shows these fields for completeness as a low-level diagnostic; it does not claim they mean anything.
To see the parts of a file's security that do count, use ls -l, which shows the owner SID and the mode column built from the security descriptor.
Custom output formats #
Two options replace the default block with output you control.
| Option | Effect |
|---|---|
-c FORMAT, --format=FORMAT | Print FORMAT for each file, substituting % directives. A newline is added after each file. |
--printf=FORMAT | Like --format, but interpret backslash escapes (\n, \t) in FORMAT and add no trailing newline. Include \n yourself if you want one. |
$ stat --format='%n is %s bytes' notes.txt
notes.txt is 219 bytes
File directives #
Used in the format string when stating files (the default mode):
| Directive | Substitutes |
|---|---|
%n | File name. |
%N | Quoted file name; for a symlink, with the target shown. |
%F | File type in words (regular file, directory, …). |
%s | Total size, in bytes. |
%b | Number of allocated blocks. |
%B | Size in bytes of each block counted by %b. |
%o | Preferred I/O transfer block size. |
%i | Inode number. |
%h | Number of hard links. |
%d / %D | Device number, in decimal / in hexadecimal. |
%t / %T | For a device file, the major / minor device type, in hexadecimal. |
%m | Mount point of the file system the file is on. |
%f | Raw mode value, in hexadecimal. |
%x / %X | Last access time — human-readable / seconds since the epoch. |
%y / %Y | Last modification time — human-readable / seconds since the epoch. |
%z / %Z | Last status-change time — human-readable / seconds since the epoch. |
%w / %W | File creation (birth) time — human-readable / seconds since the epoch. - or 0 if unknown. |
The directives %a and %A (the mode in octal and in symbolic form) and %u, %U, %g, %G (the owner and group, numeric and by name) report the decorative inode fields described above. They are available for completeness; they are not the file's access policy.
The %C directive (security context) is inactive — it produces no meaningful value on a standard Peios system.
File-system directives #
With -f, stat reports on the file system a file lives on rather than the file itself, and the format string uses a different directive set:
| Directive | Substitutes |
|---|---|
%n | File name. |
%i | File-system ID, in hexadecimal. |
%t / %T | File-system type — in hexadecimal / in words. |
%l | Maximum length of a file name. |
%s | Block size, for fast transfers. |
%S | Fundamental block size, used for the block counts. |
%b | Total data blocks in the file system. |
%f | Free blocks. |
%a | Free blocks available to an ordinary principal. |
%c | Total inodes (file nodes). |
%d | Free inodes. |
Options #
| Option | Effect |
|---|---|
-f, --file-system | Report on the file system containing each file, instead of the file. |
-L, --dereference | Follow symbolic links — report on the link's target rather than the link itself. |
-t, --terse | Print the information on a single line, as bare values with no labels. Useful for scripts. |
Exit status #
| Code | Meaning |
|---|---|
0 | Every file was stated successfully. |
1 | A file could not be stated, or an option or format directive was invalid. |
pwd
Peios / Using Peios / Peiosutils / Listing and paths
pwd — "print working directory" — prints the absolute path of the directory you are currently in.
pwd [options]
$ pwd
/home/jack/projects
Every process has a working directory: the directory that relative path names are interpreted against. pwd tells you what yours is.
Physical and logical paths #
There are two honest answers to "where am I", and they differ only when symbolic links are involved.
Suppose /home/jack/work is a symbolic link pointing at /data/projects/jack, and you move into it. The physical path — the real location, with every link resolved — is /data/projects/jack. The logical path — the route you took to get there — is /home/jack/work.
| Option | Path printed |
|---|---|
-P, --physical | The physical path: every symbolic link resolved to its target. |
-L, --logical | The logical path: the route recorded as you navigated, taken from the PWD environment variable when it is accurate. |
By default pwd prints the physical path — -P is the default, and naming it explicitly just makes that choice visible. Use -L when you want the path as you navigated it, links and all.
If -L is requested but the recorded PWD value is missing or does not actually match the current directory, pwd falls back to the physical path rather than print something wrong.
A note on shells #
Many command shells provide their own built-in pwd, and the shell's version is what runs when you type pwd at a prompt. The two behave the same for everyday use. To be certain you are running this command rather than the shell built-in, invoke it by its full path.
Exit status #
| Code | Meaning |
|---|---|
0 | The working directory was printed. |
1 | The working directory could not be determined or could not be printed. |
readlink
Peios / Using Peios / Peiosutils / Listing and paths
readlink prints where a symbolic link points.
readlink [options] file...
A symbolic link is a file whose contents are another path. readlink reads that path and prints it:
$ readlink /home/jack/work
/data/projects/jack
By default readlink resolves exactly one level: it prints the link's immediate target, whether or not that target is itself a link, and whether or not it exists. If the named file is not a symbolic link, readlink prints nothing and reports failure.
Canonicalising a whole path #
The canonicalise options change readlink from "read this one link" into "resolve this entire path to its real, absolute form" — following every symbolic link in every component, collapsing . and ... The three differ only in how strict they are about components existing:
| Option | Resolves | Existence requirement |
|---|---|---|
-f, --canonicalize | Every link in the path. | Every component except the last must exist. |
-e, --canonicalize-existing | Every link in the path. | Every component must exist. |
-m, --canonicalize-missing | Every link in the path. | No component need exist. |
With any of these, readlink succeeds on an ordinary (non-link) file too — it simply returns the canonical path. realpath is the dedicated command for this canonicalising job and has more options for it; the canonicalise flags here exist so readlink can do it without a second tool.
Output and error control #
| Option | Effect |
|---|---|
-n, --no-newline | Do not print the trailing newline. Ignored, with a warning, when more than one file is given. |
-z, --zero | End each output line with a NUL character instead of a newline. |
-q, --quiet / -s, --silent | Suppress most error messages. This is the default. |
-v, --verbose | Report error messages that the quiet default would suppress. |
When POSIXLY_CORRECT is set, readlink reports errors even with -q or -s.
With several operands, the current implementation stops at the first failure. Output already printed is retained, but later operands are not examined. Treat a failed invocation as incomplete.
Exit status #
| Code | Meaning |
|---|---|
0 | Every named file was resolved and printed. |
1 | In default mode, a file was not a symbolic link; in canonicalising mode, a path could not be resolved under the chosen strictness. |
realpath
Peios / Using Peios / Peiosutils / Listing and paths
By default, realpath prints an absolute path with symbolic links resolved and . and .. components removed. Options below change which components must exist, how links are handled, and whether output is absolute or relative.
realpath [options] file...
$ realpath ../work/./notes.txt
/data/projects/jack/notes.txt
Use realpath in scripts to turn a relative path into an absolute path or remove redundant components. Choose the existence and link-handling options for the job, and check the exit status before using the result.
How much must exist #
By default realpath requires every component of the path except the last to exist. These options change that requirement:
| Option | Existence requirement |
|---|---|
-E, --canonicalize (default) | Every component except the last must exist. |
-e, --canonicalize-existing | Every component must exist — including the final one. |
-m, --canonicalize-missing | No component need exist. The path is canonicalised purely as text where it cannot be walked. |
How symbolic links are handled #
| Option | Effect |
|---|---|
-P, --physical | Resolve each symbolic link as it is encountered. This is the default. |
-L, --logical | Resolve .. components before resolving the symbolic links they follow. |
-s, --strip, --no-symlinks | Do not resolve symbolic links at all — only remove . and .. components. The result is canonical as text, but a link in the path is left as-is. |
Output options #
| Option | Effect |
|---|---|
--relative-to=DIR | Print the result as a path relative to DIR instead of as an absolute path. |
--relative-base=DIR | Print an absolute path, unless the result lies inside DIR, in which case print it relative to DIR. |
-z, --zero | End each output line with a NUL character instead of a newline. |
-q, --quiet | Do not print a warning when a path is invalid. See Exit status below for the quiet-mode compatibility warning and script workaround. |
realpath and readlink #
readlink with -f/-e/-m does the same canonicalising job. The difference is focus: readlink is primarily for reading a single link's target, with canonicalising as an extra; realpath is built for canonicalising and carries the richer option set — relative output, the symlink-handling modes, the strip-only mode. Use realpath when canonicalising is the actual goal.
Exit status #
| Code | Meaning |
|---|---|
0 | Every path was resolved and printed. |
1 | A path could not be resolved under the chosen options or its output could not be written. |
Compatibility warning: builds using the uncorrected implementation can return 0 for invalid operands when -q is used. Until your build includes the quiet-status correction, omit -q when a script must detect those failures. Successful operands are still printed, so nonempty output alone does not show that every operand succeeded.
basename
Peios / Using Peios / Peiosutils / Listing and paths
basename takes a path and prints just its final component — the file name, with all the leading directories removed.
basename name [suffix]
basename [options] name...
$ basename /home/jack/projects/notes.txt
notes.txt
It is a pure text operation: basename never looks at the file system. It works on the string you give it, so the path need not exist.
Removing a suffix #
Give a second argument and basename also strips that suffix from the end of the name — handy for turning a file name into a bare stem:
$ basename /home/jack/projects/notes.txt .txt
notes
The suffix is only removed if it is actually there, and never if it would leave an empty string.
Options #
basename takes one name by default. The options let it process several at once.
| Option | Effect |
|---|---|
-a, --multiple | Treat every argument as a name to process, instead of treating the second argument as a suffix. Required to pass more than one name. |
-s, --suffix=SUFFIX | Remove SUFFIX from each name. Implies -a, so it applies to every name given. |
-z, --zero | End each output line with a NUL character instead of a newline. |
$ basename -s .txt notes.txt readme.txt changelog.txt
notes
readme
changelog
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A usage error — a missing or extra operand. |
dirname
Peios / Using Peios / Peiosutils / Listing and paths
dirname takes a path and prints everything except its final component — the directory the name lives in. It is the counterpart of basename.
dirname [options] name...
$ dirname /home/jack/projects/notes.txt
/home/jack/projects
Like basename, it is a pure text operation — dirname never touches the file system, so the path need not exist.
How it handles edge cases #
- If the name has no
/in it at all, there is no directory part, sodirnameprints.— the current directory. - Trailing slashes on the name are ignored before the final component is removed.
$ dirname notes.txt
.
$ dirname /home/jack/projects/
/home
Multiple names #
dirname accepts any number of names and prints the directory part of each on its own line:
$ dirname /etc/hosts /var/log/messages report.txt
/etc
/var/log
.
Options #
| Option | Effect |
|---|---|
-z, --zero | End each output line with a NUL character instead of a newline. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A usage error — no name was given. |
pathchk
Peios / Using Peios / Peiosutils / Listing and paths
pathchk checks whether a path name is valid and usable. It tells you, before you try to create a file, whether the name would be rejected — because it is too long, contains an unusable component, or could not be reached.
pathchk [options] name...
$ pathchk /home/jack/projects/notes.txt
$ echo $?
0
pathchk prints nothing when a name is fine; it prints a diagnostic and fails when a name is not. It is meant for scripts that build up path names and want to fail early, with a clear message, instead of discovering the problem halfway through an operation.
Like the other name commands in this topic, pathchk does not require the file to exist — it is checking the name, not looking for the file.
What the default check covers #
With no options, for each name pathchk checks:
- that the name is not empty;
- that no part of the name exceeds the length limits of the file systems the path would actually touch;
- that the leading directories of the name can be searched.
Stricter checks #
The options replace "valid on this system" with "valid across a wide range of systems" — useful when a name has to work somewhere other than where you are checking it.
| Option | Adds |
|---|---|
-p | Check against a fixed, conservative set of length limits instead of the local file system's, and flag characters that are not broadly portable. |
-P | Reject an empty name, and reject any component that begins with -. |
--portability | Apply both -p and -P — the strictest check. |
A name with a leading - on a component is worth catching: many commands would read it as an option rather than a file name. -P flags exactly that class of trouble.
Exit status #
| Code | Meaning |
|---|---|
0 | Every name passed every requested check. |
1 | At least one name failed. A diagnostic naming the problem was printed. |
dircolors
Peios / Using Peios / Peiosutils / Listing and paths
When ls colours its output, it reads the colours from an environment variable named LS_COLORS. dircolors is the command that produces the value of that variable.
dircolors [options] [file]
dircolors does not set the variable itself — a command cannot change its parent shell's environment. Instead it prints shell commands that, when run by the shell, set LS_COLORS. The usual way to use it is to have the shell evaluate that output:
eval "$(dircolors)"
Put that line in a shell startup file and every ls --color afterwards picks up the colours. With no arguments, dircolors uses a built-in default colour scheme.
Choosing the shell syntax #
The commands dircolors prints have to match the shell that will run them. By default it guesses the syntax from the SHELL environment variable; these options state it outright.
| Option | Output syntax |
|---|---|
-b, --sh, --bourne-shell | Bourne-shell-family syntax. |
-c, --csh, --c-shell | C-shell-family syntax. |
If no shell option is given and SHELL is not set, dircolors cannot guess and reports an error.
Customising the colours #
To change the colours, you start from the default scheme, edit it, and feed it back.
| Option | Effect |
|---|---|
-p, --print-database | Print the built-in colour database in its editable source form, instead of shell commands. |
--print-ls-colors | Print the colours fully escaped, one per line, for inspection. |
The workflow is:
dircolors -p > ~/.dircolors # save the default scheme to a file
# ...edit ~/.dircolors to taste...
eval "$(dircolors ~/.dircolors)" # load the edited scheme
When dircolors is given a file argument, it reads the colour definitions from that file instead of using the built-in database. The file maps file types and name extensions to colours; dircolors -p shows the format, with each line commented.
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A usage error, or a colour-definition file that could not be read or parsed. |
Viewing and joining text
Peios / Using Peios / Peiosutils / Viewing and joining text
Much of the work at a command line is reading text — looking at a file, checking the start or end of one, pulling a column out, stitching two files together. This topic covers the commands for that: viewing files, showing parts of them, and joining or splitting text by line and by column.
This page names every command in the topic. The companion topic, Transforming text, covers the commands that reshape text — sorting, de-duplicating, substituting characters.
The commands #
Viewing a whole file
| Command | Purpose |
|---|---|
cat | Print one or more files straight through — and join several into one. |
tac | Print a file with its lines in reverse — last line first. |
more | Show a file one screen at a time, pausing between screens. |
nl | Print a file with its lines numbered. |
od | Dump a file's raw bytes in octal, hex, decimal, or character form. |
Viewing part of a file
| Command | Purpose |
|---|---|
head | Print the first lines (or bytes) of a file. |
tail | Print the last lines of a file — and, with -f, keep printing as the file grows. |
Cutting and combining
| Command | Purpose |
|---|---|
cut | Pull selected columns — byte ranges or delimited fields — out of each line. |
paste | Join files side by side, line for line. |
join | Join two files on a shared field, like a database join. |
comm | Compare two sorted files and report which lines they share. |
Splitting a file into files
| Command | Purpose |
|---|---|
split | Break a file into equal-sized pieces. |
csplit | Break a file into pieces at lines that match a pattern. |
A note on input #
Almost every command here follows the same convention for where its text comes from. Name one or more files and it reads those; name none and it reads from standard input, so it can sit in the middle of a pipeline. Where a command accepts files, the name - stands for standard input, so you can mix files and piped input in one invocation.
Where to start #
To simply print a file, or join a few together, read cat — the most-used command in the topic.
cat
Peios / Using Peios / Peiosutils / Viewing and joining text
cat prints the contents of files. Its name is short for "concatenate" — given several files, it prints them one after another, as a single stream.
cat [options] [file...]
$ cat notes.txt
$ cat part1.txt part2.txt part3.txt > whole.txt
With no file — or with the name - — cat reads standard input, which is what makes it useful in a pipeline or for capturing typed input into a file.
Plain use #
Most of the time cat is run with no options at all: it copies its input to its output, byte for byte, unchanged. The options exist for the times you want to see something about the text that is normally invisible, or to adjust spacing and numbering.
Numbering lines #
| Option | Effect |
|---|---|
-n, --number | Number every output line. |
-b, --number-nonblank | Number only the non-empty lines. Overrides -n. |
Making invisible characters visible #
These options reveal characters that normally print as nothing, or as whitespace — useful when a file is not behaving and you suspect a stray tab or control character.
| Option | Effect |
|---|---|
-E, --show-ends | Print a $ at the end of each line, so trailing spaces become visible. |
-T, --show-tabs | Print tab characters as ^I instead of as whitespace. |
-v, --show-nonprinting | Print control and other non-printing characters using ^ and M- notation (leaving newline and tab as they are). |
-e | Shorthand for -vE. |
-t | Shorthand for -vT. |
-A, --show-all | Shorthand for -vET — show ends, tabs, and all non-printing characters at once. |
Adjusting spacing #
| Option | Effect |
|---|---|
-s, --squeeze-blank | Collapse runs of blank lines into a single blank line. |
Exit status #
| Code | Meaning |
|---|---|
0 | Every file was printed. |
1 | A file could not be read. |
tac
Peios / Using Peios / Peiosutils / Viewing and joining text
tac prints a file with its lines in reverse — the last line first, the first line last. The name is cat spelled backwards, which is exactly what it does.
tac [options] [file...]
$ tac events.log
Reversing a log file so the newest entries come first is the everyday use. With no file, tac reads standard input.
Reversing on something other than lines #
tac does not have to split on newlines. You can tell it to treat some other string as the separator, and it will reverse the order of the pieces between those separators.
| Option | Effect |
|---|---|
-s, --separator=STRING | Use STRING as the separator between records, instead of a newline. |
-r, --regex | Treat the separator as a regular expression rather than a literal string. |
-b, --before | Expect the separator before each record rather than after it. This matters for how the separator is reattached when the records are reordered. |
$ tac -s ', ' -r names.csv
Exit status #
| Code | Meaning |
|---|---|
0 | Every file was printed. |
1 | A file could not be read, or the separator regular expression was invalid. |
head
Peios / Using Peios / Peiosutils / Viewing and joining text
head prints the beginning of a file — by default, the first 10 lines.
head [options] [file...]
$ head config.toml
It is the quick way to glance at the top of a file without printing the whole thing — a header, the first few records, the start of a log. With no file, head reads standard input.
Choosing how much #
| Option | Effect |
|---|---|
-n, --lines=NUM | Print the first NUM lines instead of 10. |
-c, --bytes=NUM | Print the first NUM bytes instead of counting lines. |
NUM accepts a unit suffix (K, M, …) when counting bytes.
Counting from the end #
If NUM is given a leading -, the meaning flips: head prints everything except the last NUM.
$ head -n -5 report.txt # all but the final 5 lines
Headers for multiple files #
When given more than one file, head prints a header line before each one so you can tell them apart. Two options override that:
| Option | Effect |
|---|---|
-q, --quiet | Never print the file-name headers. |
-v, --verbose | Always print the header, even for a single file. |
The header is printed only once the file has actually been opened. A file that exists but cannot be read produces just the error message, with no ==> name <== line before it.
Other options #
| Option | Effect |
|---|---|
-z, --zero-terminated | Treat the NUL character as the line delimiter instead of newline. |
Exit status #
| Code | Meaning |
|---|---|
0 | Every file was printed. |
1 | A file could not be read. |
tail
Peios / Using Peios / Peiosutils / Viewing and joining text
tail prints the end of a file — by default, the last 10 lines.
tail [options] [file...]
$ tail server.log
It is the counterpart of head: the quick way to see the most recent lines of a file. With no file, tail reads standard input.
Choosing how much #
| Option | Effect |
|---|---|
-n, --lines=NUM | Print the last NUM lines instead of 10. |
-c, --bytes=NUM | Print the last NUM bytes instead of counting lines. |
With -n, if NUM is given a leading +, the meaning flips: tail prints everything from line NUM onward.
$ tail -n +20 report.txt # from line 20 to the end
Following a growing file #
tail's most useful trick is -f. Instead of printing the end and exiting, it stays running and prints new lines as they are appended — the standard way to watch a log file live.
| Option | Effect |
|---|---|
-f, --follow | Keep the file open and print new data as it is added. |
-F | Follow the file by name, and keep retrying if it is missing. Equivalent to --follow=name --retry. This survives log rotation — when the file is replaced, -F picks up the new one. |
--retry | Keep trying to open a file that is not yet accessible. |
--pid=PID | While following, stop once process PID exits. |
-s, --sleep-interval=N | Wait N seconds between checks of the file. |
Following by name picks up a file that replaces the watched one. If what appears under the name is a symbolic link, tail reports that the file "has been replaced with an untailable symbolic link" and stops reading it, rather than following the link to wherever it points.
Press Ctrl-C to stop a tail -f.
Headers for multiple files #
As with head, when given more than one file tail prints a header before each.
| Option | Effect |
|---|---|
-q, --quiet | Never print the file-name headers. |
-v, --verbose | Always print the header, even for a single file. |
Other options #
| Option | Effect |
|---|---|
-z, --zero-terminated | Treat the NUL character as the line delimiter instead of newline. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A file could not be read. |
more
Peios / Using Peios / Peiosutils / Viewing and joining text
more displays a file one screenful at a time. Where cat prints a whole file at once — and a long file scrolls straight past — more shows the first screen, then waits for you before showing the next.
more [options] file...
$ more long-report.txt
Moving through the file #
While more is paused, it is waiting for a keypress:
| Key | Action |
|---|---|
| Space | Show the next screenful. |
| Return | Show the next line. |
q | Quit. |
/ | Search forward for a string. |
h | Show the built-in help. |
more moves forward through a file. It is the simple, always-available pager — enough for reading something through once.
Options #
| Option | Effect |
|---|---|
-n, --lines=NUM | Show NUM lines per screenful instead of filling the terminal. --number=NUM is the same. |
-F, --from-line=NUM | Begin displaying at line NUM. |
-P, --pattern=STRING | Search for STRING and begin displaying at the first match. |
-e, --exit-on-eof | Exit automatically at the end of the file, rather than waiting. |
-s, --squeeze | Collapse runs of blank lines into one. |
-u, --plain | Suppress underlining in the displayed text. |
-p, --print-over | Clear the screen and print the next page, instead of scrolling. |
-c, --clean-print | Redraw each page in place, cleaning line ends, instead of scrolling. |
-d, --silent | When an unrecognised key is pressed, show a short hint instead of ringing the terminal bell. |
-l, --logical | Do not pause when a line contains a form-feed character. |
-f, --no-pause | Count logical lines rather than screen lines when deciding where to pause. |
Exit status #
| Code | Meaning |
|---|---|
0 | The file was displayed. |
1 | A file could not be opened. |
nl
Peios / Using Peios / Peiosutils / Viewing and joining text
nl prints a file with its lines numbered. cat -n also numbers lines; nl is the command for when you need control over the numbering — which lines count, how the number looks, where it restarts.
nl [options] [file...]
$ nl chapter.txt
1 Once the system is installed,
2 the first boot brings up
3 the configuration service.
Which lines get a number #
By default nl numbers only the non-empty lines. -b sets the rule:
-b STYLE | Numbers… |
|---|---|
-b a | every line. |
-b t | only non-empty lines. This is the default. |
-b n | no lines. |
-b pBRE | only lines that match the basic regular expression BRE. |
| Option | Effect |
|---|---|
-b, --body-numbering=STYLE | The numbering rule, as above. |
-l, --join-blank-lines=N | Count a run of N blank lines as one numbered line. |
How the number looks #
| Option | Effect |
|---|---|
-n, --number-format=FORMAT | ln = left-justified; rn = right-justified; rz = right-justified with leading zeros. |
-w, --number-width=N | Use N columns for the number. |
-s, --number-separator=STRING | Put STRING between the number and the line text. |
-v, --starting-line-number=N | Start counting from N. |
-i, --line-increment=N | Increase the number by N at each counted line. |
Logical pages #
nl can treat one file as a sequence of logical pages, each with a header, a body, and a footer, and number the three parts by different rules. Pages are separated by special delimiter lines in the input.
| Option | Effect |
|---|---|
-h, --header-numbering=STYLE | Numbering style for header sections. |
-f, --footer-numbering=STYLE | Numbering style for footer sections. |
-d, --section-delimiter=CC | The characters that mark a section boundary in the input. |
-p, --no-renumber | Do not reset the line number at the start of each logical page. |
The styles for -h and -f are the same a / t / n / pBRE set as -b. For an ordinary file with no delimiter lines, the whole file is one body and only -b matters.
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A file could not be read, or an option value was invalid. |
cut
Peios / Using Peios / Peiosutils / Viewing and joining text
cut extracts columns from text. For each line of input it prints only the parts you select, dropping the rest.
cut option... [file...]
$ cut -d , -f 1,3 people.csv # the 1st and 3rd comma-separated field
$ cut -c 1-8 log.txt # the first 8 characters of each line
Every run of cut needs two decisions: what counts as a column, and which columns to keep.
What counts as a column #
Choose exactly one mode:
| Option | A column is… |
|---|---|
-b, --bytes | a single byte. |
-c, --characters | a single character. |
-f, --fields | a field — a stretch of text between delimiters. |
-b and -c cut by position. -f cuts by field, which is what you want for tabular data — a CSV, a table, the columns of a report.
Which columns to keep #
The mode option takes a sequence: numbers and inclusive ranges, separated by commas.
| Sequence | Selects |
|---|---|
3 | column 3 only. |
2,5,9 | columns 2, 5, and 9. |
5-7 | columns 5 through 7. |
3- | column 3 to the end of the line. |
-4 | the start of the line through column 4. |
1,4-6,9 | any mixture of the above. |
| Option | Effect |
|---|---|
--complement | Invert the selection — keep every column except those named. |
Field mode: the delimiter #
In field mode, cut needs to know what separates the fields.
| Option | Effect |
|---|---|
-d, --delimiter=CHAR | The character that separates fields. The default is a tab. |
-w | Separate fields on runs of whitespace (spaces and tabs) instead of a single character. Cannot be combined with -d. |
-s, --only-delimited | Print only lines that actually contain the delimiter; drop lines that have no fields to cut. |
--output-delimiter=STRING | Put STRING between the kept fields in the output, instead of repeating the input delimiter — handy for converting one delimited format to another. |
Other options #
| Option | Effect |
|---|---|
-z, --zero-terminated | Treat the NUL character as the line delimiter, so a "line" may itself contain newlines. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A mode was missing or given twice, an option was misused, or a file could not be read. |
paste
Peios / Using Peios / Peiosutils / Viewing and joining text
paste joins files side by side. It takes the first line of each file and prints them on one line, then the second line of each, and so on — merging files into columns.
paste [options] [file...]
$ paste names.txt ages.txt
Ada 36
Grace 41
Linus 29
By default the merged pieces are separated by a tab. paste is the column-wise counterpart of cat, which joins files end to end.
Pasting one file at a time #
| Option | Effect |
|---|---|
-s, --serial | Paste each file's lines onto a single line, one file at a time, instead of merging files in parallel. A file's lines become a row. |
$ paste -s -d , names.txt
Ada,Grace,Linus
Choosing the separator #
| Option | Effect |
|---|---|
-d, --delimiters=LIST | Use the characters in LIST as separators instead of a tab. When LIST has more than one character, paste cycles through them. |
-z, --zero-terminated | Treat the NUL character as the line delimiter instead of newline. |
paste and join #
paste merges by position — line 1 with line 1, line 2 with line 2, blind to content. When you want to merge by a matching value — pairing rows that share a key — that is join.
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A file could not be read, or the delimiter list was malformed. |
join
Peios / Using Peios / Peiosutils / Viewing and joining text
join merges two files on a shared field. For every pair of lines — one from each file — that have the same value in their join field, it prints a combined line. It is the command-line form of a database join.
join [options] file1 file2
$ join employees.txt salaries.txt
If employees.txt has 101 Ada and salaries.txt has 101 80000, join pairs them on the shared 101 and prints 101 Ada 80000.
The input must be sorted #
join works by stepping through both files together, so both files must be sorted on the join field. If they are not, join will miss matches. Sort them first with sort, on the same field join will use.
join checks the ordering as it goes and reports a file that is out of order; --nocheck-order turns that check off, and --check-order forces it on.
Choosing the join field #
By default join joins on the first field of each file, and fields are separated by whitespace.
| Option | Effect |
|---|---|
-1 FIELD | Join on FIELD of file 1. |
-2 FIELD | Join on FIELD of file 2. |
-j FIELD | Join on FIELD of both files — shorthand for -1 FIELD -2 FIELD. |
-t CHAR | Use CHAR as the field separator for input and output. |
Unmatched lines #
By default join prints only the lines that paired. A line with no match on the other side is dropped. These options bring unmatched lines back:
| Option | Effect |
|---|---|
-a FILENUM | Also print unpaired lines from file FILENUM (1 or 2). |
-v FILENUM | Print only the unpaired lines from file FILENUM, and suppress the joined output. |
-e EMPTY | Fill in any missing field with the string EMPTY. |
Shaping the output #
| Option | Effect |
|---|---|
-o FORMAT | Build each output line from the field list FORMAT, rather than the default layout. |
-i, --ignore-case | Ignore letter case when comparing join fields. |
--header | Treat the first line of each file as column headers — print them, paired, without trying to match them. |
-z, --zero-terminated | Treat the NUL character as the line delimiter. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A file could not be read, was not sorted, or an option was invalid. |
comm
Peios / Using Peios / Peiosutils / Viewing and joining text
comm compares two sorted files and tells you which lines are unique to each and which are common to both.
comm [options] file1 file2
By default it prints three columns:
$ comm list-a.txt list-b.txt
apple
banana
cherry
date
- Column 1 — lines only in
file1. - Column 2 — lines only in
file2. - Column 3 — lines in both.
Each column is indented past the one before, so a line's column position tells you where it was found.
The input must be sorted #
comm steps through both files together, so both must be sorted. On unsorted input the comparison is meaningless. Sort the files first with sort.
comm checks the ordering and reports a file that is out of order. --check-order forces the check; --nocheck-order disables it.
Showing only the columns you want #
Each column can be switched off. The remaining columns close up to fill the gap.
| Option | Effect |
|---|---|
-1 | Suppress column 1 — hide lines unique to file1. |
-2 | Suppress column 2 — hide lines unique to file2. |
-3 | Suppress column 3 — hide lines common to both. |
These combine to answer specific questions. comm -12 shows only the common lines — the intersection of the two files. comm -23 shows lines in file1 that are not in file2 — the difference.
Other options #
| Option | Effect |
|---|---|
--output-delimiter=STR | Separate the columns with STR instead of spaces. |
--total | Print a final summary line with the count in each column. |
-z, --zero-terminated | Treat the NUL character as the line delimiter. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A file could not be read, or was not sorted. |
split
Peios / Using Peios / Peiosutils / Viewing and joining text
split breaks one file into several smaller files of roughly equal size.
split [options] [input [prefix]]
$ split -l 1000 big.log
$ ls
xaa xab xac xad
By default split writes 1000 lines per piece, names the pieces xaa, xab, xac, … — a prefix of x followed by a two-letter suffix — and reads its input from a named file or from standard input.
To put the file back together, concatenate the pieces in order: cat xaa xab xac … > original.
Each piece is a newly created file, and a new file needs a security descriptor; each piece inherits one from the directory it is created in.
How big each piece is #
Choose one of these to set the piece size:
| Option | Each piece holds… |
|---|---|
-l, --lines=N | N lines. This is the default behaviour, with N = 1000. |
-b, --bytes=SIZE | SIZE bytes. |
-C, --line-bytes=SIZE | up to SIZE bytes, but only whole lines — never a line split across two pieces. |
-n, --number=CHUNKS | the input divided into a fixed number of pieces (see below). |
A SIZE accepts a unit suffix: K, M, G, … as powers of 1024, or KB, MB, … as powers of 1000.
Fixed number of chunks (-n) #
-n divides the input into a set number of pieces rather than fixing each piece's size. CHUNKS may be:
| Form | Effect |
|---|---|
N | Split into N pieces by size. |
l/N | Split into N pieces without splitting any line across pieces. |
r/N | Like l/N, but distribute lines round-robin across the pieces. |
K/N | Write only the Kth of N pieces, to standard output. |
Naming the pieces #
| Option | Effect |
|---|---|
prefix (operand) | The leading part of each output name. Default x. |
-a, --suffix-length=N | Use N characters for the suffix. Default 2. |
-d, --numeric-suffixes[=START] | Use numeric suffixes (00, 01, …) instead of letters, optionally from START. |
-x, --hex-suffixes[=START] | Use hexadecimal suffixes. |
--additional-suffix=SUFFIX | Append a fixed SUFFIX to every output name — for giving the pieces an extension. |
Each piece is created as a new file. If a name already exists, split opens it and checks, on the open handle, that it is not the input before truncating it; an output that turns out to be the input is refused with "would overwrite input".
Other options #
| Option | Effect |
|---|---|
-e, --elide-empty-files | With -n, do not write out pieces that would be empty. |
-t, --separator=SEP | Use SEP as the line separator instead of newline. |
--verbose | Print a line as each output file is opened. |
Exit status #
| Code | Meaning |
|---|---|
0 | The file was split. |
1 | A failure — the input could not be read, or the suffixes were exhausted. |
csplit
Peios / Using Peios / Peiosutils / Viewing and joining text
csplit — "context split" — breaks a file into pieces at points you choose: a line number, or a line that matches a pattern. Where split cuts a file into equal sizes, csplit cuts it at meaningful boundaries.
csplit [options] file pattern...
$ csplit report.txt '/^Chapter/' '{*}'
That cuts report.txt at every line beginning with Chapter, including an initial piece before the first match. That initial piece is empty if the first line matches. The pieces are written to xx00, xx01, xx02, … and csplit prints the byte size of each.
Each piece is a newly created file, and a new file needs a security descriptor; each piece inherits one from the directory it is created in.
Patterns #
Each pattern argument marks a cut point. csplit copies everything up to that point into the next output file, then continues.
| Pattern | Cut where… |
|---|---|
N (a number) | line N is reached — the piece is lines up to N-1. |
/REGEX/ | a line matches REGEX; that line starts the next piece. |
%REGEX% | a line matches REGEX — but skip the text up to it instead of writing it to a piece. |
/REGEX/+N or /REGEX/-N | an offset of N lines from the matching line. |
{N} | repeat the previous pattern N more times. |
{*} | repeat the previous pattern as many times as the file allows. |
A pattern that finds no match is an error, and by default csplit removes the pieces it had already written. -k keeps them.
Naming the pieces #
| Option | Effect |
|---|---|
-f, --prefix=PREFIX | Use PREFIX instead of xx at the start of each output name. |
-n, --digits=N | Use N digits in the numeric part of the name instead of 2. |
-b, --suffix-format=FORMAT | Use a custom printf-style FORMAT for the numeric suffix. |
Other options #
| Option | Effect |
|---|---|
-k, --keep-files | Do not delete the output files if csplit fails partway. |
-z, --elide-empty-files | Do not write out pieces that would be empty. |
--suppress-matched | Drop the lines that the patterns matched, rather than keeping them in a piece. |
-s, --quiet | Do not print the byte size of each piece. |
Exit status #
| Code | Meaning |
|---|---|
0 | The file was split. |
1 | A pattern did not match, a line number was out of range, or the input could not be read. |
od
Peios / Using Peios / Peiosutils / Viewing and joining text
od — short for "octal dump" — prints the raw bytes of a file. Where cat shows you a file as text, od shows you the actual bytes underneath: each one rendered as a number, a character, or a floating-point value, in whatever base you ask for. It is the tool for looking at binary files, checking exactly which bytes a text file contains, or finding a stray control character that isn't visible on screen.
od [options] [file...]
$ od hello.txt
0000000 062510 066154 020157 067527 066162 020144 000012
0000015
With no file — or with the name - — od reads standard input, so it can sit at the end of a pipeline. Given several files, it reads them one after another as a single continuous stream, and the byte offsets run straight through from one file into the next.
How the output is laid out #
Every line of output has two parts:
- An offset on the left: the position, counted in bytes from the start of the input, of the first byte on that line. By default it is printed in octal (see
-Ato change the base). - One or more format columns on the right: the bytes of that line rendered in the format(s) you chose.
With no format option, od prints the input as octal, two bytes at a time — the traditional default. The very last line of output is a lone offset marking the total length of the input.
If you ask for more than one format at once, each format is printed on its own line, stacked under a single shared offset. Only the first line of the group carries the offset; the rest are indented to line up beneath it.
Choosing what the bytes look like #
There are two ways to say how bytes should be rendered: a set of single-letter named shortcuts for the common cases, and the general -t / --format option for full control. You can give several at once, and they are applied in the order they appear on the command line.
Named format shortcuts #
Each of these selects one output format. Combine them freely (od -cx prints characters and hex together).
| Option | Renders each unit as |
|---|---|
-a | Named characters, ignoring the high-order bit (control characters shown by name, e.g. nul, sp, del). |
-b | Octal, one byte at a time. |
-c | Printable ASCII / UTF-8 characters, with backslash escapes (\n, \t, …) for the rest. |
-d | Unsigned decimal, 2-byte units. |
-D | Unsigned decimal, 4-byte units. |
-o | Octal, 2-byte units. |
-O | Octal, 4-byte units. |
-s | Signed decimal, 2-byte units. |
-i | Signed decimal, 4-byte units. |
-l | Signed decimal, 8-byte units. |
-I, -L | Signed decimal, 8-byte units. |
-x, -h | Hexadecimal, 2-byte units. |
-X, -H | Hexadecimal, 4-byte units. |
-f | Floating point, single precision (32-bit). |
-e, -F | Floating point, double precision (64-bit). |
Type specifications (-t, --format) #
-t takes a type specification and gives you every combination the named shortcuts cover and more. Repeat -t (or list several specs in one argument) to print multiple formats.
A type specification is a type letter, an optional size, and an optional z suffix:
| Type letter | Meaning |
|---|---|
a | Printable 7-bit ASCII, named (like -a). |
c | UTF-8 characters, with octal escapes for undefined bytes (like -c). |
d | Signed decimal. |
u | Unsigned decimal. |
o | Octal. |
x | Hexadecimal. |
f | Floating point. |
For the numeric types (d, u, o, x, f) a size says how many bytes make up one unit. It can be a number, or a letter:
| Size | Applies to | Bytes per unit |
|---|---|---|
1 / C | integer types | 1 |
2 / S | integer types | 2 |
4 / I | integer types | 4 |
8 / L | integer types | 8 |
4 / F | floating point | 4 (single precision) |
8 / D | floating point | 8 (double precision) |
16 / L | floating point | 16 (extended / long double) |
2 / H | floating point | 2 (IEEE half precision) |
2 / B | floating point | 2 (bfloat16) |
If you leave the size off, integer types default to 4 bytes and floating point defaults to 8 bytes. For the character types a and c, a size is not allowed.
Add a z at the end of a spec to append an ASCII dump — the bytes of that line shown as text, . for anything non-printable — to the right of the numbers. For example, od -t x1z gives the familiar hex-plus-text layout:
$ od -A x -t x1z file.bin
000000 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 0a >Hello, world!.<
Examples: od -t d2 is signed decimal in 2-byte units; od -t x1 is hex byte-by-byte; od -t fD is double-precision floats; od -t c is characters. od -t x1 -t c prints hex and characters as two stacked lines.
Byte order (--endian) #
For multi-byte numeric formats, --endian sets the byte order used to assemble each unit. Without it, od uses the machine's native byte order. Use --endian=big or --endian=little to force one regardless of the host.
The offset column #
| Option | Effect |
|---|---|
-A RADIX, --address-radix=RADIX | Print offsets in the given base. RADIX is one of o (octal, the default), d (decimal), x (hexadecimal), or n (none — omit the offset column entirely). |
Only the first character of the value is examined, so od -A n and od -Anone mean the same thing.
Which bytes to read #
By default od dumps the whole input. Two options narrow that to a window:
| Option | Effect |
|---|---|
-j BYTES, --skip-bytes=BYTES | Skip BYTES bytes of input before starting to dump. When several files are given, the skip runs across the concatenated stream. |
-N BYTES, --read-bytes=BYTES | Dump at most BYTES bytes, then stop. |
In both, BYTES is decimal by default, octal if it starts with 0, or hexadecimal if it starts with 0x. A suffix scales it: b = ×512, KB/K = ×1000 / ×1024, MB/M = ×1000² / ×1024², GB/G = ×1000³ / ×1024³.
Collapsing repeated lines #
When several output lines in a row would be identical, od prints the first one, then a single line containing just * to stand for the run, and resumes at the next line that differs. This keeps a dump of a large block of identical bytes (a file full of zeros, say) short.
| Option | Effect |
|---|---|
-v, --output-duplicates | Turn the collapsing off — print every line, including repeats, with no * markers. |
Output width #
| Option | Effect |
|---|---|
-w[BYTES], --width[=BYTES] | Print BYTES input bytes per output line. The default is 16; giving -w with no value uses 32. |
The width must be a whole multiple of the size of the largest format unit in play; if it isn't, od warns and rounds it to a usable value.
Finding printable strings #
| Option | Effect |
|---|---|
-S[BYTES], --strings[=BYTES] | Instead of dumping bytes, scan the input and print only runs of at least BYTES printable characters, one per line, each preceded by its offset. BYTES defaults to 3 when omitted. |
This turns od into a quick way to pull the human-readable text out of a binary file. The offset in front of each string honours -A (and is dropped entirely under -A n). -j and -N still apply, so you can restrict the scan to a window.
The traditional offset form #
od also accepts the historical calling convention, where an offset (and, in --traditional mode, a label) is given as a bare operand rather than an option:
od [named-format-options] [file] [[+]offset[.][b]]
od --traditional [options] [file] [[+]offset[.][b] [[+]label[.][b]]]
The offset says where in the input to begin, exactly like -j. It is read as octal by default, hexadecimal if prefixed with 0x, or decimal if it carries a trailing .; a trailing b multiplies it by 512. The optional leading + is allowed everywhere and is required when there is no filename (so od +20 reads standard input starting at octal offset 20).
The label (only in --traditional mode) is a second such number: the dump still begins at offset, but the offset column is displayed as if it started at label — useful for making a partial dump's offsets match the original file.
od only reads an operand as an offset in the plain form when none of -A, -j, -N, -t, -v, or -w are present. So if a real filename happens to look like a number, add a harmless option such as -j0 to force it to be treated as a filename.
A note on reading files #
od has no access rules of its own — it simply opens a file and reads its bytes. That read is subject to the same check as every read on Peios: whether you may open the file for reading is decided by the file's security descriptor (see Security descriptors). od never reveals bytes you could not already read another way; it only presents the bytes you can read in a different form.
Exit status #
| Code | Meaning |
|---|---|
0 | The input was dumped successfully. |
1 | A file could not be read, or an option, format, or offset was invalid. |
Transforming text
Peios / Using Peios / Peiosutils / Transforming text
This topic covers the commands that reshape text: putting lines in order, dropping duplicates, swapping characters, reflowing paragraphs to a width, counting what is there. Its companion, Viewing and joining text, covers printing and combining files; this topic is about changing the text itself.
The commands #
Ordering lines
| Command | Purpose |
|---|---|
sort | Sort lines — alphabetically, numerically, and many other ways. |
shuf | The opposite of sorting: put lines into a random order. |
tsort | Topological sort — order items so that each comes after the things it depends on. |
Filtering lines
| Command | Purpose |
|---|---|
uniq | Collapse or report adjacent repeated lines. |
Substituting characters
| Command | Purpose |
|---|---|
tr | Translate, squeeze, or delete individual characters. |
Whitespace
Reflowing and paginating
| Command | Purpose |
|---|---|
fmt | Reflow paragraphs to a target width. |
fold | Hard-wrap long lines at a fixed width. |
pr | Paginate and columnate text for printing. |
Indexing and counting
| Command | Purpose |
|---|---|
ptx | Produce a permuted index of the words in a file. |
wc | Count the lines, words, and bytes in a file. |
Grouping duplicates with sort and uniq #
A theme worth knowing before you start: uniq only collapses adjacent duplicate lines, so duplicates scattered through a file are not caught unless the file is sorted first. sort and uniq are almost always used together — and sort -u does both jobs in one step.
Where to start #
sort is the workhorse of the topic and the one with the most depth — start there.
sort
Peios / Using Peios / Peiosutils / Transforming text
sort reads lines and writes them out in order.
sort [options] [file...]
$ sort names.txt
With no file, sort reads standard input. Given several files, it sorts all of their lines together as one stream. By default it compares lines as plain text, character by character.
How lines are compared #
The default text comparison is often not the order you want — 10 sorts before 9, and case matters. These options change the comparison:
| Option | Compares lines as… |
|---|---|
-n, --numeric-sort | numbers. 9 sorts before 10. |
-g, --general-numeric-sort | numbers, including scientific notation. Slower than -n; use it only when -n is not enough. |
-h, --human-numeric-sort | human-readable sizes — 2K before 1M before 3G. |
-M, --month-sort | month names — JAN before FEB. |
-V, --version-sort | version numbers — 1.2.10 after 1.2.9. |
-R, --random-sort | a random order (a shuffle that groups equal lines together). |
And these adjust what is compared:
| Option | Effect |
|---|---|
-f, --ignore-case | Treat lower and upper case as the same. |
-d, --dictionary-order | Consider only letters, digits, and blanks. |
-i, --ignore-nonprinting | Ignore non-printing characters. |
-b, --ignore-leading-blanks | Ignore blanks at the start of a line (or field). |
Sort order options #
| Option | Effect |
|---|---|
-r, --reverse | Reverse the result. |
-u, --unique | Output only the first line of each run of equal lines — sort and de-duplicate in one step. |
-s, --stable | Keep equal lines in their original relative order. |
Sorting by a field #
By default sort compares whole lines. -k sorts on a key — a chosen part of each line — which is what you need for tabular data.
A line is split into fields (by default at whitespace). A key is written FIELD[.CHAR][OPTIONS][,FIELD[.CHAR]] — a start field, an optional start character within it, and an optional end. The single-letter comparison options above can be appended to a key to apply only to it.
$ sort -k 2 -n scores.txt # sort on the 2nd field, numerically
$ sort -t , -k 3 people.csv # sort a CSV on the 3rd field
| Option | Effect |
|---|---|
-k, --key=KEYDEF | Sort on the key KEYDEF. May be given more than once; keys are tried in order. |
-t, --field-separator=SEP | Use SEP to split fields, instead of the whitespace default. |
Checking and merging #
| Option | Effect |
|---|---|
-c, --check | Do not sort — just check whether the input is already sorted, and report the first line that is out of order. |
-C, --check=silent | Check, but report only through the exit status. |
-m, --merge | Treat the inputs as already-sorted files and merge them, which is faster than a full sort. |
Output and resources #
| Option | Effect |
|---|---|
-o, --output=FILE | Write to FILE instead of standard output. FILE may be one of the inputs. |
-z, --zero-terminated | Treat the NUL character as the line delimiter. |
--parallel=N | Use N threads. |
-S, --buffer-size=SIZE | Set the size of the in-memory sort buffer. |
-T, --temporary-directory=DIR | Put temporary files in DIR rather than the default location. |
--files0-from=F | Take the list of input files from F, NUL-separated. |
--debug | Underline the part of each line that was actually used as the sort key — invaluable when a -k key is not behaving. |
Exit status #
| Code | Meaning |
|---|---|
0 | The lines were sorted — or, with -c/-C, the input was already sorted. |
1 | With -c/-C, the input was not sorted. |
2 | An error — a file could not be read, or an option was invalid. |
uniq
Peios / Using Peios / Peiosutils / Transforming text
uniq deals with repeated lines: it can collapse a run of identical lines into one, count them, or print only the ones that repeated.
uniq [options] [input [output]]
$ uniq events.txt
input and output may be given as operands; with neither, uniq reads standard input and writes standard output.
uniq only sees adjacent duplicates #
This is the one thing to know about uniq: it only compares each line with the one before it. It collapses adjacent repeats. Duplicate lines scattered through a file, with other lines between them, are not caught.
So uniq is almost always paired with sort, which brings every copy of a line together first:
$ sort events.txt | uniq
If collapsing duplicates is all you need, sort -u does both jobs in one command. Use uniq itself when you want what sort -u cannot do — counting repeats, or printing only the duplicated lines.
What to output #
By default uniq prints every line, with each run of adjacent duplicates reduced to a single copy. These options change that:
| Option | Effect |
|---|---|
-c, --count | Prefix each line with the number of times it occurred. |
-d, --repeated | Print only lines that were repeated — one copy of each. |
-D | Print all copies of every repeated line, not just one. |
-u, --unique | Print only lines that were not repeated. |
--group[=WHICH] | Print every line, with runs separated by a blank line. WHICH is separate, prepend, append, or both. |
What counts as "the same" #
| Option | Effect |
|---|---|
-i, --ignore-case | Treat lines differing only in letter case as the same. |
-f, --skip-fields=N | Ignore the first N fields when comparing. |
-s, --skip-chars=N | Ignore the first N characters when comparing. |
-w, --check-chars=N | Compare at most N characters of each line. |
Other options #
| Option | Effect |
|---|---|
-z, --zero-terminated | Treat the NUL character as the line delimiter. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A file could not be read or written. |
tr
Peios / Using Peios / Peiosutils / Transforming text
tr — "translate" — works on text one character at a time. It can replace characters with other characters, remove characters, or collapse runs of a character into one.
tr [options] set1 [set2]
tr reads standard input and writes standard output. It has no file arguments; a pipeline or input redirection can supply the text.
$ tr 'a-z' 'A-Z' < notes.txt # lower-case to upper-case
$ tr -d ' ' < spaced.txt # delete every space
$ tr -s ' ' < padded.txt # collapse runs of spaces to one
The three jobs #
What tr does depends on the options and how many sets you give it.
- Translate — give two sets. Each character in
set1is replaced by the character at the same position inset2.tr 'abc' 'xyz'turns everyaintox,bintoy,cintoz. - Delete (
-d) — give one set. Every character inset1is removed. - Squeeze (
-s) — each run of a repeated character that appears in the relevant set is collapsed to a single occurrence.
-d and -s can be combined: delete one set of characters, then squeeze another.
Writing a set #
A set is a string of characters, with a few shorthands:
| Notation | Means |
|---|---|
a-z | A range — every character from a to z. |
[:alpha:], [:digit:], [:space:], … | A named character class. |
[=c=] | An equivalence class — every character that sorts the same as c. |
[c*n] | The character c repeated n times — for padding a set to a length. |
\n, \t, \\, \NNN | Escapes — newline, tab, backslash, an octal byte. |
Options #
| Option | Effect |
|---|---|
-d, --delete | Delete the characters in set1 instead of translating. |
-s, --squeeze-repeats | Collapse each run of a repeated character listed in the last set into one. |
-c, -C, --complement | Operate on the complement of set1 — every character it does not list. |
-t, --truncate-set1 | Before translating, shorten set1 to the length of set2, rather than reusing set2's last character to cover the surplus. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A usage error — the wrong number of sets, or a malformed set. |
expand
Peios / Using Peios / Peiosutils / Transforming text
expand converts tab characters into spaces, so that text lines up the same way no matter what tab width something is viewed with.
expand [options] [file...]
$ expand source.txt > source-spaces.txt
A tab is not a fixed number of spaces — it jumps to the next tab stop, and where those stops are is a display setting. expand removes the ambiguity: it replaces each tab with exactly the number of spaces needed to reach the same column, baking the layout in. With no file, it reads standard input.
unexpand is the reverse operation.
Options #
| Option | Effect |
|---|---|
-t, --tabs=N | Set tab stops every N columns instead of the default 8. |
-t, --tabs=LIST | Given a comma-separated list, place tab stops at those explicit column positions. |
-i, --initial | Convert only the tabs that come before the first non-blank character on each line — leave tabs within the text alone. Useful for re-indenting code without disturbing aligned tabs inside it. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A file could not be read, or a tab specification was invalid. |
unexpand
Peios / Using Peios / Peiosutils / Transforming text
unexpand is the reverse of expand: it converts runs of spaces back into tab characters.
unexpand [options] [file...]
$ unexpand spaced.txt > tabbed.txt
Where a run of spaces reaches a tab stop, unexpand replaces it with a tab. By default it only converts the leading spaces on each line — the indentation — and leaves spacing within the text alone. With no file, it reads standard input.
Options #
| Option | Effect |
|---|---|
-t, --tabs=N | Set tab stops every N columns instead of the default 8. A comma-separated list places stops at explicit column positions. Using -t also enables -a. |
-a, --all | Convert all runs of spaces that reach a tab stop, not only the leading indentation. |
--first-only | Convert only the leading run of blanks on each line. Overrides -a. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A file could not be read, or a tab specification was invalid. |
fmt
Peios / Using Peios / Peiosutils / Transforming text
fmt reflows paragraphs: it rejoins the lines of a paragraph and re-breaks them so each comes out close to a target width. Words move freely between lines to make the result fit.
fmt [options] [file...]
$ fmt -w 72 draft.txt
fmt works on paragraphs — runs of non-blank lines separated by blank lines — and treats word boundaries as places it may break. The result reads naturally. This is what distinguishes it from fold, which simply cuts every line at a fixed column without regard to words.
With no file, fmt reads standard input.
Setting the width #
| Option | Effect |
|---|---|
-w, --width=WIDTH | Fill lines up to at most WIDTH columns. The default is 75. |
-g, --goal=WIDTH | Aim for WIDTH columns — the width fmt targets, while -w is the hard maximum. Defaults to about 93% of the maximum. |
Controlling the reflow #
| Option | Effect |
|---|---|
-s, --split-only | Only split lines that are too long; never join short lines together. |
-u, --uniform-spacing | Put exactly one space between words and two between sentences. |
-c, --crown-margin | Preserve the indentation of a paragraph's first two lines; indent the rest to match the second. |
-t, --tagged-paragraph | Like -c, but the first line must be indented differently from the second, or the two are treated as separate paragraphs. |
-q, --quick | Break lines faster, accepting a more ragged right edge. |
Selecting which lines to format #
| Option | Effect |
|---|---|
-p, --prefix=PREFIX | Reformat only lines that begin with PREFIX; reattach PREFIX afterwards. Useful for reflowing comment blocks in source code. |
-m, --preserve-headers | Detect and preserve mail-style header lines rather than reflowing them. |
--tab-width=N | Treat a tab as N columns when measuring line length. Tabs are kept in the output; this affects measurement only. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A file could not be read, or an option value was invalid. |
fold
Peios / Using Peios / Peiosutils / Transforming text
fold breaks long lines so that none exceeds a fixed width. Every line longer than the limit is cut into pieces.
fold [options] [file...]
$ fold -w 80 wide.txt
By default fold wraps at 80 columns, and it cuts at exactly that column — even in the middle of a word. That hard cut is the difference between fold and fmt: fmt reflows paragraphs and breaks between words; fold just enforces a width, mechanically. Use fold when you need a guarantee that no line is wider than N; use fmt when you want the result to read well.
With no file, fold reads standard input.
Options #
| Option | Effect |
|---|---|
-w, --width=WIDTH | Wrap at WIDTH columns instead of 80. |
-s, --spaces | Break at a space within the width where one exists, rather than mid-word — a gentler wrap. |
-b, --bytes | Count bytes rather than display columns. Control characters such as tab and newline then count as ordinary bytes. |
-c, --characters | Count character positions rather than display columns. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A file could not be read, or the width was invalid. |
pr
Peios / Using Peios / Peiosutils / Transforming text
pr prepares text for printing on paper. It breaks a file into pages, puts a header and a trailer on each, and can arrange the text into columns.
pr [options] [file...]
$ pr report.txt
Run plainly, pr divides the input into 66-line pages, each beginning with a five-line header — the date, the file name, and a page number — and ending with a trailing margin. With no file, it reads standard input.
Columns #
pr can lay text out in several columns per page.
| Option | Effect |
|---|---|
-COLUMN | Produce COLUMN columns per page (e.g. -3 for three). Text fills down the first column, then the next. |
-a, --across | Fill the columns across — line 1 in column 1, line 2 in column 2, and so on — rather than down. |
-m, --merge | Print several files side by side, one per column. |
-w, --width=WIDTH | Set the page width, for multi-column output. |
The header and the page #
| Option | Effect |
|---|---|
-h, --header=TEXT | Use TEXT in the header instead of the file name. |
-t, --omit-header | Print neither the header nor the trailer. |
-T, --omit-pagination | Drop the header, the trailer, and all pagination — just the text. |
-l, --length=LINES | Set the page length to LINES instead of 66. |
-o, --indent=N | Indent every line by N spaces. |
-D, --date-format=FORMAT | Format the header's date with FORMAT. |
-F, -f, --form-feed | Separate pages with a form-feed character rather than blank lines. |
Selecting and numbering #
| Option | Effect |
|---|---|
--pages=FIRST[:LAST] | Print only pages FIRST through LAST. |
-n, --number-lines[=SEP[WIDTH]] | Number every line. |
-N, --first-line-number=N | Begin line numbering at N. |
-d, --double-space | Double-space the output. |
Other options #
| Option | Effect |
|---|---|
-s, --separator-char[=CHAR] | Separate columns with a single CHAR (default tab) rather than padding with spaces. |
-S, --sep-string[=STRING] | Separate columns with STRING. |
-e, --expand-tabs[=CHAR[WIDTH]] | Expand input tabs to spaces. |
-J, --join-lines | Merge full lines, turning off width truncation. |
-r, --no-file-warnings | Do not warn when a file cannot be opened. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A file could not be read, or an option was invalid. |
ptx
Peios / Using Peios / Peiosutils / Transforming text
ptx produces a permuted index — a concordance. For each significant word in the input, it prints a line showing that word together with the text around it, so the same passage appears once per keyword it contains.
ptx [options] [input...]
$ ptx manual.txt
A permuted index is the kind of index found at the back of a reference book: every keyword, in alphabetical order, each shown in its context. ptx builds one from plain text. With no file, it reads standard input.
Choosing the keywords #
| Option | Effect |
|---|---|
-W, --word-regexp=REGEXP | Treat anything matching REGEXP as a keyword. |
-b, --break-file=FILE | Take the set of word-break characters from FILE. |
-i, --ignore-file=FILE | Read a list of words to exclude from FILE. |
-o, --only-file=FILE | Index only the words listed in FILE. |
-f, --ignore-case | Fold case together when sorting. |
Output format #
| Option | Effect |
|---|---|
-w, --width=N | Set the output width, in columns. |
-g, --gap-size=N | Set the gap, in columns, between the output fields. |
-A, --auto-reference | Generate a reference (file name and line number) for each entry automatically. |
-r, --references | Treat the first field of each input line as a reference for that line. |
-R, --right-side-refs | Put references on the right, and exclude them from the width count. |
-F, --flag-truncation=STRING | Use STRING to mark where a line was truncated. |
-O, --format=roff | Emit the index as roff typesetting directives. |
-T, --format=tex | Emit the index as TeX typesetting directives. |
-M, --macro-name=NAME | Use NAME as the macro name in roff/TeX output. |
-G, --traditional | Behave like the older System V ptx, without the extended features. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A file could not be read, or an option value was invalid. |
tsort
Peios / Using Peios / Peiosutils / Transforming text
tsort performs a topological sort. Given a set of "this must come before that" rules, it produces an order in which everything appears after the things it depends on.
tsort [file]
$ tsort dependencies.txt
With no file, tsort reads standard input.
The input #
tsort reads its input as a flat sequence of whitespace-separated tokens, taken in pairs. Each pair A B means "A must come before B" — an edge in a dependency graph.
compiler linker
linker installer
compiler installer
That input says the linker depends on the compiler, the installer on the linker, and the installer on the compiler. tsort reads the pairs and prints the items in a valid order:
compiler
linker
installer
A token paired with itself (A A) simply introduces A with no dependency — a way to list an item that nothing else mentions.
Cycles #
A topological order only exists if the dependencies form no cycle. If A must come before B and B must come before A, there is no valid order. tsort detects this, reports the loop it found, and exits with a failure status — it still prints an order, but the cycle means that order cannot satisfy every rule.
Where it is used #
tsort answers "what order do I do these in?" — building components in dependency order, scheduling tasks, sequencing anything where some steps must precede others. It is the dependency-aware counterpart to sort, which orders by value rather than by dependency.
Exit status #
| Code | Meaning |
|---|---|
0 | A valid order was produced. |
1 | The input contained a cycle, had an odd number of tokens, or could not be read. |
wc
Peios / Using Peios / Peiosutils / Transforming text
wc — "word count" — counts what is in a file: its lines, its words, and its bytes.
wc [options] [file...]
$ wc report.txt
214 1832 12044 report.txt
By default wc prints three numbers per file — lines, words, bytes — followed by the file name. Given several files, it adds a total line. With no file, it reads standard input.
Choosing what to count #
Pass any of these and wc prints only the counts you ask for, in the fixed order lines, words, characters/bytes, longest-line:
| Option | Counts |
|---|---|
-l, --lines | The number of lines. |
-w, --words | The number of words — runs of non-whitespace. |
-c, --bytes | The number of bytes. |
-m, --chars | The number of characters. This differs from -c for multi-byte text, where one character is several bytes. |
-L, --max-line-length | The length of the longest line. |
$ wc -l report.txt # just the line count
214 report.txt
Other options #
| Option | Effect |
|---|---|
--files0-from=F | Take the list of files to count from F, as NUL-terminated names. - reads the list from standard input. |
--total=WHEN | Control the total line: auto (the default — shown for more than one file), always, only (just the total, no per-file lines), or never. |
Exit status #
| Code | Meaning |
|---|---|
0 | Every file was counted. |
1 | A file could not be read. |
shuf
Peios / Using Peios / Peiosutils / Transforming text
shuf shuffles: it outputs its input lines in a random order. Every possible ordering is equally likely.
shuf [options] [file]
shuf -e [options] arg...
shuf -i lo-hi [options]
$ shuf playlist.txt
shuf is the opposite of sort — instead of imposing an order, it removes one. With no file, it reads standard input.
Where the lines come from #
shuf has three ways of getting its input, one per usage form:
| Option | Input is… |
|---|---|
| (a file, or stdin) | the lines of the file. |
-e, --echo | the command-line arguments, each treated as one line. |
-i, --input-range=LO-HI | the integers from LO to HI, each treated as one line. |
$ shuf -e red green blue # shuffle three given words
$ shuf -i 1-100 # the numbers 1..100 in random order
Shaping the output #
| Option | Effect |
|---|---|
-n, --head-count=COUNT | Output at most COUNT lines — a random sample rather than the whole shuffle. |
-r, --repeat | Allow lines to be repeated, so output can be longer than the input. Pair with -n to set how many. |
-o, --output=FILE | Write to FILE instead of standard output. |
-z, --zero-terminated | Treat the NUL character as the line delimiter. |
shuf -n 1 is a common idiom — pick one random line.
Reproducible shuffles #
A shuffle is random by default. To get the same shuffle every time — for a repeatable test, say — fix the randomness:
| Option | Effect |
|---|---|
--random-seed=STRING | Seed the shuffle with STRING. The same seed gives the same shuffle. |
--random-source=FILE | Take the random bytes from FILE. The same file gives the same shuffle. Cannot be combined with --random-seed. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A failure — the input could not be read, or -r was used with no lines to repeat. |
Output and evaluation
Peios / Using Peios / Peiosutils / Output and evaluation
This topic gathers the small, sharp commands that scripts are built from: the ones that print something, that read or set the environment, that evaluate an expression or a condition, and that simply succeed or fail.
The commands #
Producing text
| Command | Purpose |
|---|---|
echo | Print its arguments as a line of text. |
printf | Print text from a format string, with precise control over layout. |
yes | Print a line over and over, without stopping. |
seq | Print a sequence of numbers. |
tee | Copy standard input to standard output and to each named file. |
The environment
| Command | Purpose |
|---|---|
env | Run a command with a modified environment — or print the current one. |
printenv | Print environment variables. |
Numbers
| Command | Purpose |
|---|---|
numfmt | Convert numbers to and from human-readable forms (1.5G, 1000000). |
factor | Print the prime factors of a number. |
Evaluating
| Command | Purpose |
|---|---|
expr | Evaluate an arithmetic, string, or comparison expression. |
test | Evaluate a condition — a file check or a comparison — as a true/false result. |
Exit status
| Command | Purpose |
|---|---|
true and false | Do nothing, and succeed — or do nothing, and fail. |
A note on exit status #
Several commands here exist to be used for their exit status rather than their output. Every command, when it finishes, leaves behind a number: 0 means success, anything else means a kind of failure. A shell's if, &&, and || all act on that number.
test is the clearest case — it prints nothing and exists only to set an exit status from a condition. true and false are the most minimal: fixed success and fixed failure. Where this topic's pages give an exit-status table, that number is often the whole point of the command.
Where to start #
For everyday printing, echo; for anything that needs columns, padding, or precise formatting, printf.
echo
Peios / Using Peios / Peiosutils / Output and evaluation
echo prints its arguments, separated by single spaces, followed by a newline.
echo [options] [string...]
$ echo Hello, Peios
Hello, Peios
It is the simplest way to put a line of text on standard output — printing a message, or feeding a fixed string into a pipe.
Options #
| Option | Effect |
|---|---|
-n | Do not print the trailing newline. |
-e | Interpret backslash escape sequences in the arguments (see below). |
-E | Do not interpret escape sequences. This is the default. |
Escape sequences #
With -e, echo recognises these backslash sequences and prints the character they stand for:
| Sequence | Character |
|---|---|
\\ | A literal backslash. |
\n | Newline. |
\t | Horizontal tab. |
\r | Carriage return. |
\b | Backspace. |
\f | Form feed. |
\v | Vertical tab. |
\a | Alert (the terminal bell). |
\e | Escape. |
\0NNN | The byte with octal value NNN. |
\xHH | The byte with hexadecimal value HH. |
\c | Stop — produce no further output. |
echo and printf #
echo is fixed: arguments, spaces, a newline. The moment you need control over the output — columns, padding, a number formatted a particular way, output with no spaces between pieces — use printf instead. printf is also more predictable across environments, since echo's handling of escapes and -n varies.
A note on shells #
Many command shells provide their own built-in echo, and the shell's version is what runs when you type echo at a prompt. Built-in versions vary — especially in how they treat -n, -e, and escape sequences — so their behaviour may differ from what is described here. To be certain you are running this command rather than the shell built-in, invoke it by its full path.
Exit status #
| Code | Meaning |
|---|---|
0 | The output was written. |
1 | The output could not be written. |
printf
Peios / Using Peios / Peiosutils / Output and evaluation
printf prints text built from a format string. The format string is printed literally, except that escape sequences become characters and substitution fields are replaced by the arguments that follow.
printf format [argument...]
$ printf 'the letter %X comes before %X\n' 10 11
the letter A comes before B
Where echo prints arguments as-is, printf gives you control: padding, column widths, number bases, fixed decimal places. It is the command for output that has to line up or be exact.
printf writes only what the format string says — there is no automatic trailing newline. Include \n yourself where you want one.
Escape sequences #
The format string interprets these backslash sequences:
| Sequence | Character |
|---|---|
\\ | Backslash. |
\n \t \r | Newline, tab, carriage return. |
\b \f \v \a | Backspace, form feed, vertical tab, alert. |
\e | Escape. |
\NNN | The byte with octal value NNN. |
\xHH | The byte with hexadecimal value HH. |
\uHHHH, \UHHHHHHHH | A Unicode character by hexadecimal code point. |
%% | A literal %. |
Substitution fields #
A field begins with % and is replaced by the next argument, formatted accordingly.
| Field | Formats the argument as… |
|---|---|
%s | a string. |
%b | a string, with backslash escapes in it interpreted. |
%q | a string, quoted so it is safe to reuse as shell input. |
%c | a single character. |
%d, %i | a signed integer. |
%u | an unsigned integer. |
%x, %X | an unsigned integer in hexadecimal (lower- or upper-case). |
%o | an unsigned integer in octal. |
%f, %F | a decimal floating-point number. |
%e, %E | a number in scientific notation. |
%g, %G | whichever of decimal or scientific notation is shorter. |
Width and precision #
Between the % and the field letter you may put two numbers — %[width][.precision]field:
- width — the minimum number of columns. Output narrower than this is padded with leading spaces; a negative width pads on the right instead.
- precision — after a
.; its meaning depends on the field. For a string it is a maximum length; for an integer, a minimum digit count (zero-padded); for a float, the number of decimal places.
$ printf '%-10s %5.2f\n' apples 3.1
apples 3.10
Reusing the format string #
If more arguments are supplied than the format string has fields, printf repeats the format string until the arguments run out. If there are too few, the leftover fields default to an empty string (or 0 for a number).
$ printf '%s: %d\n' alpha 1 beta 2 gamma 3
alpha: 1
beta: 2
gamma: 3
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A missing operand, a bad format string, or a write failure. |
yes
Peios / Using Peios / Peiosutils / Output and evaluation
yes prints a line over and over, without stopping.
yes [string...]
$ yes
y
y
y
...
With no argument it prints y forever. Given arguments, it prints them — joined by spaces — as the repeated line instead.
$ yes retry
retry
retry
...
yes never stops on its own. It ends when something downstream closes the pipe, or when you interrupt it with Ctrl-C.
What it is for #
yes exists to answer prompts. An older interactive command that asks "are you sure? [y/n]" again and again can be fed a stream of confirmations:
$ yes | slow-interactive-command
Most modern commands have a proper option for this — rm -f, and similar — and that is the better way when it exists. yes is the fallback for a command that offers no such option. It is also a quick way to generate an endless stream of identical lines for a test.
Exit status #
| Code | Meaning |
|---|---|
0 | The output was closed normally. |
1 | A write error occurred. |
seq
Peios / Using Peios / Peiosutils / Output and evaluation
seq prints a sequence of numbers, one per line.
seq [options] last
seq [options] first last
seq [options] first increment last
$ seq 3
1
2
3
$ seq 2 2 10
2
4
6
8
10
The three forms control where the sequence starts and how it steps:
seq last— count from 1 up tolast.seq first last— count fromfirstup tolast.seq first increment last— count fromfirsttolast, stepping byincrement.
The increment may be negative, to count down, and the numbers may be fractional. seq is most often used to drive a loop a fixed number of times.
Options #
| Option | Effect |
|---|---|
-s, --separator=STRING | Separate the numbers with STRING instead of a newline. |
-t, --terminator=STRING | End the output with STRING instead of a newline. |
-w, --equal-width | Pad the numbers with leading zeros so they all have the same width. |
-f, --format=FORMAT | Format each number with a printf-style floating-point FORMAT. |
$ seq -s , 1 5
1,2,3,4,5
$ seq -w 8 10
08
09
10
-f takes a single floating-point conversion — see printf for the format syntax — and cannot be combined with -w.
Exit status #
| Code | Meaning |
|---|---|
0 | The sequence was printed. |
1 | An argument was not a valid number, the increment was zero, or no argument was given. |
env
Peios / Using Peios / Peiosutils / Output and evaluation
env runs a command with a changed environment — the set of NAME=VALUE variables a process inherits. Run with no command, it prints the current environment instead.
env [options] [NAME=VALUE]... [command [arg]...]
$ env LANG=C TZ=UTC my-program
$ env
PATH=/bin
HOME=/home/jack
...
How it works #
You give env a list of NAME=VALUE assignments, then a command. env applies the assignments to its own environment and then runs the command, which inherits the result. The assignments affect only that one run — your shell's environment is untouched.
This is the clean way to run something with a particular variable set, without changing it for everything else, and the standard way to run a command with one variable temporarily different.
With no command, env simply prints the environment it would have used — which, with no options, is the current one.
Building the environment #
| Option | Effect |
|---|---|
-i, --ignore-environment | Start from an empty environment, not the inherited one — so the command sees only what you assign. A bare - argument means the same. |
-u, --unset=NAME | Remove NAME from the environment. |
--file=FILE | Read NAME=VALUE lines from a .env-style FILE and apply them. |
The order is: start (inherited, or empty with -i), apply --file, apply --unset, apply the NAME=VALUE arguments.
Running the command #
| Option | Effect |
|---|---|
-C, --chdir=DIR | Change to DIR before running the command. |
--argv0=NAME | Run the command but present NAME as its zeroth argument. |
-S, --split-string=S | Split S into separate arguments. This exists for script interpreter lines, where everything after the interpreter arrives as one string. |
-S follows GNU's quoting rules. Inside single quotes only \\ and \' are escapes; any other backslash is kept literally along with the character after it. Inside double quotes and outside quotes, an unknown backslash sequence is an error.
Signal handling #
env can adjust how the command treats signals before launching it:
| Option | Effect |
|---|---|
--ignore-signal[=SIG] | Set the named signals to be ignored. |
--default-signal[=SIG] | Reset the named signals to their default handling. |
--block-signal[=SIG] | Block delivery of the named signals while the command runs. |
--list-signal-handling | List the signal-handling changes the other options requested. |
Other options #
| Option | Effect |
|---|---|
-0, --null | When printing the environment, end each line with a NUL character instead of a newline. Not valid together with a command. |
-v, --debug | Print each processing step env performs. |
Exit status #
When env runs a command, it exits with that command's status. The exception is a failure in env itself:
| Code | Meaning |
|---|---|
| (command's own) | The command ran; this is its exit status. |
125 | env itself failed — for example, a bad option. |
126 | The command was found but could not be run. |
127 | The command was not found. |
printenv
Peios / Using Peios / Peiosutils / Output and evaluation
printenv prints environment variables.
printenv [options] [variable...]
Name one or more variables and printenv prints the value of each. Name none and it prints the whole environment, one NAME=VALUE pair per line.
$ printenv HOME
/home/jack
$ printenv
PATH=/bin
HOME=/home/jack
...
Options #
| Option | Effect |
|---|---|
-0, --null | End each output line with a NUL character instead of a newline — so values that themselves contain newlines can still be told apart. |
printenv and env #
Both can print the environment. printenv is the one built for reading it — it can pick out a single variable by name. env prints the environment too, but its real job is running a command with a modified one. Use printenv to look something up; use env to change something for a command.
Exit status #
| Code | Meaning |
|---|---|
0 | Every named variable was found and printed. |
1 | A named variable was not set. |
2 | A usage error. |
numfmt
Peios / Using Peios / Peiosutils / Output and evaluation
numfmt converts numbers between plain digits and human-readable forms — turning 1500000 into 1.5M, or 1.5M back into 1500000.
numfmt [options] [number...]
$ numfmt --to=si 1500000
1.5M
$ numfmt --from=si 1.5M
1500000
numfmt takes numbers as arguments, or — with none — reads them from standard input, which lets it rescale a column of numbers flowing through a pipe.
The direction of conversion #
| Option | Effect |
|---|---|
--from=UNIT | Parse input numbers that carry a UNIT suffix, scaling them up to plain digits. |
--to=UNIT | Scale output numbers down and add a UNIT suffix. |
A UNIT is one of:
| Unit | Suffixes mean |
|---|---|
none | No suffixes — a suffix is an error. The default. |
si | Powers of 1000: 1K = 1000, 1M = 1000000. |
iec | Powers of 1024: 1K = 1024, 1M = 1048576. |
iec-i | Powers of 1024 with a two-letter suffix: 1Ki = 1024, 1Mi = 1048576. |
auto | Accept any of the above on input, reading Ki/Mi as powers of 1024 and bare K/M as powers of 1000. |
Working on fields #
By default numfmt converts the whole input line. To convert numbers sitting in a column of wider text, name the field:
| Option | Effect |
|---|---|
--field=FIELDS | Convert only the numbers in these fields. FIELDS uses the same range syntax as cut — 3, 2-5, 2-. |
-d, --delimiter=X | Use X to separate fields instead of whitespace. |
--header[=N] | Pass the first N lines through unconverted, as a header. |
Shaping the output #
| Option | Effect |
|---|---|
--format=FORMAT | Format the number with a printf-style floating-point FORMAT, controlling width, padding, and precision. |
--padding=N | Pad the output to N columns — positive right-aligns, negative left-aligns. |
--grouping | Group digits according to the locale (for example, 1,000,000). |
--round=METHOD | Choose the rounding method used when scaling. |
--suffix=SUFFIX | Append SUFFIX to each result, and accept it on input. |
--from-unit=N / --to-unit=N | Set the unit size the input or output is counted in. |
Other options #
| Option | Effect |
|---|---|
--invalid=MODE | Choose what to do with input that is not a valid number: abort, fail, warn, or ignore. |
-z, --zero-terminated | Treat the NUL character as the line delimiter. |
--debug | Print warnings about questionable input. |
Exit status #
| Code | Meaning |
|---|---|
0 | Every number was converted. |
2 | An input was not a valid number, or an option was misused. |
expr
Peios / Using Peios / Peiosutils / Output and evaluation
expr evaluates a single expression and prints its value.
expr expression
$ expr 6 + 7
13
$ expr length "Peios"
5
Each part of the expression — every number, operator, and keyword — is a separate argument. That is why the spaces matter: expr 6+7 is one argument and not an expression, while expr 6 + 7 is three.
Arithmetic #
| Operator | Result |
|---|---|
ARG1 + ARG2 | Sum. |
ARG1 - ARG2 | Difference. |
ARG1 * ARG2 | Product. |
ARG1 / ARG2 | Integer quotient. |
ARG1 % ARG2 | Remainder. |
Comparison #
A comparison yields 1 for true and 0 for false. It is arithmetic when both sides are numbers, and lexical otherwise.
| Operator | True when… |
|---|---|
ARG1 = ARG2 | the two are equal. |
ARG1 != ARG2 | they are unequal. |
ARG1 < ARG2 | ARG1 is less. |
ARG1 <= ARG2 | ARG1 is less or equal. |
ARG1 > ARG2 | ARG1 is greater. |
ARG1 >= ARG2 | ARG1 is greater or equal. |
Logic #
| Operator | Result |
|---|---|
ARG1 | ARG2 | ARG1 if it is neither null nor 0; otherwise ARG2. |
ARG1 & ARG2 | ARG1 if neither argument is null or 0; otherwise 0. |
String operations #
| Form | Result |
|---|---|
length STRING | The number of characters in STRING. |
substr STRING POS LENGTH | LENGTH characters of STRING, counting POS from 1. |
index STRING CHARS | The position of the first of any of CHARS in STRING, or 0. |
STRING : REGEXP | Match REGEXP anchored at the start of STRING. Returns the matched length, or the text captured by \(…\). |
match STRING REGEXP | The same as STRING : REGEXP. |
Many of these operators — *, <, |, ( — are also meaningful to a shell, so quote or escape them so they reach expr intact.
Exit status #
expr's exit status reports on the value it computed:
| Code | Meaning |
|---|---|
0 | The result was neither null nor 0. |
1 | The result was null or 0. |
2 | The expression was syntactically invalid. |
3 | An error occurred during evaluation — such as division by zero. |
factor
Peios / Using Peios / Peiosutils / Output and evaluation
factor prints the prime factors of a number.
factor [options] [number...]
$ factor 360
360: 2 2 2 3 3 5
Each line of output is the original number, a colon, and its prime factors in increasing order, repeated as often as they divide in. Give several numbers and factor factors each; give none and it reads numbers from standard input.
Options #
| Option | Effect |
|---|---|
-h, --exponents | Write a repeated factor in exponent form — p^e — instead of repeating it. |
$ factor -h 360
360: 2^3 3^2 5
Exit status #
| Code | Meaning |
|---|---|
0 | Every number was factored. |
1 | An input was not a valid positive integer. |
test
Peios / Using Peios / Peiosutils / Output and evaluation
test evaluates a single condition and reports whether it is true or false. It prints nothing — the answer is its exit status: 0 for true, 1 for false. That makes it the command a shell's if, while, &&, and || are built on.
test expression
[ expression ]
The two forms are the same command. [ is test under another name; when invoked as [, it requires a closing ] as its last argument. [ -f notes.txt ] and test -f notes.txt are identical.
$ test -f notes.txt && echo "the file exists"
File tests #
Existence and type #
| Test | True when the file… |
|---|---|
-e FILE | exists. |
-f FILE | exists and is a regular file. |
-d FILE | exists and is a directory. |
-L FILE, -h FILE | exists and is a symbolic link. |
-p FILE | exists and is a named pipe (FIFO). |
-S FILE | exists and is a socket. |
-b FILE | exists and is a block device. |
-c FILE | exists and is a character device. |
-s FILE | exists and is not empty. |
Access #
These three tests ask whether you may actually use the file:
| Test | True when… |
|---|---|
-r FILE | you may read the file. |
-w FILE | you may write the file. |
-x FILE | you may execute the file, or search the directory. |
On Peios these are real access checks. test asks the kernel whether your token is granted the right in question — a live check against the file's security descriptor, the same decision any actual read, write, or execute would face. The answer is therefore honest: test -w FILE is true exactly when a write would be permitted.
-x on a regular file means "you are permitted to execute it" — which is a different question from whether the file is an executable. A file can be runnable code and still fail -x for you, and the two are decided separately; see Access decisions.
Comparing two files #
| Test | True when… |
|---|---|
FILE1 -nt FILE2 | FILE1 is newer than FILE2. |
FILE1 -ot FILE2 | FILE1 is older than FILE2. |
FILE1 -ef FILE2 | both name the same file (same device and inode). |
Other file tests #
| Test | True when the file… |
|---|---|
-t FD | file descriptor FD is open on a terminal. |
-N FILE | has been modified since it was last read. |
-O FILE, -G FILE | carries an owner-id / group-id field matching the caller's. |
-g FILE, -u FILE, -k FILE | has its set-group-id, set-user-id, or sticky inode bit set. |
The last two rows probe decorative inode fields. The Peios access model does not consult them — they are stored on the inode and reported for completeness, but they carry no authority. For a true picture of what may be done to a file, use the access tests -r, -w, -x above, not -O, -g, or -u.
String tests #
| Test | True when… |
|---|---|
-n STRING | STRING is not empty. A bare STRING means the same. |
-z STRING | STRING is empty. |
STRING1 = STRING2 | the strings are equal. |
STRING1 != STRING2 | the strings are unequal. |
STRING1 < STRING2 | STRING1 sorts before STRING2. |
STRING1 > STRING2 | STRING1 sorts after STRING2. |
Integer comparisons #
| Test | True when… |
|---|---|
A -eq B | A equals B. |
A -ne B | A does not equal B. |
A -lt B | A is less than B. |
A -le B | A is less than or equal to B. |
A -gt B | A is greater than B. |
A -ge B | A is greater than or equal to B. |
Combining conditions #
| Form | Result |
|---|---|
! EXPRESSION | The negation. |
( EXPRESSION ) | Grouping — escape the parentheses so the shell does not take them. |
EXPR1 -a EXPR2 | True when both are true. |
EXPR1 -o EXPR2 | True when either is true. |
-a and -o are genuinely ambiguous to parse and are best avoided. Combine separate test calls with the shell's own && and || instead:
test -f notes.txt && test -r notes.txt
A note on shells #
Many command shells provide their own built-in test and [, and the shell's version is what runs when you type test at a prompt or in a script. A built-in may not behave as described here — in particular, the access tests -r, -w, and -x are the real access checks described above only when this command runs. To be certain you are running this command rather than the shell built-in, invoke it by its full path.
Exit status #
| Code | Meaning |
|---|---|
0 | The expression was true. |
1 | The expression was false. |
2 | The expression was malformed. |
true and false
Peios / Using Peios / Peiosutils / Output and evaluation
true and false do nothing at all. Their only product is an exit status: true always succeeds, false always fails.
true
false
$ true ; echo $?
0
$ false ; echo $?
1
true exits 0. false exits 1. They take no arguments that matter, produce no output, and have no effect on anything.
What they are for #
A command that is only an exit status is useful as a fixed value in places that expect a command:
- An always-true or always-false loop.
while trueis the standard way to write a loop that runs until something inside it breaks out. - A deliberate placeholder. Setting a configurable command to
truemakes that step do nothing and "succeed";falsemakes it a step that always fails. - A known result in a test. When a script or a condition needs a guaranteed pass or fail to check against,
trueandfalseprovide it.
They are the simplest commands there are — and the overview's note on exit status is the whole idea behind them.
Exit status #
| Code | Meaning |
|---|---|
0 | Returned by true, always. |
1 | Returned by false, always. |
tee
Peios / Using Peios / Peiosutils / Output and evaluation
tee reads standard input and writes it, unchanged, to standard output and to each file you name. It is the T-junction of a pipeline: the data keeps flowing down the pipe while a copy is also captured on disk.
tee [options] [file...]
$ echo saved | tee note.txt
saved
$ cat note.txt
saved
The name is the shape of the letter T — one stream in, the same stream out two ways. That makes tee the tool for the moment you want to both see (or keep piping) some data and keep it at the same time.
Writing to several places at once #
Every byte that arrives on standard input is written to standard output and to each named file. Name more than one file and each one receives a full, identical copy:
$ producer | tee a.log b.log c.log | consumer
Here producer's output reaches consumer down the pipe exactly as if tee were not there, and a.log, b.log, and c.log each end up holding the same complete copy of it. With no files named, tee copies input straight to standard output and nothing else — a plain pass-through.
tee does not buffer between reads: it writes each chunk out as it is read, so a file being written by tee fills up as the data flows, rather than all at once when the input ends.
Overwrite or append #
By default, each named file is truncated — opened fresh, so any existing contents are discarded before the new data is written. A file that does not exist is created.
With -a (--append), tee appends to each named file instead: existing contents are kept and the new data is added to the end. A file that does not exist is still created.
$ echo first | tee log.txt # log.txt now holds "first"
$ echo second | tee -a log.txt # log.txt now holds "first" then "second"
Creating a file inherits a security descriptor #
When tee creates a new output file — a named file that did not already exist — that file needs a security descriptor, and it inherits one from the directory it is created in. This is the same creation-inherits-a-descriptor rule that governs every file-creating tool on Peios; see Files and directories for the shared model.
With -a, when the named file already exists, tee opens that existing file to append and creates nothing, so no new descriptor is involved. Whether opening a file to write succeeds — whether creating a new one or writing an existing one — is decided by the normal access check against the relevant directory or file. tee reports a file it cannot open and, by default, carries on with the outputs it can write.
Files named - #
tee gives no special meaning to -. A file argument of - refers to a file literally named - in the current directory; it is opened, created, and written exactly like any other named file. It is not a stand-in for standard output — standard output is always written and needs no naming.
Ignoring interrupts #
With -i (--ignore-interrupts), tee ignores the interrupt signal (the one a terminal sends on Ctrl-C). This lets tee keep copying to its files even as an interrupt tears down the rest of a pipeline, so the capture is not cut short.
Behaviour on write errors #
By default, if a write to one output fails, tee reports it and stops writing to that output, but keeps writing to the others; a broken-pipe error on standard output is treated quietly. Two options change this policy.
-p sets write-error behaviour so that errors on a pipe (a downstream reader that has gone away) are ignored while other write errors are still reported.
--output-error[=MODE] sets the policy explicitly. Given on its own, with no =MODE, it selects warn-nopipe. The modes are:
| Mode | Behaviour |
|---|---|
warn | Warn on a write error to any output, including a broken pipe, and continue with the remaining outputs. |
warn-nopipe | Warn on a write error that is not a pipe error, and continue. Broken-pipe errors are ignored. This is the mode selected by a bare --output-error. |
exit | Exit on a write error to any output, including a broken pipe. |
exit-nopipe | Exit on a write error that is not a pipe error. Broken-pipe errors are ignored. |
Options #
| Option | Effect |
|---|---|
-a, --append | Append to each named file rather than truncating it. |
-i, --ignore-interrupts | Ignore the interrupt signal. |
-p | Ignore errors from writing to a broken pipe, still reporting other write errors. |
--output-error[=MODE] | Set the write-error policy: warn, warn-nopipe, exit, or exit-nopipe. A bare --output-error means warn-nopipe. |
Exit status #
| Code | Meaning |
|---|---|
0 | The input was copied to standard output and every named file without error. |
1 | A file could not be opened, a write failed under a policy that reports or exits on it, or the input could not be read. |
Hashing and encoding
Peios / Using Peios / Peiosutils / Hashing and encoding
This topic covers two related jobs. Hashing reduces a file to a short fixed-size value — a checksum — that can be used to tell whether the file has changed. Encoding rewrites binary data as plain text so it can travel safely through channels that only handle text.
The commands #
Checksums and hashes
| Command | Purpose |
|---|---|
| checksum commands | md5sum, sha1sum, sha256sum, and the rest — one command per hash algorithm. |
cksum | The general checksum tool — any of the algorithms, selected by an option. |
sum | A small, legacy block-checksum. |
Encoding
| Command | Purpose |
|---|---|
base32 and base64 | Encode binary data as text using the base32 or base64 alphabet, and decode it back. |
basenc | The general encoder — base64, base32, base16, and several more, selected by an option. |
What these commands are not #
It is worth being clear up front, because both jobs are easy to mistake for something they are not.
A hash is not encryption. Hashing is one-way: a checksum tells you whether data has changed, but the original cannot be recovered from it. Comparing it with a trusted expected hash can detect corruption or tampering; a hash supplied alongside an untrusted file does not establish who published that file. Hashing does not keep anything secret.
Encoding is not encryption either. base64 rewrites data into a text-safe form, but anyone can decode it straight back — it is a change of representation, not of secrecy. Encoding something does not protect it.
Neither hashing nor encoding hides data. They are about integrity and transport, not confidentiality.
Where to start #
To verify a file you downloaded against a published checksum, read the checksum commands. To turn binary data into something safe to paste into text, read base32 and base64.
Checksum commands
Peios / Using Peios / Peiosutils / Hashing and encoding
This page covers a family of commands that all work the same way and differ only in which hash algorithm they use:
| Command | Algorithm | Digest size |
|---|---|---|
md5sum | MD5 | 128-bit |
sha1sum | SHA-1 | 160-bit |
sha224sum | SHA-224 | 224-bit |
sha256sum | SHA-256 | 256-bit |
sha384sum | SHA-384 | 384-bit |
sha512sum | SHA-512 | 512-bit |
b2sum | BLAKE2b | up to 512-bit |
sha256sum [options] [file...]
Everything below is written with sha256sum, but applies to every command in the table.
Computing a checksum #
Run plainly, the command prints the hash of each file, followed by the file name:
$ sha256sum installer.iso
9f86d0818884...b1a5 installer.iso
With no file, it reads standard input. The hash is a fingerprint of the file's contents: change a single byte and the hash changes completely.
Verifying with a checksum #
The everyday use is verification — confirming a file is exactly what it should be, usually a download.
First, the file's publisher computes a checksum and publishes it, often in a file:
$ sha256sum installer.iso > installer.iso.sha256
Obtain the publisher's checksum file through a trusted source. Computing a new checksum from your own download only records its current contents; it does not verify them against the publisher's copy.
Use the complete digest-and-filename lines, not a bare hash or the abbreviated display example above. Run the check from the directory against which the manifest's file names were written. Giving an absolute path to the checksum file does not change that working directory.
For example, with both files in ~/Downloads:
$ cd ~/Downloads
$ sha256sum --check --strict installer.iso.sha256
installer.iso: OK
-c reads each hash filename line, recomputes the hash, and reports OK or FAILED. A checksum mismatch means the file differs from the one the checksum describes. File-access failures and malformed input can also prevent verification; read the diagnostic and check the exit status before using the download.
Keep manifests newline-delimited. --zero output is not a NUL-delimited input format for --check.
| Option | Effect |
|---|---|
-c, --check | Read checksums from the given files and verify them. |
--ignore-missing | In check mode, skip unavailable listed files when intentionally checking a subset; see the caveat below. |
--quiet | In check mode, print nothing for files that pass — only failures. |
--status | In check mode, suppress comparison results and summary warnings; determine success from the exit status. File-access or argument errors may still print diagnostics. |
-w, --warn | Warn about improperly formatted lines in the checksum file. |
--strict | In check mode, fail if any checksum line is malformed. |
Without --strict, malformed lines can be skipped while the command still succeeds. A checksum input with no properly formatted lines fails in either mode.
Avoid --ignore-missing when verifying that a complete download set is present. In the current implementation it skips file-open failures, including errors other than a missing file. It still fails if no file in a checksum input verifies successfully.
Output format #
These dedicated commands produce plain, untagged hash file output by default. Unlike cksum, they do not accept --untagged.
| Option | Effect |
|---|---|
--tag | Produce a tagged line — SHA256 (file) = hash — that records which algorithm was used. |
-z, --zero | End each output line with a NUL character instead of a newline. |
-b, --binary | Note the file as read in binary mode. |
-t, --text | Note the file as read in text mode. |
b2sum additionally accepts -l, --length=BITS to produce a shorter BLAKE2b digest.
A note on choosing an algorithm #
These commands detect change — but not all of them resist a deliberate attempt to forge a match. MD5 and SHA-1 can be defeated by an attacker who wants two different files to share a checksum. They are still fine for catching accidental corruption, but for verifying that a file has not been tampered with, use a SHA-2 command (sha256sum and up) or b2sum.
Exit status #
| Code | Meaning |
|---|---|
0 | Success under the selected options. |
1 | Verification failed, a file could not be accessed or read, or input/options were invalid. Malformed checksum lines cause failure with --strict, or when no properly formatted lines exist. |
The flag and verification behavior above is checked against peiosutils source at ec00f31 and its checksum validator. It was not tested on an installed Peios release. Treat any nonzero status as failure and use the diagnostics to distinguish a mismatch from an input or access error.
cksum
Peios / Using Peios / Peiosutils / Hashing and encoding
cksum is the general checksum command. Where each of the checksum commands is fixed to one algorithm, cksum does any of them — the algorithm is an option.
cksum [options] [file...]
$ cksum installer.iso
3915528286 372736000 installer.iso
$ cksum -a sha256 --untagged installer.iso
9f86d0818884...b1a5 installer.iso
With no -a, cksum computes a CRC checksum and prints the CRC value, the file's byte count, and the name. With -a, it uses the selected algorithm. The SHA-256 example adds --untagged to request the plain hash file output; without it, SHA-256 output is tagged by default.
Choosing the algorithm #
| Option | Effect |
|---|---|
-a, --algorithm=NAME | Use the named algorithm. |
NAME may be:
| Name | Algorithm |
|---|---|
crc | The default CRC. |
crc32b | A CRC-32 variant. |
md5 | MD5 — as md5sum. |
sha1 | SHA-1 — as sha1sum. |
sha224, sha256, sha384, sha512 | The SHA-2 family — as the matching sha*sum. |
sha3 | SHA-3. |
blake2b | BLAKE2b — as b2sum. |
sm3 | The SM3 hash. |
sysv, bsd | The legacy block checksums — as sum. |
crc, crc32b, sha3, and sm3 are available only through cksum — there is no dedicated command for them.
Verifying #
Choose the check command to match the publisher's manifest format. Run it from the directory against which the manifest's file names were written; the manifest's own location does not set that directory. The checksum workflow covers trusted expected values, complete manifest lines and exit-status checks.
For tagged lines such as SHA256 (installer.iso) = ..., the input identifies the algorithm:
cksum --check --strict CHECKSUMS
For untagged SHA-256 lines such as hash installer.iso, supply the algorithm:
cksum --algorithm=sha256 --check --strict SHA256SUMS
The dedicated sha256sum --check --strict SHA256SUMS command already selects SHA-256. With cksum, an explicitly selected algorithm must agree with any tagged input. Do not add --tag, --binary or --text to a check command: those output-mode options are rejected during verification.
You cannot combine --check with --algorithm=crc, crc32b, bsd or sysv. The default CRC output is not a checkable checksum manifest. Use normal newline-delimited checksum output for manifests; --zero output is not read as NUL-delimited check input.
| Option | Effect |
|---|---|
-c, --check | Read checksums and verify the files against them. |
--ignore-missing | Skip unavailable listed files when intentionally checking a subset; see the caveat below. |
--quiet | Print nothing for files that pass. |
--status | Suppress comparison results and summary warnings; check the exit status. File-access or argument errors may still print diagnostics. |
-w, --warn | Warn about malformed checksum lines. |
--strict | Fail on a malformed checksum line. |
Without --strict, malformed lines can be skipped while a check still succeeds. A checksum input with no properly formatted lines fails in either mode. Use --ignore-missing only for an intentional subset: the current implementation skips file-open failures beyond missing files, so success does not establish that the whole set was available. It still fails if no file in a checksum input verifies successfully.
Output format #
| Option | Effect |
|---|---|
--tag | Produce a tagged ALGORITHM (file) = hash line. The default for most algorithms. |
--untagged | Produce a plain hash file line. |
--raw | Output the raw binary digest, with nothing else. |
--base64 | Print the digest in base64 rather than hexadecimal. |
-l, --length=BITS | For an algorithm that supports it, produce a shorter digest. |
-z, --zero | End each output line with a NUL character. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success under the selected options. |
1 | Verification failed, a file could not be accessed or read, or input/options were invalid. Malformed checksum lines cause failure with --strict, or when no properly formatted lines exist. |
These verification rules are checked against current command handling and manifest validation, not tested on an installed Peios release. Treat any nonzero status as failure.
sum
Peios / Using Peios / Peiosutils / Hashing and encoding
sum computes a small, old-style checksum of a file and reports it along with the file's size in blocks.
sum [options] [file...]
$ sum archive.tar
12345 84 archive.tar
The output is the checksum, the block count, and the file name. With no file, sum reads standard input.
Which algorithm #
sum predates the modern hashes and offers two historical algorithms:
| Option | Algorithm |
|---|---|
-r | The BSD checksum, counted in 1 KiB blocks. This is the default. |
-s, --sysv | The System V checksum, counted in 512-byte blocks. |
When to use it #
sum exists for compatibility — for reading checksums produced by old tools, and for scripts that still expect its output. Its checksum is short and weak: it catches some accidental corruption, but it is easily fooled and gives no protection against tampering.
For anything new, do not use sum. Use a checksum command such as sha256sum, or cksum — both of which cksum can still produce, via cksum -a bsd and cksum -a sysv, if you need the legacy values.
Exit status #
| Code | Meaning |
|---|---|
0 | The checksum was computed. |
1 | A file could not be read. |
base32 and base64
Peios / Using Peios / Peiosutils / Hashing and encoding
base32 and base64 encode binary data as plain text — and decode it back. They are the same command with one difference: the alphabet they use.
base32 [options] [file]
base64 [options] [file]
$ echo Peios | base64
UGVpb3MK
$ echo UGVpb3MK | base64 -d
Peios
What encoding is for #
Some channels only carry text safely — they mangle or reject arbitrary bytes. Encoding rewrites binary data using a small, safe set of characters, so it can pass through unharmed; decoding reverses it exactly. base64 uses 64 characters and is the more compact; base32 uses 32. Preserve the encoded text exactly, including letter case: the current Base32 decoder expects uppercase letters. With no file, both read standard input.
Encoding is not encryption — anyone can decode the result. See the overview.
Options #
By default these commands encode. The options below apply identically to both.
| Option | Effect |
|---|---|
-d, --decode | Decode instead of encode. |
-i, --ignore-garbage | When decoding, skip any characters that are not part of the alphabet, rather than failing on them. |
-w, --wrap=COLS | When encoding, wrap the output to lines of COLS characters. The default is 76; -w 0 disables wrapping and produces one unbroken line. |
When decoding, line-feed and carriage-return characters in the input are tolerated; -i is for other stray characters. Use it only when those characters are known transport noise. It discards characters outside the alphabet; it does not repair a damaged payload or normalize letter case.
Decode before using the result #
A decoder can write part of the result before reporting invalid input. A nonempty output file is therefore not proof that decoding succeeded. Decode to a separate file, check the exit status, and use the result only on success. Do not redirect over your input or an existing file you need: the shell opens and truncates the output before decoding starts.
For example, in a directory you control, choose a new output name and run:
if ; then
else
fi
In a script, put any processing of the decoded file inside the success branch. The failure branch returns a nonzero status and deliberately leaves any partial file for inspection; remove it before treating that name as a completed result. The same status check applies to base32 and basenc. Decoding success establishes a valid encoding, not the origin or safety of the decoded content.
This failure behavior is checked against current peiosutils source, not tested on an installed Peios release.
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | A file could not be read, or — when decoding — the input was not valid for the alphabet. |
basenc
Peios / Using Peios / Peiosutils / Hashing and encoding
basenc is the general encoding command. Where base32 and base64 each do one encoding, basenc does many — the encoding is chosen by an option.
basenc encoding [options] [file]
$ echo Peios | basenc --base16
5065696F730A
Every run of basenc must name an encoding. With no file, it reads standard input.
The encodings #
| Option | Encoding |
|---|---|
--base64 | Standard base64 — the same as the base64 command. |
--base64url | Base64 with a file- and URL-safe alphabet. |
--base32 | Standard base32 — the same as the base32 command. |
--base32hex | Base32 with the extended-hex alphabet. |
--base16 | Hexadecimal. |
--base2msbf | A bit string, most-significant bit first. |
--base2lsbf | A bit string, least-significant bit first. |
--z85 | A compact, ASCII85-style encoding. When encoding, the input length must be a multiple of 4; when decoding, a multiple of 5. |
--base58 | Base58 — an alphabet chosen so its characters are not visually confusable. |
Options #
By default basenc encodes. These options apply to whichever encoding you chose:
| Option | Effect |
|---|---|
-d, --decode | Decode instead of encode. |
-i, --ignore-garbage | When decoding, skip characters that are not part of the alphabet. |
-w, --wrap=COLS | When encoding, wrap output to lines of COLS characters. -w 0 disables wrapping. |
Check decoding before using the output #
Select the same encoding used by the sender. Decoding can write a partial result before failing, so check the exit status before using the output. Follow the separate-output example, replacing base64 --decode with, for example, basenc --base64url --decode for URL-safe Base64 input. Do not use --ignore-garbage to guess your way past a damaged payload: it only discards characters outside the selected alphabet.
basenc and the dedicated commands #
For plain base64 or base32, base64 and base32 are the shorter way to ask. Use basenc when you need an encoding the dedicated commands do not offer — hex, URL-safe base64, the bit-string forms, z85, or base58.
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | No encoding was named, a file could not be read, or the input was not valid for the chosen encoding. |
System and processes
Peios / Using Peios / Peiosutils / System and processes
This topic gathers the commands that deal with the running system rather than with files: reading the time, asking what hardware and system you are on, launching another command under some constraint, signalling a process, working with the terminal.
The commands #
Time
| Command | Purpose |
|---|---|
date | Print — or set — the system date and time. |
sleep | Pause for a given length of time. |
uptime | Show how long the system has been running. |
Running a command under a constraint
| Command | Purpose |
|---|---|
timeout | Run a command, and kill it if it runs too long. |
nohup | Run a command so it survives the terminal closing. |
nice | Run a command at an adjusted scheduling priority. |
stdbuf | Run a command with altered stream buffering. |
chroot | Run a command with a different directory as its root. |
Signalling processes
| Command | Purpose |
|---|---|
kill | Send a signal to a process. |
System identity and capacity
| Command | Purpose |
|---|---|
uname | Print system information — the OS, the kernel, the machine. |
arch | Print the machine's hardware architecture. |
hostname | Display — or set — the system's host name. |
hostid | Print the host's numeric identifier. |
nproc | Print how many processor cores are available. |
Who you are
| Command | Purpose |
|---|---|
whoami | Print the name of the user a process is running as. |
id | Print the user and group identity of a process, or of a named user. |
groups | Print the groups a user belongs to. |
logname | Print the login name of the user a process is running as. |
These report the projection of a token onto POSIX user and group numbers, which is what Linux programs see. The numbers grant nothing on their own — id explains what they can and cannot say, and token shows the identity underneath them.
Tokens and logon sessions
| Command | Purpose |
|---|---|
token | Inspect and manage a process's token. |
logonse | Inspect and manage logon sessions and their processes. |
Storage and terminal
| Command | Purpose |
|---|---|
sync | Flush cached writes out to persistent storage. |
tty | Print the name of the terminal on standard input. |
stty | Display or change terminal settings. |
A note on changing the system #
Most commands here only report. A few can change the running system — date can set the clock, hostname can set the host name. Changing system-wide state is a privileged operation: it succeeds only for a caller whose token holds the right to do it, and is refused otherwise. The pages for those commands say so where it applies, and Privileges is the full picture.
Where to start #
uname answers "what am I running on?". kill is the one to read carefully — sending the wrong signal to the wrong process is a quick way to lose work.
date
Peios / Using Peios / Peiosutils / System and processes
date prints the current date and time. With the right option, it can also set the system clock, display some other moment, or format a date however you need.
date [options] [+format]
$ date
Sun 17 May 2026 14:32:08 BST
$ date +%Y-%m-%d
2026-05-17
Formatting the output #
A +format argument controls the output. The format string is printed literally, except for % sequences, each of which is replaced by a piece of the date. The common ones:
| Sequence | Is replaced by |
|---|---|
%Y %m %d | Year, month, day. |
%H %M %S | Hour, minute, second. |
%F | The full date — same as %Y-%m-%d. |
%T | The time — same as %H:%M:%S. |
%A %a | Weekday name, full and abbreviated. |
%B %b | Month name, full and abbreviated. |
%j | Day of the year. |
%s | Seconds since 1970-01-01 UTC. |
%z %Z | Numeric and named time zone. |
%% | A literal %. |
The full set is long; date --help lists every sequence with an example.
Displaying a different moment #
| Option | Effect |
|---|---|
-d, --date=STRING | Show the time described by STRING — "yesterday", "next Friday", "@1615432800" — instead of now. |
-r, --reference=FILE | Show FILE's last modification time. |
-f, --file=DATEFILE | Like -d, but once for each line of DATEFILE. |
-u, --universal | Work in Coordinated Universal Time (UTC) rather than the local zone. |
Standard formats #
| Option | Output |
|---|---|
-I, --iso-8601[=FMT] | ISO 8601 — FMT is date, hours, minutes, seconds, or ns. |
-R, --rfc-email | The format used in email headers. |
--rfc-3339=FMT | RFC 3339 — FMT is date, seconds, or ns. |
Setting the clock #
| Option | Effect |
|---|---|
-s, --set=STRING | Set the system clock to the time described by STRING. |
Setting the clock changes state for the whole system, so it is a privileged operation — it succeeds only for a caller whose token carries the right to set the time, and is refused otherwise. See Privileges.
Exit status #
| Code | Meaning |
|---|---|
0 | The date was printed or set. |
1 | An invalid date or format, or the clock could not be set. |
sleep
Peios / Using Peios / Peiosutils / System and processes
sleep does nothing for a given length of time, then exits.
sleep number[suffix]...
$ sleep 5 # pause for five seconds
$ sleep 1.5h # pause for an hour and a half
It is used to space things out — a pause between the steps of a script, a delay before a retry.
Specifying the duration #
number may be a whole number or a fraction. A suffix gives the unit:
| Suffix | Unit |
|---|---|
s | Seconds. This is the default if no suffix is given. |
m | Minutes. |
h | Hours. |
d | Days. |
Given more than one argument, sleep waits for the sum of them — sleep 1m 30s pauses for ninety seconds.
Exit status #
| Code | Meaning |
|---|---|
0 | The full time elapsed. |
1 | A bad argument, or sleep was interrupted before the time was up. |
timeout
Peios / Using Peios / Peiosutils / System and processes
timeout runs a command with a time limit. If the command finishes in time, nothing special happens. If it is still running when the limit is reached, timeout kills it.
timeout [options] duration command...
$ timeout 30s slow-fetch https://example.com
It is the guard against a command that might hang — a network fetch, a script that could loop forever.
The duration #
duration is a number with an optional unit suffix: s for seconds (the default), m for minutes, h for hours, d for days. A duration of 0 disables the limit.
How the command is stopped #
When the limit is reached, timeout sends the command a TERM signal — a request to shut down cleanly. A well-behaved command stops there. A command that ignores TERM keeps running, which is what -k is for.
| Option | Effect |
|---|---|
-s, --signal=SIGNAL | Send SIGNAL instead of TERM when the limit is reached. |
-k, --kill-after=DURATION | If the command is still running DURATION after the first signal, send it a KILL signal — which cannot be ignored. |
--preserve-status | Exit with the command's own status even when it timed out. |
--foreground | Allow the command to keep reading from the terminal; do not time out the command's own children. |
-v, --verbose | Report to standard error whenever a signal is sent. |
Exit status #
timeout passes through the command's exit status when the command finishes on its own. Otherwise:
| Code | Meaning |
|---|---|
| (command's own) | The command finished within the limit. |
124 | The command timed out. |
125 | timeout itself failed. |
126 | The command was found but could not be run. |
127 | The command was not found. |
137 | The command was ended by a KILL signal. |
nohup
Peios / Using Peios / Peiosutils / System and processes
nohup runs a command so that it survives the terminal closing. Normally, when a terminal session ends, the system sends a hangup signal to the commands running under it, and they stop. A command started with nohup ignores that signal and keeps running.
nohup [options] command [arg]...
$ nohup long-build &
It is how you start something that needs to outlast your session — a long build, a background job — without it being killed the moment you disconnect.
Where the output goes #
A command run with nohup is meant to outlive the terminal, so nohup cannot leave its streams attached to one. It redirects any stream that is still connected to a terminal:
- Standard input, if it is a terminal, is replaced with an empty source — the command reads nothing.
- Standard output, if it is a terminal, is appended to a file named
nohup.outin the current directory. If that file cannot be opened there,nohupfalls back tonohup.outin your home directory. - Standard error, if it is a terminal, is sent to wherever standard output now goes.
Streams that you have already redirected yourself — to a file, or a pipe — are left as they are.
How nohup.out is secured #
When nohup has to create nohup.out, that file is a new file and needs a security descriptor. nohup does not let it inherit one from the directory. It creates nohup.out with a deliberate, locked descriptor: full access for the owner, and no one else.
The reasoning is that nohup.out captures whatever the command writes, which the person running it has not chosen to share. A file that appears as a side effect should not be more open than its creator intended — so nohup gives it the safe default of owner-only, rather than whatever the surrounding directory would have handed down.
The POSIX mode bits agree with the descriptor: the file is created with mode 0600 in the same open call that creates it, so there is no moment at which it exists more openly.
| Option | Effect |
|---|---|
--sddl=SDDL | Create nohup.out with the security descriptor given as an SDDL string, instead of the owner-only default. |
--sddl only matters when nohup actually creates the file. If nohup.out already exists, nohup appends to it and leaves its existing security alone.
Exit status #
When nohup runs a command, it exits with that command's status once it finishes. The exception is a failure in nohup itself:
| Code | Meaning |
|---|---|
| (command's own) | The command ran; this is its exit status. |
125 | nohup could not set things up — it could not detach from the terminal, or could not open nohup.out anywhere. The command was not run. |
nice
Peios / Using Peios / Peiosutils / System and processes
nice runs a command at an adjusted scheduling priority — making it more, or less, willing to give the processor up to other work.
nice [options] [command [arg]...]
$ nice -n 15 big-batch-job
With no command, nice prints the current niceness.
Niceness #
A process has a niceness — a number from -20 to 19:
- A high niceness (toward 19) means the process is "nicer" to others — it yields the processor readily. Good for background work that should not get in the way.
- A low niceness (toward -20) means the process is favoured — it gets the processor more often.
By default nice adds 10 to the niceness, making the command run in the background more gracefully.
| Option | Effect |
|---|---|
-n, --adjustment=N | Add N to the niceness instead of 10. N may be negative. |
Raising a command's niceness — running it lower priority — is always allowed. Lowering the niceness, to claim a higher priority than normal, is a privileged request and may be refused; when it is, nice warns and runs the command at the priority it was permitted.
Exit status #
When nice runs a command, it exits with that command's status. The exception is a failure in nice itself:
| Code | Meaning |
|---|---|
| (command's own) | The command ran; this is its exit status. |
0 | No command was given; the niceness was printed. |
125 | nice itself failed — for example, a bad adjustment value. |
126 | The command was found but could not be run. |
127 | The command was not found. |
kill
Peios / Using Peios / Peiosutils / System and processes
kill sends a signal to a process. Despite the name, a signal is not always fatal — it is a message, and some signals ask a process to do something other than stop.
kill [options] pid...
$ kill 4821 # ask process 4821 to terminate
$ kill -KILL 4821 # force it to stop
A pid is a process identifier — the number a running process is known by.
Signals #
With no option, kill sends TERM — a polite request to shut down, which a well-behaved process honours by cleaning up and exiting. The signals you will use most:
| Signal | Meaning |
|---|---|
TERM | Terminate. The default — a request to stop cleanly. |
KILL | Stop immediately. Cannot be caught or ignored, so it cannot be refused — but the process gets no chance to clean up. The last resort. |
HUP | Hang up. Conventionally tells a long-running service to reload its configuration. |
INT | Interrupt — the signal a terminal sends on Ctrl-C. |
STOP / CONT | Suspend the process / resume a suspended one. |
| Option | Effect |
|---|---|
-s, --signal=SIGNAL | Send SIGNAL instead of TERM. |
-SIGNAL | A shorthand — -KILL or -9 both send KILL. |
-l, --list | List the signal names. |
-L, --table | List the signals as a table of numbers and names. |
Use KILL only when TERM has failed. A process killed outright cannot save its work or release what it holds.
A -N that is not a known signal number is an error ("invalid signal"). It is never reinterpreted as a negative process ID.
Exit status #
| Code | Meaning |
|---|---|
0 | Every signal was sent. |
1 | A signal could not be sent — a process that does not exist, or one you are not permitted to signal. |
uname
Peios / Using Peios / Peiosutils / System and processes
uname prints information about the system you are running on.
uname [options]
$ uname
Peios
$ uname -a
Peios host-01 1.4.0 #1 SMP 2026-05-10 x86_64 Peios
With no option, uname prints the operating-system name — Peios.
What each option prints #
| Option | Prints |
|---|---|
-s | The kernel name. |
-o | The operating-system name — Peios. |
-n | The node name — the name this system is known by on the network. |
-r | The kernel release. |
-v | The kernel version. |
-m | The machine's hardware name (its architecture). |
-p | The processor type. |
-i | The hardware platform. |
-a | Everything — equivalent to -mnrsvo. |
Give several options and uname prints those fields, in a fixed order, on one line.
The operating system is Peios #
uname -o, and the operating-system field of uname -a, report Peios. This is the field a script should check when it wants to confirm what system it is on.
For the machine architecture alone, arch is the shorter command — it prints exactly what uname -m prints.
Exit status #
| Code | Meaning |
|---|---|
0 | The requested information was printed. |
1 | The system information could not be read. |
arch
Peios / Using Peios / Peiosutils / System and processes
arch prints the machine's hardware architecture — the kind of processor the system runs on.
arch
$ arch
x86_64
That is the whole command. It takes no options and prints a single name.
arch prints exactly what uname -m prints. It exists as a short, obvious name for the one question "what architecture is this?" — convenient in a script that picks an architecture-specific path.
Exit status #
| Code | Meaning |
|---|---|
0 | The architecture was printed. |
1 | The architecture could not be determined. |
hostname
Peios / Using Peios / Peiosutils / System and processes
hostname displays the system's host name — the name the system is known by.
hostname [options] [name]
$ hostname
host-01.example.com
What is displayed #
By default hostname prints the fully qualified name. These options narrow or change what is shown:
| Option | Prints |
|---|---|
-f, --fqdn | The fully qualified domain name — the full host.domain form. This is the default. |
-s, --short | The short host name only — the part before the first dot. |
-d, --domain | The DNS domain part only. |
-i, --ip-address | The network address (or addresses) the host name resolves to. |
Setting the host name #
Given a name argument, hostname sets the system's host name to it.
$ hostname host-02
Changing the host name changes state for the whole system, so it is a privileged operation — it succeeds only for a caller whose token carries the right to do it, and is refused otherwise. See Privileges.
Exit status #
| Code | Meaning |
|---|---|
0 | The host name was displayed or set. |
1 | A failure — the name could not be read, set, or resolved. |
hostid
Peios / Using Peios / Peiosutils / System and processes
hostid prints the host's numeric identifier, in hexadecimal.
hostid
$ hostid
007f0100
The host id is a number associated with the system. It takes no options and prints one value.
Where hostname gives the system's name — meant to be read by people — hostid gives a numeric value, used by software that wants a machine identifier in a fixed numeric form.
Exit status #
| Code | Meaning |
|---|---|
0 | The host id was printed. |
nproc
Peios / Using Peios / Peiosutils / System and processes
nproc prints the number of processor cores available.
nproc [options]
$ nproc
8
By default it prints the number of cores available to the current process — which can be fewer than the machine has, if the process has been restricted to a subset. It is most often used to decide how many parallel jobs to run.
Options #
| Option | Effect |
|---|---|
--all | Print the number of cores the whole system has, ignoring any restriction on the current process. |
--ignore=N | Subtract N from the count — for leaving some cores free rather than using every one. |
The environment variables OMP_NUM_THREADS and OMP_THREAD_LIMIT, if set, also bound the number nproc reports.
Exit status #
| Code | Meaning |
|---|---|
0 | The count was printed. |
1 | An option value was invalid. |
uptime
Peios / Using Peios / Peiosutils / System and processes
uptime shows how long the system has been running, along with a few related figures.
uptime [options]
$ uptime
14:32:08 up 6 days, 3:21, load average: 0.18, 0.24, 0.21
The default line packs in three things:
- the current time;
- how long the system has been up;
- the load average — a rough measure of how busy the system has been over the last 1, 5, and 15 minutes.
If you know uptime from other systems you may be expecting a count of logged-in users between the uptime and the load average. Peios does not print one. That count comes from a login database (utmp) that Peios does not keep, so the field is left out rather than always reporting 0 users.
To inspect Peios logon sessions and their processes, use logonse. Logon sessions record authentication events, including service and batch logons; their count is not a count of logged-in users.
Options #
| Option | Effect |
|---|---|
-p, --pretty | Show just the uptime, in a readable phrase — up 6 days, 3 hours, 21 minutes. |
-s, --since | Show the date and time the system started, rather than how long ago that was. |
Exit status #
| Code | Meaning |
|---|---|
0 | The uptime was printed. |
1 | The boot time could not be read. |
sync
Peios / Using Peios / Peiosutils / System and processes
sync flushes cached writes to persistent storage.
sync [options] [file...]
$ sync
When a program writes to a file, the data does not always reach the physical device immediately — for speed, the system holds recent writes in memory and writes them out a little later. sync forces that pending data out now, so that what is on the device matches what programs have written.
It is the command to run before doing something that should not lose recent writes — before powering off by hand, or before removing storage.
Options #
With no argument, sync flushes everything. The options narrow it:
| Option | Effect |
|---|---|
file... | Flush only the given files, rather than everything. |
-d, --data | For the given files, flush only the file data, not metadata that does not need it. |
-f, --file-system | Flush the entire file system that contains each given file. |
Exit status #
| Code | Meaning |
|---|---|
0 | The flush completed. |
1 | A named file could not be reached. |
tty
Peios / Using Peios / Peiosutils / System and processes
tty prints the name of the terminal connected to standard input.
tty [options]
$ tty
/dev/pts/3
If standard input is not a terminal — because it has been redirected from a file or a pipe — tty prints not a tty instead.
That second behaviour is the useful one: a script can run tty to find out whether it is being run interactively or as part of a pipeline, and behave accordingly.
Options #
| Option | Effect |
|---|---|
-s, --silent | Print nothing; report the answer only through the exit status. |
Exit status #
| Code | Meaning |
|---|---|
0 | Standard input is a terminal. |
1 | Standard input is not a terminal. |
2 | A usage error. |
stty
Peios / Using Peios / Peiosutils / System and processes
stty displays and changes the settings of a terminal — how it handles input, output, and special keys.
stty [options] [setting...]
$ stty
speed 38400 baud; line = 0;
-brkint -imaxbel iutf8
Run with no arguments, stty prints the settings that differ from the usual defaults. Given setting arguments, it changes them.
Viewing the settings #
| Option | Effect |
|---|---|
-a, --all | Print every setting, in a readable layout. |
-g, --save | Print every setting in a single compact line — a form that can be fed straight back to stty to restore them. |
The -g form is how you save and restore a terminal's state:
$ saved=$(stty -g) # capture the current state
$ stty -echo # ...change something...
$ stty "$saved" # restore exactly what was saved
Changing the settings #
A setting argument turns something on or off, or assigns a value. A few common ones:
| Setting | Effect |
|---|---|
echo / -echo | Show / hide typed characters — -echo is how a password prompt hides input. |
rows N / cols N | Tell the terminal its size. |
| A number | Set the connection speed (baud rate). |
stty has a large catalog of settings; stty -a shows them all with their current values.
Choosing the terminal #
| Option | Effect |
|---|---|
-F, --file=DEVICE | Operate on DEVICE instead of the terminal on standard input. |
Exit status #
| Code | Meaning |
|---|---|
0 | The settings were printed or changed. |
1 | A setting was invalid, or the terminal could not be accessed. |
stdbuf
Peios / Using Peios / Peiosutils / System and processes
stdbuf runs a command with altered buffering on its standard streams.
stdbuf [options] command...
$ slow-producer | stdbuf -oL grep error
What buffering is, and why change it #
A program usually does not write each piece of output the instant it is produced — it collects output in a buffer and writes it in batches, which is faster. The cost is delay: when a program's output feeds a pipe, that batching can hold lines back for a long time, which is unhelpful when you are watching a pipeline live.
stdbuf lets you override that batching for a command, so its output appears sooner.
Setting the buffering #
| Option | Stream |
|---|---|
-i, --input=MODE | Standard input. |
-o, --output=MODE | Standard output. |
-e, --error=MODE | Standard error. |
MODE is one of:
| MODE | Buffering |
|---|---|
0 | Unbuffered — every write goes out immediately. |
L | Line-buffered — output is flushed at the end of each line. Not valid for input. |
| a size | Fully buffered with a buffer of that many bytes. Accepts suffixes — K, M, and so on. |
-oL — line-buffered output — is the common case: it makes a command in a pipeline emit each line as it is finished.
How it works #
stdbuf works by preloading a small helper library, libstdbuf.so, into the command, so the C library's buffering defaults are changed before the command's main runs. The library is installed with the utilities at /usr/libexec/peiosutils/libstdbuf.so; stdbuf looks for it beside its own executable first and in that directory second, and refuses to run the command if it is missing rather than run it unbuffered. stdbuf then waits for the command and exits with its status.
Nothing is written anywhere at run time. Earlier builds embedded the library and extracted it to $TMPDIR on every invocation, which is where two upstream security advisories lived; the shipped build no longer does that.
A limitation #
stdbuf adjusts the default buffering. A command that manages its own stream buffering will override what stdbuf sets, and some commands do not use buffered streams at all — for those, stdbuf has no effect.
Exit status #
When stdbuf runs a command, it exits with that command's status. The exception is a failure in stdbuf itself:
| Code | Meaning |
|---|---|
| (command's own) | The command ran; this is its exit status. If the command was killed by a signal, stdbuf exits with 128 plus the signal number, as a shell would. |
125 | stdbuf itself failed, for example an unusable TMPDIR. |
126 / 127 | The command could not be run / could not be found. |
chroot
Peios / Using Peios / Peiosutils / System and processes
chroot runs a command with a different directory as its root — its /. Inside that command, the chosen directory is the top of the file system, and nothing above it can be named or reached.
chroot newroot [command [arg]...]
$ chroot /mnt/system /bin/sh
With a command, chroot changes the root to newroot and runs the command there. With no command, it starts an interactive shell.
What it does #
A process normally sees the whole file system, rooted at the real /. After chroot, the named directory becomes that process's /: a path like /etc/config inside the command resolves to newroot/etc/config, and there is no path that reaches outside newroot at all.
It is used to work inside another installed system — repairing or configuring a system image by entering it — and to run a command confined to a known subtree.
For the command to be usable, newroot must already contain what the command needs: the command's own executable, and whatever libraries and files it depends on, all present under newroot.
Options #
| Option | Effect |
|---|---|
--skip-chdir | Do not change the working directory to / after changing the root. Permitted only when newroot is the current /. |
A privileged operation #
Changing a process's root directory is a privileged operation. chroot succeeds only for a caller whose token carries the right to do it, and is refused otherwise — the ability to redraw a process's view of the file system is not something every principal holds. See Privileges.
Exit status #
chroot exits with the command's own status once it finishes. Otherwise:
| Code | Meaning |
|---|---|
125 | chroot itself failed — for example, newroot is not a directory. |
126 | The command was found but could not be run. |
127 | The command was not found. |
whoami
Peios / Using Peios / Peiosutils / System and processes
whoami prints the name of the user the calling process is running as.
whoami
$ whoami
jack
It takes no arguments. It is the same answer as id -un, which is all it has ever been.
Where the name comes from #
The name is resolved from the process's effective user ID, which is a projection of the token's user SID. The lookup goes to the authority — there is no /etc/passwd to read — and comes back as the principal's canonical name. Resolving names is the full picture.
The name is the principal's; the number it was looked up by is not what grants anything. If you want the identity access is actually decided against, id -Z prints the SID.
Exit status #
| Code | Meaning |
|---|---|
0 | The name was printed. |
1 | The user ID could not be resolved to a name. |
A failure here usually means the authority could not be reached rather than that the account is missing — identity does not resolve before the authority is running, which is the correct answer rather than a gap.
id
Peios / Using Peios / Peiosutils / System and processes
id prints who a process is: its user, its primary group, the groups it belongs to, and its security context.
id [options] [USER]...
$ id
uid=1001000(jack) gid=101(Authenticated Users) groups=101(Authenticated Users),100(Everyone),102(Administrators),107(Local) context=S-1-5-21-1004336348-1177238915-682003330-1000:High
With no argument it reports the calling process. Given one or more users, it reports on those instead.
The numbers are projections #
This is the thing to understand before reading id's output on Peios.
The uid= and gid= numbers are projections of the token's SIDs. They are real, stable, and exactly what Linux programs see and what filesystems store — but nothing is decided by them. Access is decided against the SID the number was projected from, by the access check. A number here grants nothing.
That distinction is not academic, because the projection cannot carry everything a token holds:
- Groups whose SIDs are deliberately unnumbered —
Interactive,Remote Interactive,Network,Batch,Service— cannot appear at all. They are what make a rule like "network logons cap at Low" expressible, and they have no gid by design. - A group's attributes have nowhere to go. A deny-only group, which can block access through a deny entry but grant nothing through an allow entry, appears in
groups=looking exactly like an ordinary membership. - The integrity level and the logon session have no number either.
So two tokens can project to identical numbers and mean different things. The clearest case is a filtered token: it carries the same user SID and the same group numbers as the token it came from, and differs only in attributes the projection cannot express.
That is why the default line ends with a context= field.
The context field #
context= is the authoritative half: the user SID access is actually decided against, and the integrity level.
context=S-1-5-21-1004336348-1177238915-682003330-1000:High
The integrity level is part of it precisely because the SID alone does not separate an elevated token from its filtered counterpart — both carry the same user SID, and the level is what differs.
If you know id from another Unix, this is the field that carries the SELinux context there. Peios has no SELinux; its security context is the token, so that is what the field holds.
context= is omitted when a user is named rather than the calling process — a named user has no token here to read — and when POSIXLY_CORRECT is set, so a caller that asked for the POSIX shape still gets it.
For the whole token rather than this summary, use token.
Options #
| Option | Effect |
|---|---|
-u, --user | Print only the effective user ID. |
-g, --group | Print only the effective group ID. |
-G, --groups | Print only the group IDs, space-separated. |
-n, --name | With -u, -g or -G, print names instead of numbers. |
-r, --real | With -u, -g or -G, print the real ID rather than the effective one. |
-z, --zero | Separate entries with NUL rather than whitespace. Not allowed in the default format. |
-Z, --context | Print only the security context. |
-p | A readable, multi-line form. |
-P | Print as a password-file entry. |
-a | Ignored; accepted for compatibility. |
-A | Not applicable on Peios — see below. |
-n and -r need one of -u, -g or -G; they do not change the default format.
-r means something different here #
On other Unixes the real/effective split is the setuid boundary. On Peios a setuid bit does not change a token, so that boundary does not exist. What the split tracks instead is impersonation: the real IDs come from the process's own token, the effective ones from whatever token is in force.
id never impersonates, so in practice it reports the same either way and the euid=/egid= fields never appear. That is not a fault — there is genuinely nothing differing to report.
-A is not applicable #
-A reports BSD audit session properties. Peios audits through its own event system and keeps no BSM session, so -A says so rather than silently succeeding at nothing.
id -G and id -G <yourself> differ #
Asking about yourself and asking about your name are two different questions here, and they give different answers:
$ id -G → 101 100 102 107
$ id -G jack → 101 102
id -Greads your process's credential — the projection of your token's group set, which includes the groups the authority staples on when the token is minted:EveryoneandLocal, which are on every token.id -G jacklooks the name up through the principal store, which returns recorded memberships only. The stapled groups are not recorded anywhere, because membership in them is a rule rather than a stored fact — so they do not come back.
On other systems these two differ only in the order they print. Here the second list is genuinely shorter, and neither is wrong.
The order follows GNU: id -G lists the real group first, then the effective group if it differs, then the rest; the groups= field of the default output leads with the effective group.
Exit status #
| Code | Meaning |
|---|---|
0 | The identity was printed. |
1 | A user could not be found, an option combination was rejected, or the context could not be read. |
groups
Peios / Using Peios / Peiosutils / System and processes
groups prints the groups a user belongs to.
groups [USERNAME]...
$ groups
Authenticated Users Everyone Administrators Local
$ groups jack
jack : Authenticated Users Administrators
With no argument it reports the calling process. Given names, it reports on those.
Those two answers are both correct #
The example above is not a mistake, and it is the thing worth knowing about this command on Peios.
groups reads the calling process's credential — the projection of its token's group set. That set includes the groups the authority staples on when the token is minted: Everyone and Local are on every token, whoever you are.
groups jack looks the name up through the principal store, which returns recorded memberships — the ones something actually wrote down.
The stapled groups are not recorded anywhere, because membership in them is a rule, not a stored fact. Everyone is in Everyone; nothing needs to record it, and nothing can enumerate it. So they appear in the first answer and not the second.
On other Unixes these two forms differ only in the order they print.
What cannot appear #
Both forms are lossy in the same way, and unavoidably so — a group can only be listed if it has a group ID, and some deliberately do not:
Interactive,Remote Interactive,Network,BatchandServiceare unnumbered on purpose. They describe how you signed in rather than who you are, which is what lets a rule like "network logons cap at Low" be written at all. You are inInteractiveat the console and not over the network, so there is no static answer to record.- Your logon session SID is minted fresh for each sign-in and has no number either.
- A group's attributes are not representable. A deny-only group — one that can block access through a deny entry but grant nothing through an allow entry — is printed exactly like an ordinary membership.
token show --all is the lossless view, with every SID and its attributes.
Exit status #
| Code | Meaning |
|---|---|
0 | The groups were printed. |
1 | A named user could not be found, or the group list could not be read. |
A group ID with no name is printed as the bare number and sets the exit status, rather than stopping the listing.
logname
Peios / Using Peios / Peiosutils / System and processes
logname prints the login name the calling process runs under.
logname
$ logname
jack
It takes no arguments.
Login name, not current name #
logname and whoami look like the same command and answer slightly different questions.
whoami reports the effective identity — whoever the process is acting as right now. logname reports the login identity: the principal the process's own token is for, regardless of any token it has since taken on. When a process is impersonating a client, whoami follows the impersonation and logname does not.
Most of the time nothing is impersonating and the two agree.
If you know logname from another Unix, note that it answers this from the token rather than from a login-record file. There is no utmp on Peios — nothing writes one — so the traditional source does not exist. The token is the record of who a process is, and it is a better one: it cannot drift from the identity access is actually checked against, because it is that identity.
Exit status #
| Code | Meaning |
|---|---|
0 | The login name was printed. |
1 | No login name could be determined. |
token
Peios / Using Peios / Peiosutils / System and processes
token is the command-line tool for working with tokens directly. It reads a token's contents, adjusts it, produces derived tokens, and drives impersonation — the low-level operations this topic describes, exposed at a shell.
token subcommand [target] [arguments]
$ token # one-line summary of your own token
$ token show --all # every field of your own token
$ token privs --pid 4821 # the privileges on process 4821's token
token is a direct, debug-level tool. Day to day you do not inspect tokens by hand — the system does. token is for diagnosing an access problem, for understanding what identity a process is really running under, and for building and testing identity setups. Run with no subcommand, it prints a one-line summary of your own token.
Choosing which token #
Almost every subcommand operates on a token, and these flags choose which one. With none, the target is your own.
| Flag | Target |
|---|---|
--self | Your own token. The default. |
--real | Your primary token specifically, rather than the effective one — relevant when your thread is impersonating. |
--pid PID | The primary token of process PID. |
--tid TID | The effective token of thread TID: its impersonation token, or the process primary token when not impersonating (used with --pid). |
--peer SOCK_FD | The peer's captured token on a connected socket — see Peer tokens. |
Reading another process's token is itself access-controlled: it succeeds only with the right authority over that process.
Inspecting a token #
show #
token show prints a token's contents. It is the default — bare token is token show --short.
| Flag | Effect |
|---|---|
--short | A one-line summary. |
--all | Every query class — the fullest dump. |
Field accessors #
Each of these prints one part of a token, for when you want just that piece:
| Subcommand | Prints |
|---|---|
user | The user SID — who the token is. |
owner | The default owner SID. |
group | The primary group SID. |
groups | The group list. |
privs | The privileges, with their enabled state. |
caps | The capabilities. |
claims | The user and device claims. |
integrity | The integrity level. |
logon | The logon type and logon SID. |
source | What minted the token. |
origin | The originating session for a derived token. |
stats | Token statistics — IDs, timestamps, the modification counter. |
default-dacl | The token's default DACL. |
token show reports the token's LogonSession LUID as session_id and its separate interactive-environment scope as interactivity_scope.
query #
token query CLASS performs a raw read of a single named token-info class and prints the result as JSON — the lowest-level inspection route, for tooling.
compare #
token compare LEFT RIGHT compares two tokens. Each positional target is a positive PID or self. To select a thread, primary self token, or peer token on the right, omit RIGHT and use the usual target flags:
The table shows two value columns with = or != for each user SID, integrity SID, LogonSession ID, group state and privilege state. SIDs include their friendly names where known. Groups are enabled, disabled, or deny-only; privileges are enabled or disabled. A - means the group or privilege is absent from that token. The table includes entries present in either token.
With --json, header contains user, integrity and session_id; groups and privileges are arrays. Every compared field has left, right and a boolean matches. Missing group or privilege values are JSON null. Group entries also carry a group SID/name object, and privilege entries carry bit and name (null when unknown). Equality uses SID identity and the stated values, independent of friendly-name rendering. Both equal and differing comparisons exit successfully when all queries succeed.
threads #
token threads PID lists threads in ascending TID order, with an impersonating yes/no column and the effective user's SID and friendly name:
The command enumerates /proc/PID/task and uses the normal access-controlled thread-token query. It reports primary-token threads as no, including when several threads have the same user. An impersonating thread remains yes even if its impersonation token has the same user as the primary token.
JSON output has pid, complete, and a threads array. A successful row contains tid, impersonating and user. A thread that disappears or cannot be queried retains its row with an error, null identity fields, and complete: false; the command exits nonzero. Human output shows ? and the error for that row. Directory-enumeration failures also exit nonzero.
Both commands support the normal --raw, --label and --json output choices. Queries are consecutive observations of a running system, not an atomic snapshot of all token fields or threads.
Changing a token #
adjust #
token adjust mutates a token in place:
| Form | Changes |
|---|---|
adjust privs NAME=STATE … | Enable, disable, or remove privileges. STATE is enabled, disabled, or removed. |
adjust groups IDX=STATE … | Enable or disable groups by their list index. |
adjust default --dacl SDDL | Replace the token's default DACL. Also --owner-idx / --group-idx. |
adjust interactivity-scope SCOPE | Replace the token's interactive-environment scope. The legacy name adjust session SCOPE is accepted as an alias; neither form changes the LogonSession auth_id. |
restrict #
token restrict produces a restricted token — a more limited variant of a token.
| Flag | Effect |
|---|---|
--drop-privs MASK|NAMES | Privileges to drop. |
--deny IDX,… | Group indices to mark deny-only. |
--restrict SID,… | The restricting SIDs to apply. |
duplicate #
token duplicate (alias dup) copies a token, optionally changing its --type (primary or impersonation), its impersonation --level, or its --access mask.
link and linked #
token link joins two tokens as an elevation pair — a full token and its filtered counterpart — given their file descriptors and a session id. token linked shows a token's elevation-linked counterpart, if it has one. See Elevation.
Impersonation #
| Subcommand | Effect |
|---|---|
impersonate | Begin impersonating the target token on the calling thread. With a trailing -- command …, run that command under the impersonating token. |
revert | Drop any active impersonation on the calling thread. |
See Impersonation for the model these drive.
Creating tokens #
| Subcommand | Effect |
|---|---|
create SPEC | Create a token from a binary token-spec (SPEC is a file, or - for standard input). |
install SPEC | Create a token from a spec and install it as the caller's primary token. |
Creating and installing tokens is a privileged operation, reserved for the components that legitimately mint identity.
Output options #
| Flag | Effect |
|---|---|
--raw | Render SIDs in raw S-1-… form only. |
--label | Render SIDs as their labels where known, falling back to raw. |
--json | Emit JSON instead of human-readable output. |
token and the inspection surfaces #
token is the convenient front-end. Underneath, it reads the same kernel surfaces and rules described in Inspecting tokens — that page covers the query mechanism, the access rules for reading another process's token, and what cannot be inspected.
Exit status #
| Code | Meaning |
|---|---|
0 | The operation succeeded. |
1 | A usage error. |
| non-zero | The operation failed — no such target, an access denial, or a bad spec. |
logonse
Peios / Using Peios / Peiosutils / System and processes
logonse is the command-line tool for logon sessions — the kernel's records of authentication events that this topic describes. It lists the active sessions, shows which processes belong to one, creates and destroys sessions, signs them out, and (as a related low-level job) shows a process's Process Security Block (PSB) and sets its mitigation flags.
logonse subcommand [arguments]
$ logonse list
$ logonse show 4711
logonse is a low-level administrative and debugging tool. It requires a subcommand: list, show, create, destroy, end, or psb.
Listing sessions #
logonse list #
Lists every live logon session: who it is for, what kind of sign-in made it, the authentication package, when it was made, and the processes in it.
$ logonse list
session 999 Local System (S-1-5-18) service
package: Negotiate
created: when the machine started
pids: [562, 571, 620, 667, 670]
session 1007 jack (S-1-5-21-…-1000) remote-interactive
package: lpsd
created: 2026-10-04 10:02:41
pids: [2611, 2633, 2650]
logonse show #
Shows one session, the same way.
$ logonse show 1007
Where the answers come from #
The sessions come from the kernel's own list, /sys/kernel/security/kacs/sessions, which only BUILTIN\Administrators and SYSTEM may read. It lists every session, including one that has no process — held alive only by a token file descriptor somewhere, or made and not yet used — and such a session shows pids: none you can see.
The processes come from walking the running processes and reading each one's token (/proc/<pid>/token) to find which session it belongs to. That shows only the processes you may inspect: an administrator sees every process but the protected ones, such as peinit and authd, which no administrator can inspect. Processes start and exit while the walk runs, so it is a close approximation of the moment, not a locked one.
Without Administrators, the kernel's list is refused, and logonse says so and shows only the sessions of processes you can inspect — your own.
Creating and destroying sessions #
logonse create #
Creates a new logon session for a user, described by a logon type, an authentication-package name, and the user's SID.
$ logonse create --logon-type interactive --auth-package Negotiate --user-sid S-1-5-21-...-1001
| Flag | Meaning |
|---|---|
--logon-type TYPE | The kind of logon: interactive, network, batch, service, network-cleartext, new-credentials, or remote-interactive. |
--auth-package STR | The name of the authentication package that vouched for the logon. |
--user-sid SID | The user the session belongs to, as an S-1-… SID or an SDDL alias such as BA. |
On success logonse prints the new session's id. Creating a session is a privileged operation — minting authentication records is reserved for the components that legitimately do so.
logonse destroy #
Destroys a session — but only an empty one, with no tokens still referencing it.
$ logonse destroy 4711
A session with live tokens cannot be destroyed this way; its tokens must go first. See Session lifecycle.
Signing a session out #
logonse end #
Signs a session out: authd ends every process running in it, asking each to end and, after a few seconds, ending any that haven't. Once the last of them has gone, the kernel ends the session.
$ logonse end 1012
ended session 1012: 2 processes ended
You may end your own sessions, and anyone's that the machine's SessionEndSecurity setting allows, which as shipped is Administrators and SYSTEM. A service's session can't be ended this way: it ends when the service is stopped. Nor can the kernel's own sessions, 999 and 998. A refusal says which of these it is. See Ending a session.
| Flag | Meaning |
|---|---|
--check | Only ask whether you may end it, and end nothing. |
If authd couldn't end every process, logonse end says how many still hold the session and exits with status 1.
A process's PSB #
logonse psb #
With only --pid, logonse psb shows a process's Process Security Block: whether it is protected by PIP (Process Integrity Protection, which shields signed system processes from everyone else), which mitigations are on, and the process's GUID, which events carry.
$ logonse psb --pid 1
psb pid=1
pip: protected, Peios TCB
mitigations: none
guid: 3f2c9a1e-6b0d-4c8e-9a41-2d7e5f10b6c3
Anyone the process's descriptor lets query it may read this, which by default is everyone — even for a protected process whose everything else is closed to you.
With --mitigations, it requests those mitigation flags instead of showing the PSB. Read the current state with only --pid before considering a change, and use the application and lifecycle guidance to review the required policy and activation limits.
A request is one-way, activation-backed and all-or-nothing. An authorised caller can still be refused because the target's existing state or platform cannot satisfy a protection. The Kernel TRM documents CFIF as unavailable and CFIB activation as self-only, so a mask containing those flags is not a general-purpose way to harden another process. Do not use ALL as a blanket policy. Check the command's result, then inspect the committed state again; a requested mask alone is not proof of protection.
| Flag | Meaning |
|---|---|
--pid PID | The process to act on. |
--mitigations MASK | The mitigation bitmask to request, in hexadecimal or decimal. Without it, the PSB is shown. |
This subcommand is about process hardening rather than logon sessions — it lives in logonse because both deal with low-level per-process kernel state. For what the mitigation flags mean and how they behave, see Process mitigations.
Output options #
| Flag | Effect |
|---|---|
--json | Emit JSON instead of human-readable output. Accepted by every subcommand. |
Exit status #
| Code | Meaning |
|---|---|
0 | The operation succeeded. |
1 | A usage error, or the operation failed. |
mount
Peios / Using Peios / Peiosutils / Disks and mounts
mount attaches a filesystem to the Peios mount tree. With no operands it instead lists what is currently mounted. It is a faithful reworking of the util-linux mount(8) surface for Peios, with two structural differences: Peios has no /etc/fstab and no /etc/mtab, so every mount is described entirely on the command line and live mount state is read from the kernel; and a mount can apply a KACS mount policy to the new filesystem at attach time (see Mount policies).
mount [-t TYPE] [-o OPTIONS] SOURCE TARGET
mount [-o remount,OPTIONS] TARGET
mount --bind|--rbind|--move SOURCE TARGET
mount --make-shared|--make-private|... TARGET
mount [-l] [-t TYPE]
Internally mount uses the fd-based mount API (fsopen / fsconfig / fsmount / move_mount, plus open_tree and mount_setattr), never the classic single-shot mount(2). This matters in one place you can see: when a mount fails, the kernel's fs_context message log is drained and printed as the reason, even without -v.
Operands and argument shapes #
Because there is no fstab to consult, operand resolution is strict:
- No operands, no verb — list mode (see below).
- Two operands —
SOURCE TARGET. - One operand — an error. In util-linux a lone operand is resolved through fstab; Peios has none, so a single operand cannot be turned into a
SOURCE TARGETpair. Supply both, or use--source/--target. - A lone target is valid only for
-o remountand for a standalone propagation change (--make-*), which act on an existing mount point.
--source SRC and --target DIR name the operands explicitly and may be combined with a single positional. --target-prefix DIR prepends DIR/ to the target after it is chosen. Options may be interspersed with operands (mount SRC -o ro TGT), matching util-linux.
Paths, -o key=value values, labels and UUIDs are handled as opaque byte strings, never assumed to be UTF-8; an embedded NUL is a usage error.
Canonicalisation #
By default source and target paths are canonicalised (made absolute, symlinks resolved). -c / --no-canonicalize disables that; X-mount.nocanonicalize[=source|target] is the -o form and can disable it for just one side. The final path handed to the kernel is protected against a TOCTOU symlink swap on its last component. This does not reject every symlink in the original input: default canonicalisation has already resolved it.
Operation modes (verbs) #
mount performs one of several operations. The structural verbs — bind, rbind, move, beneath and remount — are mutually exclusive with one another. A propagation change is not a structural verb: it may stand alone on a target, or trail another verb or a new mount in the same command (applied afterwards as one or more mount_setattr steps), and several may be combined.
| Verb | How to request it | What it does |
|---|---|---|
| New mount | mount [-t T] SRC TGT | Attach a fresh instance of the filesystem at SRC onto TGT. |
| Bind | -B / --bind, or -o bind | Make an existing subtree visible at a second location. |
| Recursive bind | -R / --rbind, or -o rbind | Bind a subtree together with every mount underneath it. |
| Move | -M / --move, or -o move | Relocate an existing mount to a new mount point. |
| Move beneath | --beneath SRC TGT | Attach SRC beneath the mount currently at TGT (the kernel enforces several constraints; violations surface as exit 32). |
| Remount | -o remount[,...] TGT | Change the options of an existing mount (see Remounting). |
| Propagation | --make-* / --make-r*, or the -o tokens below | Change how mount/unmount events propagate across a subtree. |
| List | mount / mount -l | Print the current mounts. |
Propagation flags #
Each of these sets the propagation type of the mount at the target. The --make-r* (and r-prefixed -o) forms apply recursively to the whole subtree; the recursive form is all-or-nothing (it either applies to the entire subtree or to none of it).
| Flag | -o token | Recursive flag | Recursive -o token | Meaning |
|---|---|---|---|---|
--make-shared | shared | --make-rshared | rshared | Events propagate to and from peer mounts. |
--make-slave | slave | --make-rslave | rslave | Events propagate in from the master but not back out. |
--make-private | private | --make-rprivate | rprivate | No propagation either way. |
--make-unbindable | unbindable | --make-runbindable | runbindable | Private, and cannot be bind-mounted. |
A freshly created mount, bind or move is private by default unless its destination's parent is shared, in which case it joins that peer group — the standard kernel rule.
Source specification #
| Form | Resolution |
|---|---|
Device path (/dev/sda1) | Used directly. |
-L LABEL / LABEL=, -U UUID / UUID=, PARTLABEL=, PARTUUID= | Resolved to a device via libblkid. No match is a mount failure (exit 32). |
| A directory or regular file | Used directly (a bind source or target; a file may be a bind target). |
A pseudo source (tmpfs, proc, sysfs, none, …) | Passed through; -t is required because it cannot be probed. |
| An image file | Attached through a loop device (see Loop devices). |
Filesystem type #
-t TYPE names the type explicitly. -t auto, or omitting -t entirely, asks libblkid to safely probe the source; an ambiguous or multiply-signed device is refused (exit 32) rather than guessed at. X-mount.auto-fstypes=LIST constrains the probe candidates. Type lists and no<type> negation are not accepted in the mounting path (they remain meaningful only as a listing filter).
The -o option language #
-o takes a comma-separated list of key or key=value tokens and is repeatable (all occurrences are joined). A key="..." double-quoted value protects embedded commas; the key=value split is on the first =. Every token is sorted into one of the following categories.
Per-mount attributes #
Applied to the mount itself. Each accepts its negation; ro/rw additionally accept a =vfs / =fs / =recursive scope qualifier (bare is =vfs, non-recursive; =fs targets the superblock read-only flag; =recursive applies to the whole subtree).
| Option | Effect |
|---|---|
ro / rw | Read-only / read-write. |
suid / nosuid | Honour / ignore set-user-ID bits. |
dev / nodev | Allow / disallow device nodes. |
exec / noexec | Allow / disallow execution. |
atime / noatime | Update / never update access times. |
relatime / norelatime | Relative-atime updates on or off. |
strictatime / nostrictatime | Strict-atime updates on or off. |
diratime / nodiratime | Directory access-time updates on or off. |
nosymfollow | Do not follow symlinks on this mount. There is no positive symfollow token — the attribute is cleared only via a remount mask. |
The atime tokens share a single mode field; the last one specified wins.
Superblock flags #
| Option | Effect |
|---|---|
sync / async | Synchronous / asynchronous writes. |
dirsync | Synchronous directory updates. |
lazytime / nolazytime | Lazy on-disk timestamp updates on or off. |
iversion / noiversion | Inode version counting on or off. |
silent / loud | Suppress or emit certain kernel messages. |
mand / nomand | Obsolete (removed from the kernel). Accepted and ignored with a note under -v. |
Filesystem-specific parameters #
Any token not recognised above is forwarded verbatim to the filesystem: key=value as a string parameter, a bare key as a flag. There is no built-in allow-list and no length limit; a rejection by the filesystem is reported with the offending key and the kernel's message.
For example, StrataFS takes its precedence-ordered directory stack through
strata=:
See StrataFS for the stack flags, merge
and write-routing model, and the stratafs inspection command.
Userspace-only tokens #
These never reach the filesystem. They include the meta-verbs remount, bind, rbind, move; the loop controls loop / loop=/dev/loopN, offset=, sizelimit= (numeric values accept K/M/G/T and KiB/MiB/… suffixes); the propagation tokens listed above; and defaults, which expands to rw,suid,dev,exec,async (later tokens override it).
Functional X-mount.* options #
| Option | Effect |
|---|---|
X-mount.mkdir[=mode] | Attempt to create missing target directories, like -m. In the reviewed implementation, the requested mode is not applied. |
X-mount.subdir=DIR | Attach subdirectory DIR of a freshly mounted filesystem at the target. Effective only for a new-instance mount; silently ignored (noted under -v) for bind/move/remount/propagation. |
X-mount.noloop | Suppress the implicit loop device for a regular-file source. |
X-mount.auto-fstypes=LIST | Constrain the -t auto probe to these types. |
X-mount.nocanonicalize[=source|target] | The -o form of -c; with =source or =target it disables canonicalisation for just that path. |
X-mount.idmap and X-mount.owner / group / mode are not supported and are rejected with a usage error: Peios ownership is SID/security-descriptor based, so the fix is to add a SID/ACE to the descriptor rather than remap or chown.
KACS mount policy #
This is the genuinely Peios-specific part of mount. A mount policy is a per-superblock setting that governs how FACS treats the filesystem; the full model is described in Mount policies and its detail pages. mount can set that policy at attach time:
| Option | Policy class |
|---|---|
-o policy=deny-missing | facs_deny_missing — a file with no SD is unreachable. |
-o policy=synth-ephemeral | facs_synthesize_ephemeral — a missing SD is synthesised in memory only. |
-o policy=synth-persist | facs_synthesize_persistent — a missing SD is synthesised and written back. |
--synth-sddl SDDL | Provide the mount-level SD template used during synthesis (only valid with a synth-* policy). |
policy=unmanaged is not user-settable; only the kernel sets the unmanaged class, for its own pseudo-filesystems. policy= is valid only on a new mount of a real filesystem — combining it with bind/move/remount/propagation, or with list mode, is a usage error. See Policy classes for what each class does and SD storage by filesystem for how the SD is physically stored.
The policy is applied to the detached filesystem before it is attached: if setting it fails, that filesystem is not attached with an unintended policy. This is an attachment guarantee, not whole-command rollback: target creation may already have occurred. In kernel source 8e0e22de3a59cad506bbbf8873de456e16ad272d, the policy gate accepts enabled SeManageVolumePrivilege or SeTcbPrivilege and requires successful privilege-use marking. Failure of that gate gives EPERM (exit 1) and no attachment. --synth-sddl is validated client-side first — it must be well-formed SDDL and must include an owner.
Peios applies no coarse uid==0 check anywhere: the mount applet is not installed set-user-ID, and every privileged action is authorised per-operation by KACS.
Remounting #
-o remount changes the options of an existing mount without detaching it. Peios does not perform the util-linux "read the old flags and re-supply them" dance — the fd-based API applies deltas rather than resetting, so each remount touches only what you name. Per-mount attributes (ro/rw, nosuid, atime, …) are changed via mount_setattr; superblock flags, filesystem parameters and ro=fs/rw=fs go through the superblock reconfigure path. =recursive remounts the whole subtree, all-or-nothing.
A bind mount shares its source's superblock, so any superblock-level option on a bind remount (a Category-B flag, a filesystem parameter, or ro=fs/rw=fs) is refused as a usage error rather than silently mutating the shared superblock for every mount of that filesystem. Only per-mount VFS attributes are valid on a bind remount.
Loop devices #
A regular-file source is backed by a loop device automatically when the type is unspecified or the filesystem is recognised by libblkid; X-mount.noloop suppresses this. -o loop forces auto-allocation of a free device, loop=/dev/loopN names one, and offset= / sizelimit= select a region of the file (they imply loop and are an error against a real block device). A backing file already attached at the same offset and size is reused rather than doubly attached, to avoid corruption. Loops created by mount are auto-cleared by the kernel on unmount, so they do not leak; umount -d force-clears the rest (see umount).
Other flags #
| Flag | Effect |
|---|---|
-t, --types TYPE | Filesystem type, or auto. |
-o, --options LIST | Mount options (above); repeatable. |
-r, --ro, --read-only | Mount read-only (-o ro). |
-w, --rw, --read-write | Mount read-write and forbid the automatic read-only fallback on a write-protected device (-o rw). |
--source SRC / --target DIR | Name the operands explicitly. |
--target-prefix DIR | Prepend DIR/ to the target. |
-B, --bind / -R, --rbind / -M, --move / --beneath | The structural verbs. |
--make-*, --make-r* | Propagation changes. |
--exclusive | Force a unique superblock instance (no reuse). Meaningful only for multi-instance filesystems (e.g. tmpfs) or read-only block mounts; on an already-mounted writable block device it fails with EBUSY (exit 32). |
-m, --mkdir[=MODE] | Attempt to create missing target directories. The requested MODE is parsed but not applied at the reviewed source revision. |
-L, --label LABEL / -U, --uuid UUID | Select the source by filesystem label / UUID. |
-c, --no-canonicalize | Do not canonicalise paths. |
-f, --fake | Skip mount and policy syscalls; target preparation still runs. With --mkdir or X-mount.mkdir, directories may be created. See execution limits. |
-v, --verbose | Narrate resolved values and each syscall; drain the kernel fs_context log. Repeatable but -vv is the same as -v. |
-l, --show-labels | In list mode, append each filesystem's label. |
--onlyonce | Accepted and stored, but unused by the reviewed execution path. Do not rely on it to skip an existing mount or make repeat startup safe. |
-N, --namespace NS | Operate inside mount namespace NS (a PID, an ns file path, or a named namespace). Source resolution happens in the caller's namespace; the mount lands in the target namespace. |
-i, --internal-only | Do not invoke a mount.<type> helper. |
-n, --no-mtab | Accepted and ignored (Peios has no mtab). |
--synth-sddl SDDL | KACS synth-policy template SD (above). |
-h, --help / -V, --version | Standard. |
No external mount helpers ship in this version, so a network or FUSE type fails with "no helper for type" (exit 1) — a clean deferral, not a crash.
Source-verified execution limits #
These limits were checked in peiosutils 3344d4690476fd66bfaec99b1ae92190bbcba06f.
They describe that implementation, not a runtime test or a historical release
boundary; check the version in the image you operate.
- No already-mounted guard from
--onlyonce. The request parser stores it, but the executor never consults it. Inspect existing mounts before a repeat operation; this finding does not mean every repeat necessarily succeeds or creates an overmount. --fakecan create targets. Target preparation precedes the new-mount fake return. With either mkdir option it may write directories. It is not a zero-write preview or proof that a real mount's authority and filesystem checks will pass.- MODE is not target security. The creation helper
discards the requested mode and ignores the directory-creation result before
continuing path preparation. Do not rely on
--mkdir=0700for privacy. Establish and verify the target's native security separately; a parsed option is not a verified descriptor. - Failure can follow attachment. New-mount execution sets policy before attachment, but a subsequent propagation step can fail after the mount is attached. Retain diagnostics and inspect the live source, target, filesystem, writable state and policy before retrying. Do not infer an unchanged system from a nonzero exit status.
These gaps matter for startup integration:
--onlyonce is not an idempotence mechanism, and a failed or stopped command
is not by itself a verified detach or recovery procedure.
List mode #
With no operands, mount prints one line per mount, read from /proc/self/mountinfo:
SOURCE on TARGET type FSTYPE (OPTIONS)
mount -t TYPE with no operands filters the list by filesystem type instead of mounting; here type lists (-t ext4,xfs) and no<type> negation (-t nosysfs) are honoured. -l appends [LABEL] to each line when a label is known.
Details of the rendering: the option field is the positional VFS-then-superblock merge with a coalesced ro/rw, not sorted; mountinfo octal escapes are decoded; control characters in the mount point become ?; and the source is shown as the kernel recorded it, with /dev/loopN resolved to its backing file and /dev/dm-N to /dev/mapper/<name>. The listing shows only mounts the caller's token may observe; a partial or empty view is correct, not an error, and the paths of filtered-out parents are never reconstructed.
For a device-oriented view of what is available to mount, use lsblk.
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | Incorrect invocation or a permission denial — bad flags, mutually-exclusive verbs, a lone operand, an invalid policy= or --synth-sddl, an authorisation failure (EPERM/EACCES), an embedded NUL, or "no helper for type". |
2 | System error (out of memory, no free loop device, cannot fork). |
4 | Internal error (an invariant failure). |
8 | Interrupted (SIGINT), after signal-safe cleanup. |
32 | Mount failure — a syscall in the flow failed, a label/UUID had no match, a probe was undetermined, or a --beneath/move/--exclusive kernel refusal. |
126 | An external mount.<type> helper was found but failed to execute (moot until a helper ships). |
Code 16 (mtab) is never produced. Code 64 (some succeeded, some failed) only arises when several source arguments are given and at least one succeeds while another fails.
umount
Peios / Using Peios / Peiosutils / Disks and mounts
umount detaches a filesystem from the Peios mount tree. It is the counterpart of mount and, like it, reads live mount state from the kernel (/proc/self/mountinfo) rather than any /etc/mtab — Peios has none. Each operand is resolved against that live state and unmounted with umount2(2).
umount [-lfRA] [-dr] TARGET|SOURCE...
Operands and how they are resolved #
Each operand is either a mount point or a source:
- If the (canonicalised) operand is itself a mount point in the current mount table, it is unmounted directly. This is always unambiguous.
- Otherwise the operand is treated as a source and matched against the sources in the mount table. If it is mounted in exactly one place, that place is unmounted. If it is mounted in several places,
umountrefuses with an error naming the candidates — resolve it by naming a specific mount point, or use-Ato unmount all of them.
If an operand matches nothing, it is "not mounted": normally an error (exit 32), but -g makes that a success and -q suppresses the message.
Several operands may be given in one invocation, and options may be interspersed with them (umount /a -f /b). Paths are handled as opaque bytes; an embedded NUL is a usage error. By default each operand is canonicalised; -c disables that and additionally selects UMOUNT_NOFOLLOW so the kernel does not follow a symlink in the final component.
Recursive and all-targets unmounts #
Two flags expand a single operand into multiple unmounts. Within one operand the expansion stops on the first failure.
-R/--recursiveunmounts the target and everything mounted underneath it, including any over-mount stack at a single point, ordered deepest-first.-A/--all-targetsunmounts every mount point of the given source in the current mount namespace. This is the live-mountinfo "unmount everywhere" complement to source resolution; it is not an fstab feature.-A -Rtogether compose: for each mount point of the source, recurse underneath it.
-R and -r are mutually exclusive (combining them is a usage error, exit 1).
Flags #
| Flag | Effect |
|---|---|
-l, --lazy | Detach the filesystem now and clean up references later (MNT_DETACH). |
-f, --force | Force the unmount (MNT_FORCE), e.g. for an unreachable server. |
-R, --recursive | Unmount the target and everything under it (see above). |
-A, --all-targets | Unmount every mount point of the given source (see above). |
-d, --detach-loop | After unmounting, free the backing loop device. Best-effort and verified: it clears the device only if it still backs the mount just removed, so a recycled loop number is never clobbered. Usually redundant, since mount-created loops auto-clear on unmount. |
-r, --read-only | If the unmount fails, remount the filesystem read-only instead (via mount_setattr). Mutually exclusive with -R. |
-g, --graceful | Exit 0 when the target is absent or not mounted (rather than failing). Applied unconditionally. |
-q, --quiet | Suppress "not mounted" messages. |
-c, --no-canonicalize | Do not canonicalise paths; selects UMOUNT_NOFOLLOW. Applied unconditionally. |
-v, --verbose | Say what is being unmounted. Repeatable. |
-N, --namespace NS | Enter mount namespace NS (a PID, an ns file path, or a named namespace) before reading its mount table and unmounting. All work happens inside that namespace. |
-n, --no-mtab | Accepted and ignored (no mtab on Peios). |
-i, --internal-only | Do not invoke a umount.<type> helper (none ship). |
--fake | Dry run: resolve operands and report, but skip the unmount syscalls. |
-h, --help / -V, --version | Standard. |
The umount2 flag mapping is direct: -l → MNT_DETACH, -f → MNT_FORCE, -c (or a symlinked final component) → UMOUNT_NOFOLLOW. MNT_EXPIRE is deliberately not exposed, matching util-linux.
On privilege #
As with mount, there is no coarse uid==0 check: the applet is not set-user-ID and every unmount is authorised per-operation by KACS. Where util-linux silently suppresses an option for non-root users (-c, and -g's effectiveness), Peios applies it unconditionally.
Exit status #
| Code | Meaning |
|---|---|
0 | Success (or a -g no-op on an absent target). |
1 | Incorrect invocation or a permission denial — including -R with -r, a missing operand, an ambiguous source, an embedded NUL, or an authorisation failure. |
2 | System error (e.g. cannot read the mount table). |
8 | Interrupted (SIGINT). |
32 | Unmount failed, or the target is not mounted (unless -g). |
64 | Several source/target arguments were given and at least one succeeded while another failed. |
126 | An external umount.<type> helper was found but failed to execute (moot until a helper ships). |
A single -R / -A / -A -R invocation stops on the first failure and yields 32; the aggregate 64 only appears across multiple top-level arguments. To see what is currently mounted before unmounting, use mount with no arguments or lsblk.
lsblk
Peios / Using Peios / Peiosutils / Disks and mounts
lsblk lists the block devices on the system and their relationships — disks, their partitions, and the device-mapper and loop devices layered on top. It is the device-oriented companion to mount: where mount with no arguments shows what is mounted, lsblk shows what is available to mount and what filesystem sits on each device.
lsblk [options] [DEVICE...]
By default it prints a tree, one row per device with children indented beneath their parent. Given one or more DEVICE operands (a name like sda, a full path like /dev/sda, or anything resolving to a device node), it re-roots the output at those devices.
Where the data comes from #
Each column is sourced from one of four places, and any that cannot be read degrades to an empty cell rather than aborting the run:
- sysfs (
/sys/block) — the device tree, sizes, and the topology/hardware columns. Peios leaves/sys/blockunpatched, so this is the standard no-udev path. - libblkid — filesystem identity:
FSTYPE,FSVER,UUID,LABEL, and the partition-table columns (PTTYPE,PTUUID,PARTTYPE,PARTLABEL,PARTUUID). libblkid is opened at runtime, and it reads the device itself, so these columns are empty for anyone who may not open it: as shipped, a disk's node lets only SYSTEM and Administrators open it. - The device node's security descriptor —
OWNERandMODE, read the same wayls -lreads them. - The
/dev/disk/by-*symlink farm —ID-LINK. Populated by the device manager once it has run; there is deliberately no/run/udev/dataparser, so the column reads the links themselves and is empty in the initramfs, where no device manager runs.
Output modes #
The mode selects how the rows are formatted; it does not change which columns are shown.
| Flag | Mode |
|---|---|
| (default) | Indented tree. |
-l, --list | Flat list — the same columns, no tree glyphs. |
-J, --json | JSON. Flag columns render as bare booleans and MOUNTPOINTS as an array; children nest under a children key. |
-P, --pairs | KEY="value" pairs, one device per line. |
-r, --raw | Raw, space-separated. Values that could contain a space or control character are hex-escaped (\xNN) so fields stay parseable. |
-T, --tree[=COLUMN] | Force tree output even alongside -l, optionally attaching the tree glyphs to COLUMN instead of NAME. |
Columns #
With no column flag, lsblk prints the default set: NAME, MAJ:MIN, RM, SIZE, RO, TYPE, MOUNTPOINTS. Three flags swap in a preset, and -o names an explicit list (which wins over all of them):
| Flag | Column set |
|---|---|
-o, --output LIST | Exactly the comma-separated columns named (case-insensitive; an unknown name is a usage error). |
-O, --output-all | Every available column. |
-f, --fs | Filesystem view: NAME, FSTYPE, FSVER, LABEL, UUID, MOUNTPOINTS. |
-m, --perms | Permissions view: NAME, SIZE, OWNER, MODE. |
The available columns are NAME, KNAME, PATH, MAJ:MIN, FSTYPE, FSVER, LABEL, UUID, PTUUID, PTTYPE, PARTTYPE, PARTLABEL, PARTUUID, MOUNTPOINT, MOUNTPOINTS, SIZE, RO, RM, HOTPLUG, TYPE, OWNER, MODE, MODEL, VENDOR, REV, SERIAL, TRAN, HCTL, ALIGNMENT, MIN-IO, OPT-IO, PHY-SEC, LOG-SEC, STATE, ROTA, SCHED, PKNAME, and ID-LINK.
The OWNER and MODE columns #
OWNER and MODE describe the device node, not the filesystem on it, and they mirror ls -l exactly — there is no GROUP column and no POSIX permission bits, because access to a device node is governed by its security descriptor, not by a mode. OWNER is the owner SID (S-1-…); MODE is a three-character [type][x][+]: the device-type character (b for a block device, c for a character device), an x slot (never set for a block device), and a + when the DACL is inheritance-protected. When the SD cannot be read — for instance on a non-Peios host with no KACS syscalls — OWNER shows ? and MODE degrades honestly.
Filtering, sorting and shaping #
| Flag | Effect |
|---|---|
-a, --all | Include empty (zero-size) devices, which are hidden by default. |
-d, --nodeps | Do not print a device's holders or slaves (drop the children). |
-I, --include LIST | Show only devices with these major numbers (comma-separated). |
-e, --exclude LIST | Exclude devices by major number. The default is 1 (RAM disks); an explicit -e replaces that default, and -a clears exclusions entirely. |
-s, --inverse | Print dependencies in inverse order (holders above the devices they depend on). |
-M, --merge | Collapse a subtree shared by several parents (a multipath device) to a single occurrence. |
-E, --dedup COLUMN | Drop rows whose COLUMN value duplicates an earlier one. |
-x, --sort COLUMN | Sort siblings by COLUMN (numeric columns sort by value, not text). |
Formatting #
| Flag | Effect |
|---|---|
-b, --bytes | Print SIZE as an exact byte count instead of a human-readable value. |
-p, --paths | Print full /dev paths in the NAME column. |
-n, --noheadings | Omit the header row. |
-i, --ascii | Draw the tree with ASCII characters instead of box-drawing glyphs. |
-y, --shell | Render column keys shell-safe (MAJ:MIN becomes MAJ_MIN). |
-w, --width NUM | Truncate each table row to NUM columns wide. |
--sysroot DIR | Read sysfs, the mount table and /dev from DIR instead of / (chiefly for testing against a fixture). |
-h, --help / -V, --version | Standard. |
Exit status #
| Code | Meaning |
|---|---|
0 | Success. |
1 | Incorrect invocation (an unknown column, a malformed major-number or width value) or a failed run (for example, /sys/block is unreadable). |
lsblk uses this single non-zero code, matching util-linux. A per-device read failure is not fatal: it degrades the affected columns to empty or ? and the run continues. A broken output pipe (piping into head, for instance) is treated as success.
mkirf
Peios / Using Peios / Peiosutils / Boot images
mkirf compiles an initramfs source tree into an initramfs image. The source tree is an ordinary directory — on a running Peios system that directory is /boot/initramfs/ — whose contents map 1:1 onto / inside the initramfs. The image is a single deterministic, compressed newc cpio archive — zstd by default, --compress gzip for a kernel built without zstd support — that the bootloader loads into memory alongside the kernel.
mkirf [--watch] [--debounce SECS] [--exclude GLOB]... [--compress ALGO] <src-dir> <out-file>
This page is the command reference. For what the initramfs stage is — prelude, the /boot/initramfs/ layout, and the handoff to the real root — see The initramfs stage; for how hooks declare their order, see Boot hooks. mkirf reads <src-dir> and writes <out-file>; it never writes back into the source tree, so the directory you inspect is always exactly what packages and you have put there.
What a build does #
Given a source tree, one build runs a fixed sequence:
-
Validate the layout.
<src-dir>must be a directory, must carry an executableinit(the initramfs PID 1 — a symlink is fine as long as it resolves within the tree), and must not already contain a hook sequence —hooks.seqorsystem/prelude/hooks.seq.<n>— whichmkirfgenerates. A missing or non-executableinit, or a stray sequence file, stops the build. -
Walk the tree. Every object beneath
<src-dir>becomes a cpio entry — regular files (with their executable bit preserved), directories, symlinks (target stored verbatim), character/block device nodes, and FIFOs. Sockets, and any other unsupported type, are rejected. Entries are sorted inLC_ALL=Cbyte order, which places every directory before its descendants — the order the kernel's unpacker requires. -
Resolve the hook order. Hooks are the regular files directly under
<src-dir>/usr/libexec/prelude/hooks.d/(packaged) or<src-dir>/lcl/libexec/prelude/hooks.d/(operator-placed), with the operator's copy winning when both hold the same file name. The legacy<src-dir>/hooks/is still read, and each hook found there is reported with the directory it should move to.mkirfparses each hook's# /// hookmetadata block, topologically sorts the resulting capability DAG, and bakes the execution order into generated manifests injected into the image at/system/prelude/hooks.seq.<n>. A tree with none of those three directories, or with nothing in them, simply has no hooks — the manifests are still written, empty, because prelude treats a missing sequence as a broken image rather than as "nothing to run".The version is in the file name, and
mkirfwrites every version it can express faithfully — currentlyhooks.seq.1(the flat resolved order) andhooks.seq.2(that order plus each hook's declarations).mkirfships inpeiosutilsand prelude ships in its own package, so an image can pair a newer writer with an older reader; naming the versions separately lets one image satisfy both, which a single file carrying an in-band version cannot. A version is dropped only when a future format carries something it can no longer project honestly.They live under
/system— the tier for derived content, alongsidesystem/retc— rather than/usr, which is the vendor tier no package ships these into, or/var/state, which is for mutable state rather than immutable build output. See Validation and errors below. -
Write atomically. The archive is written to a sibling temp file and
renamed into place, so a failed run never leaves a half-written image behind. On successmkirfprints the path, entry count, and byte size to stderr.
Early (pre-decompression) segments #
A reserved <src-dir>/++/ directory, if present, holds early initramfs segments — content the kernel consumes before it decompresses the main archive, namely CPU microcode and ACPI table overrides. Each immediate child of ++/ is one segment whose own contents map onto the cpio root (++/microcode/kernel/x86/microcode/GenuineIntel.bin becomes kernel/x86/microcode/GenuineIntel.bin); the segment directory name is a human label and never appears in the archive. Every ++/ entry must itself be a directory. The segments are merged into one uncompressed cpio prepended ahead of the compressed main archive. With no ++/ directory the output is exactly a single compressed member. mkirf stays format-agnostic here: it knows "early segments", never "microcode".
The deterministic-build guarantee #
The same source tree always compiles to the same image, byte for byte. This is what makes "did anything actually change?" a meaningful question and underpins later work such as signed boot artifacts. Determinism comes from normalising everything that would otherwise vary between builds:
- Ownership, inode numbers, and timestamps are all emitted as zero.
- Permission bits are normalised. The source tree is authoritative for file type and, for regular files, executability — nothing else. Read/write permission bits are not Peios's access mechanism, so they are flattened to a constant: directories
0755, symlinks0777, FIFOs and device nodes0644, regular files0755if executable and0644otherwise. - The compressed member uses a fixed compressor at a fixed level: zstd level 3 by default (on a real image it compresses ~9x faster than gzip to a slightly smaller archive, and the kernel decompresses it ~6x faster at boot), or gzip level 6 with mtime 0 and no embedded filename (the
gzip -nequivalent) under--compress gzip. - The early region is byte-stable, with file payloads padded to a 16-byte boundary by widening the preceding entry's name field with NUL bytes.
Validation and errors #
mkirf will not produce an image from a source tree that cannot boot. A misconfiguration is caught when the image is built — on a running system where the message is easy to read — rather than as a mystery failure at the next boot. The checks:
- Layout.
<src-dir>is not a directory; noinit, orinitis not a regular file, or is not executable; a pre-existing hook sequence; a++entry that is not a directory; two++segments defining the same file. (Exit 1.) - Hooks. A malformed
# /// hookmetadata block (unclosed, a non-comment line inside it, a duplicate or unknown key, a badly-formed array), a dependency cycle, arequiresthat nothing supplies, or a capability that is both provided and contributed to (a capability is either a set of alternatives or a set of contributors, never both). Anafternaming a capability nothing supplies is not an error — that is what distinguishes it fromrequires. (Exit 1.) A hook with no metadata block at all is not an error — it is the escape hatch: it is scheduled last, in name order, andmkirfprints a warning so a forgotten block is visible. - I/O. Any filesystem read, write, or compression failure. (Exit 1.)
- Usage. A semantic invocation error clap cannot express — currently,
--watchwith<out-file>inside<src-dir>(see below). (Exit 2.)
Watch mode #
With --watch, mkirf stays resident: it builds once up front (so the watch never begins from a stale archive), then watches <src-dir> recursively and rebuilds after every change, debounced. This is what lets /boot/initramfs/ behave like an ordinary part of the filesystem — edit a hook and the initramfs is current again — rather than a build artifact an administrator has to remember to regenerate.
Watch mode is a foreground loop that runs until killed; supervising and restarting it is a service manager's job, not mkirf's. A rebuild that fails (say, a hook edited into a dependency cycle) is logged but does not stop the watch, so fixing the offending file recovers on the next change.
<out-file> must not live inside <src-dir>: each rebuild would write the image back into the watched tree and retrigger the watch endlessly. mkirf refuses this invocation up front (exit 2). The --debounce window (default 5 seconds) is the settle time — mkirf waits for the tree to be quiet for that long before rebuilding, so a burst of edits produces one rebuild rather than many.
Options #
| Option | Effect |
|---|---|
--watch | Stay resident and rebuild on every change to <src-dir>, after an initial build. Runs until killed. |
--debounce SECS | With --watch, the settle time before a rebuild. Default 5. A non-numeric value is a usage error. |
--exclude GLOB | Exclude paths (relative to <src-dir>) matching GLOB from the image. Repeatable. * and ? stay within a path segment; ** crosses separators. A matched directory is pruned with its whole subtree. A malformed glob is a usage error. |
The two positionals are both required:
| Positional | Meaning |
|---|---|
<src-dir> | The source tree; its contents map onto / in the initramfs. |
<out-file> | The output cpio.gz archive. |
Exit status #
| Code | Meaning |
|---|---|
0 | The image was built (or --help/--version was printed). |
1 | An operational failure — an invalid layout, an unresolvable hook set, or an I/O/compression error. |
2 | A usage error — a bad option, a missing positional, or --watch with <out-file> inside <src-dir>. |
See also #
- The initramfs stage — what
mkirf's output is for. - Boot hooks — the
# /// hookmetadata block and how order is resolved. - mkuki — wraps
mkirf's image, together with a kernel and command line, into a bootable UEFI unified kernel image.
mkuki
Peios / Using Peios / Peiosutils / Boot images
mkuki builds a UEFI unified kernel image (UKI): it takes a PE/COFF EFI stub and appends the kernel, the initramfs, and the kernel command line to it as named PE sections, producing a single EFI binary that UEFI firmware boots directly. Where mkirf produces the initramfs image, mkuki is the step that wraps that image — together with a kernel and a command line — into one bootable artifact. The two are the two halves of Peios' Dynamic Boot system.
mkuki (--kernel PATH | --kernel-dir PATH) --initramfs PATH
(--cmdline TEXT | --cmdline-file PATH) --out PATH
[--stub PATH] [--watch] [--debounce SECS]
mkuki --stub-info
The UKI section layout #
A UKI is an ordinary PE/COFF EFI executable — the stub — with three extra sections appended, which the stub reads at boot to find its payloads:
| Section | Content | Source |
|---|---|---|
.cmdline | The kernel command line, NUL-terminated. | --cmdline or --cmdline-file |
.linux | The kernel image. | --kernel, or the unique kernel resolved by --kernel-dir |
.initrd | The initramfs cpio image. | --initramfs |
mkuki appends them in exactly that order. Each new section is marked as initialised, read-only data. The tool works at the PE level directly: it parses the stub's DOS/PE headers and section table, computes correctly-aligned virtual addresses and file offsets for the new sections, appends their data, updates the section count and the SizeOfImage/SizeOfHeaders fields, and — if the stub's header has no spare room for three more section entries — grows the header region and relocates the existing sections' file pointers to make space. A stub that is not a valid MZ/PE image, or whose optional header is malformed, is rejected.
The command line #
The command line is taken either literally from --cmdline or read from the file named by --cmdline-file (exactly one is required). Either way, mkuki trims trailing newlines and carriage returns and appends a single NUL terminator. A command line containing an embedded NUL byte is rejected.
Selecting a release-named kernel #
--kernel names one concrete image. --kernel-dir instead names a directory whose immediate children are kernel releases, each containing a vmlinuz-* image—the layout used by /usr/lib/modules. Exactly one matching regular file must exist. Zero matches are an incomplete installation, and multiple matches are ambiguous; mkuki refuses both rather than guessing which kernel the firmware should boot.
Directory selection matters most in watch mode. Kernel package upgrades replace the release-named child directory, so a watcher bound to the old concrete path would never adopt the new kernel. With --kernel-dir, mkuki watches the whole directory tree and resolves its unique kernel again before every rebuild. A transient zero- or two-kernel state during an upgrade makes that rebuild fail without killing the watcher; the next filesystem change retries after the package transaction reaches a unique kernel again.
The stub #
--stub names the EFI stub to append to. With no --stub, mkuki uses a stub bundled into the binary (the systemd EFI stub). mkuki --stub-info prints that bundled stub's provenance and exits, so you can see exactly what the default is without building anything.
Output and atomicity #
mkuki creates any missing parent directories of --out, writes the assembled image to a sibling temp file, and renames it into place, so an interrupted or failed build never leaves a half-written UKI behind. On success it prints the output path and byte size to stderr. Each of the three payloads must be non-empty; an empty .cmdline, .linux, or .initrd fails the build.
Watch mode #
With --watch, mkuki stays resident: it builds once up front, then rebuilds the UKI whenever an input changes — so the boot image tracks a new kernel, a freshly-repacked initramfs (mkirf's half of Dynamic Boot), or an edited command-line file with no manual step. Like mkirf's watch mode, it is a foreground loop that runs until killed; supervising it is a service manager's job, and a rebuild that fails (for example, a kernel caught mid-copy) is logged rather than fatal, so fixing the input recovers on the next change.
The watched inputs are the kernel source, --initramfs, and—only if it is a --cmdline-file, since a literal --cmdline is static—the command-line file. For --kernel and the other concrete files, mkuki watches each input's parent directory non-recursively rather than the file itself. This survives atomic temp-and-rename writes, which appear as directory events where a stale single-file inode watch would miss them. A --kernel-dir tree is watched recursively so release directories can be replaced.
An output directly inside a non-recursive watched directory would retrigger the watcher, so mkuki refuses it. With a recursive --kernel-dir, the output must be outside the entire watched tree for the same reason. The --debounce window (default 5 seconds) is the settle time before a rebuild.
Options #
| Option | Effect |
|---|---|
--kernel PATH | A concrete kernel image; becomes the .linux section. Mutually exclusive with --kernel-dir. |
--kernel-dir PATH | Resolve the unique regular file at PATH/*/vmlinuz-*; in watch mode, recursively watch PATH and resolve it again for every rebuild. Mutually exclusive with --kernel. |
--initramfs PATH | The initramfs cpio; becomes the .initrd section. Required (except with --stub-info). |
--cmdline TEXT | The kernel command line, given literally; becomes the .cmdline section. Mutually exclusive with --cmdline-file. |
--cmdline-file PATH | Read the kernel command line from PATH instead. Mutually exclusive with --cmdline. |
--out PATH | The output UKI path. Required (except with --stub-info). |
--stub PATH | The PE/COFF EFI stub to append sections to. Defaults to the bundled systemd stub. |
--stub-info | Print the bundled stub's provenance and exit. |
--watch | Stay resident and rebuild the UKI whenever the selected kernel source, --initramfs, or --cmdline-file changes. Runs until killed. |
--debounce SECS | With --watch, the settle time before a rebuild. Default 5. |
Exactly one of --kernel or --kernel-dir, and exactly one of --cmdline or --cmdline-file, must be given: supplying both alternatives in a pair, or neither, is a usage error.
Exit status #
| Code | Meaning |
|---|---|
0 | The UKI was built (or --stub-info, --help, or --version was printed). |
1 | A build or watch failure — a missing or unreadable input, an invalid stub, an empty payload, or an I/O error. |
2 | A usage error — a bad or missing option, or an invalid --cmdline/--cmdline-file combination. |
See also #
- mkirf — builds the
.initrdpayloadmkukiwraps. - The initramfs stage — what the bundled initramfs does once firmware boots the UKI.
reg
Peios / Using Peios / Peiosutils / Registry tools
reg is the command-line interface to the live registry — the running LCS store, reached through the registry system calls. Where regman reads shipped documentation and tells you what a key means, reg talks to the kernel and reads or writes what a key is. It is the everyday tool for scripting configuration: querying effective values, writing to layers, masking and hiding, managing security descriptors, watching for change, and taking backups.
reg <command> [options] <key> [value] [data]
reg is the read/write half of the registry toolset and regman is the lookup half. The division is exact:
| To… | Use… |
|---|---|
| Read or change what a value is set to, on this machine, right now | reg |
| Look up what a value means — its type, default, valid range, when it applies | regman |
Consult regman first to learn the legal range for a knob, then use reg set to write a value inside it. reg never checks a value's meaning and regman never touches the live store; they are complementary.
How reg treats access and privilege #
Every reg operation goes through the kernel's access check against the single key it opens — the same access-control path as any other protected object, with no traversal check on the parent keys. reg performs no identity or membership checks of its own: it does not refuse a privileged operation up front. It attempts the operation and reports faithfully whatever the kernel returns. An operation that needs a privilege you lack (creating a link, a positive-precedence layer, a backup or restore, reading a SACL) simply fails with access denied (exit 3) rather than being blocked by the tool. A single reg command may legitimately span privilege tiers.
Addressing model #
Almost every subcommand shares one addressing scheme: a key path positional, and an optional value name positional. Understanding the split is most of understanding reg.
Key paths #
The key is a positional path argument. Both / and \ are accepted as separators — neither character is legal inside an LCS key component, so there is no ambiguity, and you may use whichever your shell quotes most comfortably:
reg get Machine/System/KMES
reg get 'Machine\System\KMES' # identical
A leading separator is optional and ignored (/Machine/X is the same as Machine/X). Paths are compared case-insensitively. The first component is the hive:
| Path | Meaning |
|---|---|
Machine\… | The machine hive — system-wide configuration. |
Users\<SID>\… | A specific principal's hive. |
CurrentUser\… | A kernel alias, rewritten to the caller's own Users\<SID>\ at the syscall boundary. |
On output, reg displays paths with a backslash by default. --sep=/ (or REG_SEP=/) switches display to forward slash for that invocation; this affects display only, never how a path is parsed.
The value-name positional #
The value name is a separate positional argument — it is never folded into the key path. This is deliberate: an LCS value name may itself contain / or \ (which a key component may not), so keeping them apart removes all ambiguity.
reg get Machine/System/KMES BufferCapacity
# └──── key path ─────┘ └── value ──┘
The presence or absence of the value argument selects what the command targets:
- No value argument ⇒ the command targets the key itself — its metadata, its values as a set, its security descriptor, its children.
- A value argument ⇒ the command targets that one named value.
- The default value (the empty-name value) is addressed by the literal token
@in the value-name position, mirroring.regconvention:reg get Machine/App @.
This split applies uniformly to get, set, del, mask, and unmask.
Value literals and types #
On set (and in a batch), the value's data is a single token whose registry type is either forced by a type: prefix or inferred from its shape. The registry value types and their literal syntax:
| Prefix | Registry type | Data syntax |
|---|---|---|
sz: | REG_SZ | UTF-8 string (rest of token, verbatim). |
expand: | REG_EXPAND_SZ | UTF-8 string, %VAR% left unexpanded on disk. |
dword: | REG_DWORD | 42 or 0x2A; must fit u32. |
dword-be: | REG_DWORD_BIG_ENDIAN | 42 or 0x2A; must fit u32. |
qword: | REG_QWORD | 42 or 0x2A; must fit u64. |
multi: | REG_MULTI_SZ | Comma-separated; \, escapes a literal comma. |
hex: / bin: | REG_BINARY | Hex bytes; :, -, and space separators are ignored. |
link: | REG_LINK | An absolute key path (a symlink target). |
none: | REG_NONE | No data (token must be empty after the prefix). |
When the substring before the first : is not a recognised keyword (for example http://host), the token is not treated as typed and inference applies:
| Token shape | Inferred type |
|---|---|
all decimal digits, fits u32 | REG_DWORD |
all decimal digits, fits u64 but not u32 | REG_QWORD |
0x… hex, ≤ 8 significant hex digits | REG_DWORD |
0x… hex, ≤ 16 significant hex digits | REG_QWORD |
| anything else (including empty) | REG_SZ |
Inference is intentionally broad, so any all-digit token becomes a number — a PIN, a zip code, or 007 becomes a REG_DWORD and loses its textual form. To force a string, prefix it with sz: (sz:007 stores the string 007; sz:dword:42 stores the literal string dword:42). Because the coercion is a known trap, reg set always echoes the resolved type on success, so a surprising conversion is never silent even under --quiet.
Global options #
Every subcommand accepts the following. Each behavioural toggle also has an environment-variable equivalent, so it can be set per-invocation (the flag) or once per shell or script (the variable). The flag wins over the variable, which wins over the built-in default.
| Option | Env var | Effect |
|---|---|---|
--json | REG_JSON=1 | Emit structured JSON instead of human text. watch emits JSON-lines. |
-v, --verbose | REG_VERBOSE=1 | More detailed output. |
-q, --quiet | — | Suppress non-essential output. (A surprising type coercion is still reported.) |
--sep=CHAR | REG_SEP | Path display separator: \ (default) or /. |
--help | — | Print help for the command and exit 0. |
--version | — | Print the version and exit 0. |
Mutating subcommands additionally accept:
| Option | Env var | Effect |
|---|---|---|
--layer NAME | REG_LAYER | Target layer for the write (default: the base layer). |
-y, --yes | REG_ASSUME_YES=1 | Skip the confirmation prompt on a destructive command. |
Destructive commands (del -r, restore, layer del) prompt for confirmation when standard input is a terminal, and auto-proceed when it is not — so interactive use is guarded while scripts are never blocked. Set -y/--yes or REG_ASSUME_YES=1 to skip the prompt explicitly.
Reading #
reg get <key> [value] #
Read the effective (resolved) value. With a value, prints that value's data; with no value, lists the key's effective values (the default @ value first, then named values sorted case-insensitively) — a shorthand for ls --values-only.
| Option | Effect |
|---|---|
-L, --layers | Annotate the value with the layer it resolved from and its sequence number. |
--raw | Write the value's raw bytes to standard output verbatim — for piping REG_BINARY data. |
--no-follow | Operate on a symlink key itself rather than following it to its target. |
The single-value default form prints data bare (no quotes; one element per line for REG_MULTI_SZ) so it pipes cleanly. -L shows the winning layer's provenance:
$ reg get Machine/System/KMES BufferCapacity
4194304
$ reg get Machine/App Theme -L
Theme = REG_SZ "dark" (layer: policy, seq 7)
Only the winning value is shown; the full shadowed stack beneath it is not yet exposed by any client ABI (the -L flag will render the whole stack unchanged once that primitive lands). See Layers for what "effective" resolves against.
reg ls <key> #
One-level listing of a key's immediate subkeys and values.
| Option | Effect |
|---|---|
-l, --long | Long form: for subkeys, child and value counts; for values, type and byte size. |
--keys-only | List subkeys only. |
--values-only | List values only. |
$ reg ls Machine/System/KMES -l
BufferCapacity = REG_QWORD 4194304 (8 bytes)
MaxEventSize = REG_DWORD 65536 (4 bytes)
reg tree <key> #
Recursively list the subkey tree rooted at key.
| Option | Effect |
|---|---|
--depth N | Limit recursion to N levels. |
--values | Include each node's values, not just its keys. |
$ reg tree Machine/System --depth 2
reg info <key> #
Print a key's metadata: leaf name, subkey and value counts, last-write time, hive generation, security-descriptor size, and the volatile and symlink flags.
| Option | Effect |
|---|---|
--no-follow | Inspect a symlink key itself rather than its target. |
$ reg info Machine/System/KMES
path Machine\System\KMES
name KMES
subkeys 0
values 4
last_write_ns 1717243800000000000
hive_generation 42
sd_size 164
volatile false
symlink false
Writing #
reg set <key> <value> <data> #
Create or update a value. The value name (or @) and the data token are both required. Writes are tagged with --layer (default: base). See value literals for the data syntax.
| Option | Effect |
|---|---|
--layer NAME | Write into layer NAME instead of the base layer. |
-p, --parents | Create any missing ancestor keys. |
--expected-seq N | Compare-and-swap guard: apply only if the value's current sequence is N; a mismatch exits 6 without writing. |
$ reg set Machine/App Build 4096
set Machine\App Build = REG_DWORD 4096 (layer: base)
$ reg set Machine/App Servers multi:alpha,beta --layer policy
--expected-seq supports lock-free read-modify-write: read the value with -L to learn its sequence, then write back with --expected-seq set to that number; if another writer changed it in between, your write fails cleanly instead of clobbering theirs.
reg new <key> #
Create a key with no values. Reports whether the key was created or already existed.
| Option | Effect |
|---|---|
--layer NAME | Create the key in layer NAME. |
-p, --parents | Create any missing ancestor keys. |
--volatile | Create a volatile (RAM-only) key that does not survive a reboot. |
$ reg new Machine/App/Cache --volatile
created Machine\App\Cache (layer: base)
reg del <key> [value] #
Delete a value, or a key. (Alias: delete.)
With a value, deletes this layer's entry for that value only — lower-layer entries resurface, because a per-layer delete is not a tombstone (use mask for that). With no value, deletes the key, which must be empty unless -r is given.
| Option | Effect |
|---|---|
--layer NAME | Delete from layer NAME. |
-r, --recursive | Delete the key and all its descendants, in one transaction: all of them go, or, on any failure, none do. Links under the key are deleted, not followed. Every key in the tree is opened before any is deleted, so a tree larger than reg may hold open at once (1,024 files by default) is refused whole, as is one of more than 4,096 keys, which is all a transaction holds. |
-y, --yes | Skip the confirmation prompt (recursive deletes prompt on a terminal). |
$ reg del Machine/App Legacy
deleted value Legacy from Machine\App
$ reg del Machine/App/Temp -r
Recursively delete key Machine\App\Temp and all its contents? [y/N] y
deleted Machine\App\Temp (3 keys)
See Deleting keys and values for how a per-layer delete differs from a tombstone.
Masking and hiding #
These commands expose LCS's tombstone and hide primitives, which mask lower-precedence state rather than removing a layer's own entry — the distinction that makes layers revertible. The layer is chosen with --layer (default: base).
reg mask <key> [value] / reg unmask <key> [value] #
mask sets a tombstone: a per-value marker in the target layer that hides that value from all layers below it. unmask clears it.
| Option | Effect |
|---|---|
--layer NAME | The layer that carries the tombstone. |
--all | Blanket tombstone: mask all lower-layer values on the key (no value argument). |
A value is required unless --all is given.
$ reg mask Machine/App ApiKey --layer policy
set tombstone on Machine\App ApiKey (layer: policy)
$ reg mask Machine/App --all --layer policy
set blanket tombstone on Machine\App (layer: policy)
reg hide <key> / reg unhide <key> #
hide installs a HIDDEN path entry in the target layer, masking the key's existence in that layer and below. unhide removes that layer's path entry, letting the key reappear.
| Option | Effect |
|---|---|
--layer NAME | The layer that hides (or unhides) the key. |
$ reg hide Machine/App/Debug --layer policy
hid Machine\App\Debug (layer: policy)
Layers #
reg layer ls #
List layers with name, precedence, enabled state, and (informational) owner SID, ordered by descending precedence. The base layer is always listed. A layer whose metadata values have the wrong type is marked (malformed metadata): the registry goes by what it last knew of that layer, not by what is shown.
| Option | Effect |
|---|---|
-l, --long | Long form. |
$ reg layer ls
policy prec=100 enabled owner=S-1-5-32-544
base prec=0 enabled
reg layer new <name> #
Create a layer by writing its metadata key under Machine\System\Registry\Layers\, with its precedence and enabled state, in one transaction.
| Option | Effect |
|---|---|
--precedence N | Precedence (higher wins). A precedence above 0 requires SeTcbPrivilege; the tool attempts it and reports any denial. |
--owner SID | Record an informational owner SID, stored as a binary SID. Without it, the owner is whoever creates the layer. |
--disabled | Create the layer disabled. |
$ reg layer new policy --precedence 100 --owner S-1-5-32-544
created layer policy (precedence 100, enabled)
reg layer set <name> #
Modify an existing layer's metadata.
| Option | Effect |
|---|---|
--precedence N | Change the precedence. |
--enable / --disable | Enable or disable the layer. |
--owner SID | Change the informational owner SID. |
reg layer del <name> #
Delete a layer — removes its metadata key; LCS tears down the layer's entries. Prompts for confirmation on a terminal.
$ reg layer del policy
Delete layer policy and all its entries? [y/N] y
deleted layer policy
Managing per-process private layer views is out of scope for reg; see private hives and layers.
Security descriptors #
reg sd <key> #
Show or set a key's security descriptor as SDDL, using the same codec as the sd tool, so its output is consistent. With no scope flags, the default is owner + group + DACL (a SACL requires the privileged ACCESS_SYSTEM_SECURITY right).
| Option | Effect |
|---|---|
--set SDDL | Apply the given SDDL. Without it, sd prints the current descriptor. |
--owner | Scope to the owner. |
--group | Scope to the group. |
--dacl | Scope to the DACL. |
--sacl | Scope to the SACL (needs ACCESS_SYSTEM_SECURITY). |
$ reg sd Machine/App
O:BAG:BAD:(A;;KA;;;BA)(A;;KR;;;WD)
$ reg sd Machine/App --set 'O:BAG:BAD:(A;;KA;;;BA)' --owner --dacl
set security descriptor on Machine\App
A security-descriptor change takes effect on future opens only (existing handles keep the access mask they were granted). Security changes are not layered and are not undone by layer removal — they mutate the key object directly. See Access control on keys.
Symlinks #
reg link <key> <target> #
Create a symlink key pointing at the absolute target key path. This creates a key with the immutable symlink flag and a default REG_LINK value holding the target. Requires KEY_CREATE_LINK and SeTcbPrivilege/Administrator; the tool attempts it and reports any denial.
| Option | Effect |
|---|---|
--layer NAME | Create the link key in layer NAME. |
$ reg link Machine/App/CurrentConfig Machine/App/Config/v3
linked Machine\App\CurrentConfig -> Machine\App\Config\v3 (layer: base)
get and info follow symlinks to their target by default; pass --no-follow to inspect the link key itself. See Advanced: registry links.
Watching #
reg watch <key> #
Arm a change watch and stream one event per change until interrupted (or until --count events). Each event faithfully means "something under the filter changed — re-read"; reg does not decode the change records' contents. With --json, emits one JSON object per line.
| Option | Effect |
|---|---|
--subtree | Watch descendant keys too, not just this key. |
--filter LIST | Comma-separated subset of value, subkey, sd (default: all). |
--count N | Exit after N events. |
$ reg watch Machine/System/KMES --subtree --filter value
changed: Machine\System\KMES
changed: Machine\System\KMES
See Watching for changes for why a service watches instead of polling.
Batch, export, backup, and restore #
Two paths move a subtree around: export/apply are the portable, reviewable path (a diffable text or JSON document); backup/restore are the exact, privileged path (an opaque kernel snapshot).
reg apply <file> #
Apply a batch of operations to one hive in a single transaction — all-or-nothing. All operations must target one hive (a cross-hive batch fails with exit 4). - reads standard input.
| Option | Effect |
|---|---|
--dir DIR | Apply every batch file in DIR (sorted), each as its own transaction. A missing or empty directory applies nothing and succeeds. |
--once-delete | Delete each batch file after it applies successfully (a drain). |
-y, --yes | Skip confirmation. |
The batch format is JSON (canonical and exact) or a line-oriented text format; apply auto-detects (a leading { or [ means JSON). Text-format input is not yet implemented — supply JSON, or generate one with reg export --json. Text export works for review.
$ reg export --json Machine/App - | reg apply -
applied 4 keys
A JSON batch is an object with a keys array, parents before the keys under them. Each entry has a path, an optional values array (each value {name, type, data}, or hex in place of data, as in the output formats below), and an optional descriptor: security descriptor parts in SDDL, set on the key in the same transaction as its values. Only the parts the SDDL gives are set, so "descriptor": "S:(AL;CI;0x10002;;;WD)" sets the SACL and leaves the owner, group and DACL the key inherited. Setting a SACL needs SeSecurityPrivilege, as reg sd --sacl does; a descriptor that does not parse, or that gives no part at all, fails the batch before anything is written. export never writes a descriptor.
A key entry with a member apply does not know is refused rather than ignored. Versions of reg before descriptor existed ignore it and report success without setting anything, so a batch that carries one needs a reg that knows it.
reg export <key> [file] #
Dump a subtree to the batch format. Omit file (or pass -) to write to standard output. Human-readable text by default; --json emits the canonical exact form.
| Option | Effect |
|---|---|
--layer NAME | Export one layer's view (default: the effective state). |
$ reg export Machine/App app-config.reg
reg backup <key> <file> #
Write an opaque, exact, binary kernel snapshot of a key and its subtree to file. Requires SeBackupPrivilege.
$ reg backup Machine/System/KMES kmes.snap
backed up Machine\System\KMES -> kmes.snap
reg restore <key> <file> #
Replace a key and its entire subtree from a snapshot, in one transaction. Requires SeRestorePrivilege. Prompts for confirmation on a terminal.
$ reg restore Machine/System/KMES kmes.snap
Replace key Machine\System\KMES and its entire subtree from kmes.snap? [y/N] y
restored Machine\System\KMES from kmes.snap
See Backup and restore for when to reach for each path.
Output formats #
Default output is terse, human-readable, and grep-friendly. --json switches every command to structured output — a single self-contained JSON document, except watch, which emits JSON-lines (one object per event). In any value listing, the default (@) value is emitted first, then named values sorted case-insensitively.
Type formatting on read:
| Registry type | Human form | JSON form |
|---|---|---|
REG_SZ / REG_EXPAND_SZ | the string | {"type":"sz","data":"…"} |
REG_DWORD / REG_QWORD | decimal | {"type":"dword","data":42} |
REG_MULTI_SZ | one element per line | {"type":"multi","data":["a","b"]} |
REG_BINARY | hex | {"type":"binary","data":"deadbeef"} |
REG_LINK | the target path | {"type":"link","data":"…"} |
The registry doesn't check that a value's bytes fit its type. Bytes that don't are shown as hex, in both forms, under the value's own type. Examples are a string that is not UTF-8, a number of the wrong length, and REG_NONE with data. In the JSON form such a value has a hex member, its bytes exactly, in place of data, as {"type":"sz","hex":"ff00"}: it survives export --json and apply unchanged.
Exit status #
| Code | Meaning | Typical cause |
|---|---|---|
0 | Success. | — |
1 | Usage error. | Bad arguments; a required option missing. |
2 | Key or value not found. | ENOENT. |
3 | Access denied. | EACCES, EPERM — the kernel refused the operation. |
4 | Invalid specification. | Bad path, type, or literal; a cross-hive batch; a name too long (EINVAL, EXDEV, ENAMETOOLONG). |
5 | Syscall or source failure. | EIO, ETIMEDOUT, ENOSPC, ENOMEM. |
6 | Compare-and-swap conflict. | EAGAIN — an --expected-seq guard did not match; the value changed under you. |
A denial (3) reports the operation, the target, and, where relevant, the access that was required — in the style of the sd and token tools.
See also #
regman— the registry manual: what a key or value means, as opposed to what it is set to. Consult it before youreg set.- Keys, values, and types — the data model
regaddresses. - Layers — what "effective" resolves against, and what
--layer,mask, andhideoperate on. - Access control on keys — the check every
regoperation goes through.
regman
Peios / Using Peios / Peiosutils / Registry tools
Configuration, not storage established that the registry holds values but not their meaning: it keeps a type tag and some bytes, and never knows that BufferCapacity must be a power of two or what it is for. That knowledge has to live somewhere a person can look it up. It lives in regman, the registry's manual.
regman is the man of the registry. Give it a path and it tells you what a key or value actually is. It is the everyday tool for configuring a Peios system — before you touch a knob, regman is how you find out what it does and what changing it will cost.
regman, in one sentence #
regman <path> [value] documents what a registry key or value means — its type, default, valid values, when a change takes effect, and a prose description — drawn from shipped documentation, never from the live registry.
That last clause is the one to hold onto; there is a section on it below.
Looking up a value #
Give regman a key path and a value name and it prints a knob-card — an identity line, an aligned block of facts, and a description:
$ regman 'Machine\System\KMES' BufferCapacity
Machine\System\KMES BufferCapacity documented by kmes
Type REG_QWORD
Default 4194304 (4 MB)
Valid 65536–268435456 bytes (64 KB–256 MB), power of two
Applies live — ring-buffer swap
Per-CPU ring buffer capacity, in bytes. Must be a power of two; values
that are not are treated as invalid and ignored.
Every registry value is a knob, and every knob has the same few facts worth knowing, so the card is uniform rather than free-form:
| Field | Tells you |
|---|---|
| Type | The value's registry type (REG_QWORD, REG_SZ, …). |
| Default | The value used when nothing is set. |
| Valid | The range, set, or constraint a sensible value must satisfy. |
| Applies | When a change takes effect — live, on restart, or on reboot. |
The Applies field is the one operators reach for most: it is the difference between "edit it and you're done" and "edit it and schedule a reboot". Only the fields that make sense for a given item are shown, and a knob that is being retired carries a prominent deprecation notice at the top of its card.
Looking up a key #
Give regman just a key — no value — and it does double duty as an index: the key's own description, then a list of every value documented under it, one summary line each.
$ regman 'Machine\System\KMES'
Machine\System\KMES documented by kmes
The KMES event subsystem reads its tuning parameters from the values
under this key...
Values
BufferCapacity Per-CPU ring buffer capacity in bytes.
MaxEventSize Max total size of a userspace-emitted event.
MaxNestingDepth Max nesting depth for userspace event payloads.
MaxEmitRatePerProcess Max events per second a single process may emit.
So you can start at a subtree, see what is configurable there, and drill into any single value.
Keys that come in families #
Much of the registry is not at a fixed path. Every service has a key under Machine\System\Services\, every interface one under Machine\System\Network\Interfaces\, every principal one under Machine\Generic\Authn\Policy\. You ask about a real one — sshd, an interface id — and the documentation is written once for the whole family.
regman resolves that for you. Ask about the concrete path and you get the family's page:
$ regman 'Machine\System\Services\sshd' ImagePath
Machine\System\Services\<name> ImagePath documented by peinit
Type REG_SZ
Default (required)
Valid a non-empty absolute path
Applies restart
Absolute path to the service binary.
The heading keeps the <name> so you can see you have been handed the page for the family rather than one written about sshd specifically. Everything on the card applies to the key you asked about.
A heading with a trailing ... — Machine\System\Network\Rules\<Layer>\<rule...> — means the family is a tree and the page covers it at any depth. That is what firewall rules are: an exception is a subkey of a rule, and a rule nested five deep has the same values meaning the same things as one at the top, so one page answers for all of them.
If a package has documented your exact key — its own service's knob, a particular principal's privileges — that page wins instead, and the heading shows the concrete path. The more specific documentation always answers.
Searching, when you do not know the path #
If you don't know where a setting lives, regman -k searches names and summaries — the registry's apropos:
$ regman -k buffer
Machine\System\KMES BufferCapacity Per-CPU ring buffer capacity in bytes.
regman documents intent, not current state #
This is the boundary that matters most. regman tells you what a setting should be — what it means and which values are legal — not what it is currently set to on this machine. It reads shipped documentation, never the live registry; it does not talk to LCS at all. Ask regman about BufferCapacity and you learn it must be a power of two and defaults to 4 MB; you do not learn that this particular box has it set to 8 MB right now. Reading the current value is a separate, live query against the registry — that is reg's job. regman tells you a knob should be a power of two; reg get tells you what it is set to, and reg set changes it. Reach for regman to decide what to write, and reg to write it.
Put regman next to the other two things from Configuration, not storage and a clean division of labour appears:
| To learn… | Look at… |
|---|---|
| What a setting means, and what is valid | regman — the manual |
| What the value is set to | reg — a live query against the store |
| What the subsystem is actually running on | the event log |
regman is the one you consult first, and the only one that works before you have changed anything — because documentation ships with the software, not with the machine's state. It is the natural complement to reject-or-keep: regman tells you the valid range up front, so you set a good value the first time instead of writing one the owning subsystem will quietly refuse.
Where the documentation comes from #
regman has no built-in database of every setting. Each package ships its own documentation as files dropped into a well-known directory (/usr/share/regman/), one fragment per package documenting that package's whole configuration surface. Install a package and its registry documentation appears; remove the package and it goes away. The package that owns a set of keys is the package that documents them — the only arrangement that stays correct as the system changes.
A consequence worth knowing: regman can only describe settings that some installed package has documented. An undocumented key is invisible to it — regman is exactly as complete as the packages on the machine make it. If two packages happen to document the same item, regman shows both and flags the overlap rather than silently picking a winner.
(For the people who write that documentation, regman fmt and regman lint prepare and check fragments, and regman index keeps lookups fast on large corpora — the lookup is always correct without an index, which is purely an accelerator. These are packaging concerns rather than everyday operator ones.)
Where to go next #
For the idea regman exists to serve — why the registry holds values without holding their meaning, and what reject-or-keep means — read Configuration, not storage.
For the other half of the picture — reading what a value is actually set to, which is a live query against the store rather than a documentation lookup — read LCS and sources.
evman
Peios / Using Peios / Peiosutils / Event tools
Every record in the event stream is a type name and a set of fields. The record says that something happened — kacs.audit.access.checked, with outcome.success false and an access.granted of 0x120089 — but not what any of it means. Whether access.granted is the rights the check granted or the rights a handle was opened with, whether a missing trigger.ace is a bug or expected, which of two masks you are looking at: that knowledge lives with the software that writes the records. It lives in evman, the event stream's manual.
evman is to events what regman is to the registry. Give it an event type or a field and it tells you what that is. Reach for it whenever a record does not say enough on its own — before writing a query, a filter or a security descriptor over a field, and whenever a value surprises you.
evman, in one sentence #
evman <name> documents what an event type or field means — when it fires, what each field records, which values it takes — drawn from shipped documentation, never from the live event stream.
Looking up an event type #
Give evman an event type and it prints the event's card: how important it is, what decides whether it is recorded, a description, and every field it carries.
$ evman kacs.audit.access.checked
kacs.audit.access.checked defined by kacs
Tier essential
Gating a matching SACL audit ACE, or the token's audit policy
Cardinality once per matching audit ACE, not once per access
The record that an access check completed, and what it decided. The most
common event on the system, and the one most investigations start from.
...
Fields
subject group, from kernel
subject.token.sid
subject.token.groups
...
object.kind required
object.file.path when object.kind == file
access.requested required
access.granted required
The mask this check granted.
...
| Fact | Tells you |
|---|---|
| Tier | How much the event matters, and whether an administrator can switch it off: essential events are always recorded; standard ones are on by default; verbose and debug ones are off unless switched on. |
| Gating | What decides which occurrences produce a record at all. An event gated on a SACL is silent for an object with no audit ACE, so its absence proves nothing. |
| Cardinality | How many records one occurrence produces, where that is not one. |
Each field line says when the field is present — always, optionally, or only when another field has a given value — and an indented note says what the field means in this event, where that is narrower than its general meaning. Read those notes: the same field can mean subtly different things in different events, and the note is where that is written down.
Looking up a field #
Give evman a field and it prints the field's card: its type, the values it can take, a description, and every event type that carries it.
$ evman subject.token.type
subject.token.type defined by kernel
Type str.enum
Values primary | impersonation
Closed true
Whether the effective token is a primary token or an impersonation token.
Needed beside subject.token.impersonation, because a primary token and an
Anonymous impersonation token both report level 0 there.
Carried by
kacs.audit.access.checked (via group subject)
kacs.audit.handle.used (via group subject)
lcs.audit.key.opened (via group caller)
...
Carried by is the reverse index: the event types whose records can hold this field. It is the list to check before you rely on a field across the whole stream — a query on subject.token.sid finds nothing in an event type that does not carry it.
Closed says whether the list of values is complete. A closed list never grows without the field changing; an open one may gain values, so anything you write over it should cope with a value it has not seen.
Some fields are carried in the record header rather than the payload — the time, the sequence number and who wrote the record. Their cards say so: they are on every event, and no payload repeats them.
Looking up a subtree #
Field names and event types are both dotted, general to specific. Give evman a prefix and it lists what lies beneath it, one summary line each:
$ evman kacs.audit
Event types
kacs.audit.access.checked The record that an access check completed, and…
kacs.audit.handle.used The record of what was done with a handle afte…
kacs.audit.privilege.used The record that a privilege contributed access…
$ evman subject.token
Fields
subject.token.auth-id The LUID of the logon session the effec…
subject.token.groups The group SIDs carried by the effective…
subject.token.integrity The integrity RID of the effective toke…
...
So you can start from a subsystem, or from a part of a record such as "who acted", and drill into any one entry.
A few names are not defined one by one. Any field can have a variant with a suffix such as -previous or -requested — policy.generation-previous is the generation that was in force before — and any thing can have a standard attribute such as count or size. evman resolves these to the field or thing they qualify and says which rule it applied.
Searching, when you do not know the name #
evman -k searches names and summaries — the event stream's apropos. Every term must match.
$ evman -k swap
buffer.capacity-previous The per-CPU ring capacity before a capacity swap…
buffer.generation-previous The ring generation before a capacity swap, nor…
kmes.buffer.swap.failed The record that KMES could not resize its per-C…
evman documents meaning, not what happened #
evman tells you what an event or field means — never which events have occurred on this machine. It reads shipped documentation and never touches the event stream or the event store. Ask it about kacs.audit.access.checked and you learn when the event fires and what its fields hold; you do not learn that anything was denied today. That is a query against the event store, which is the Event Viewer's job, or eventd's query interface.
| To learn… | Look at… |
|---|---|
| What an event or field means | evman — the manual |
| Which events have happened | the Event Viewer, or a query against eventd |
| Whether an event type is recorded at all | its tier, and the emission policy under Machine\Generic\Events — see regman |
Where the documentation comes from #
evman has no built-in list of events. Each package ships documentation for the events it writes and the fields it defines, as files in /usr/share/evman/. Install a package and its events appear in the manual; remove it and they go.
The manual is also the definition. Every field any Peios component writes is defined in exactly one of these files, by the component that owns it, and a field defined nowhere is not a field anything should emit. That is what keeps one value from acquiring two names: a security descriptor that denies a field must not be evadable by writing the same value under another name. The rules are in The Catalogue.
(For the people who write these files, evman lint checks them against those rules. That is a packaging concern rather than an everyday one; see Writing evman pages.)
Where to go next #
- Logs and events — what the event stream is, and how events differ from logs.
regman— the same idea for the registry, including the emission-policy keys that switch event types on and off.
Text and archive tools
Peios / Using Peios / Text and archives
Experimental includes the following commands in the system root. They are not part of the initramfs tool set.
| Task | Commands | Package |
|---|---|---|
| Search text | grep, including grep -E, grep -F and grep -P | org.gnu.grep |
| Transform lines | sed | org.gnu.sed |
| Process fields and records | awk, gawk | org.gnu.gawk |
| Select files and pass arguments | find, xargs | org.gnu.findutils |
| Compare files | diff, cmp, diff3, sdiff | org.gnu.diffutils |
| Read long files interactively | less | com.greenwoodsoftware.less |
| Identify file contents | file | com.darwinsys.file |
| Create and extract tar archives | tar | org.gnu.tar |
| Gzip compression | gzip, gunzip, zcat | org.gnu.gzip |
| XZ compression | xz, unxz, xzcat | org.tukaani.xz |
| Zstandard compression | zstd, unzstd, zstdcat | com.facebook.zstd |
| Bzip2 compression | bzip2, bunzip2, bzcat | org.sourceware.bzip2 |
| Create ZIP archives | zip | net.sourceforge.infozip.zip |
| Inspect and extract ZIP archives | unzip, zipinfo | net.sourceforge.infozip.unzip |
Peiosutils supplies the other basic text commands, including
cat,
head,
tail,
sort,
uniq,
cut,
paste,
join,
tr and
wc.
Inspecting text #
In less, use Space to move forward, b to move back, /pattern to search,
n for the next match and q to quit. The pager does not install an editor;
its edit command requires a separately installed editor.
Working with archives #
GNU tar invokes the separate compressor commands included above. The matching compression libraries alone do not provide those commands.
Use .tar.gz, .tar.xz, .tar.zst or .tar.bz2 with tar -a to choose the
compressor by suffix. file archive identifies a file by its contents.
These formats exchange file contents and conventional archive metadata. Do not use them as a substitute for a Peios system backup that preserves native security descriptors and registry state.
Editing files with nano
Peios / Using Peios / Nano
Experimental includes GNU nano in the system root through org.gnu.nano.
Run nano path/to/file. Press Ctrl+O to write the buffer, Enter to
confirm its filename, and Ctrl+X to exit. Ctrl+G opens help.
Syntax definitions, translations and manuals are in org.gnu.nano-common.
The vendor /usr/etc/nanorc enables the bundled syntax definitions. Normal
nano user configuration and HOME/XDG_DATA_HOME conventions apply. rnano
is also supplied; its restricted editing mode does not grant additional
Peios authority.
Saving documents #
Saving an existing regular file writes into the existing object. Its owner, DACL, audit policy and integrity label remain unchanged. Hardlinks continue to refer to that object. A new document receives the destination's normal Peios security inheritance. Saving in place is not an atomic replacement and does not guarantee recovery from a storage failure during the write.
FACS decides whether saving is permitted. Unix mode bits and a numeric root UID do not grant access. Editing does not require permission to change the file's security descriptor; synchronizing a save requires the applicable handle synchronization right.
Backups, recovery and history #
Use nano --backup file to retain a backup, normally file~. Peios nano
creates backups, emergency .save files and formatter/speller scratch files
with a protected DACL granting access to the effective token's user SID.
Backups are private editor copies; they do not clone the original document's
sharing policy or ownership. Normal kernel integrity/audit rules still apply.
No file contents are written before the private security is established.
History is optional: --historylog records search/replace/command history;
--positionlog remembers cursor positions. Nano uses an existing suitable
~/.nano directory or its usual $XDG_DATA_HOME/nano / ~/.local/share/nano
location. Its state directory and files must have nano's private owner/DACL
policy. Peios nano checks the actual opened file before reading or truncating
it and refuses symbolic links, multiply linked files and incompatible policies.
It does not silently change the permissions of existing history. An unsafe
state directory or file produces an error and disables the affected history
operation; ordinary document editing remains available.
On a hangup or fatal error nano attempts an emergency save, subject to space,
namespace permissions and other normal failures. It reports the saved path
when successful. If private creation is unavailable, nano reports failure
rather than substituting Unix 0600 permissions. Its backup failure prompt
still lets you choose whether to save the original without a backup.
Optional advisory lock files retain nano's interoperable format and normal destination inheritance. They contain editing identity/path information, not buffer contents, and do not authorize access to the document.
Console keyboards and fonts
Peios / Using Peios / Console tools
org.kernel.kbd supplies the upstream kbd console tools. Its matching
org.kernel.kbd-common package supplies keymaps, fonts, translations and
manual pages. Console data lives under /usr/share/kbd.
These tools operate on Linux virtual consoles: the text screens associated
with devices such as /dev/tty1. They do not configure a keyboard layout in
an SSH client, a serial terminal, or a graphical application.
Inspect a keymap without applying it #
Generate a binary keymap from the installed UK layout:
This parses the map and its includes without changing the console. The
package also includes layouts such as us, de and fr.
Configure a console #
With a token authorized for the console operation, apply a keyboard map or load a font:
The kernel determines access. Installing kbd does not grant console privileges, and the tools are not installed setuid. Keymap changes can affect other virtual consoles; selecting a device is not a promise of a separate per-session keyboard layout.
The machine's console layout #
A layout loaded with loadkeys lasts until the next boot. The one the
machine starts with is Machine\System\Console Keymap, a keymap name such
as uk, de or dvorak, loaded at boot by the console-keymap service.
Absent, it is the kernel's built-in US layout.
In System Settings, in the Language & Keyboard section, Layout
under Console Keyboard offers installed console layouts, grouped by
kind (QWERTY, QWERTZ, AZERTY and the rest). Choosing one saves the value;
when the service is available and you may restart it, the app also requests
a console-keymap restart to load it. In a terminal:
Changing it needs write access to Machine\System\Console, which as
shipped only Administrators have, and loading it at once needs the right
to restart the service. Without that right, a configured boot-triggered
console-keymap service can apply it at the next boot. On a custom image
without that service, saving the preference does not arrange for it to be
loaded, now or at boot; System Settings reports that condition.
A saved value or an accepted restart request is not proof that the loader finished. Check the service status and output if it does not take effect, and verify the layout at the machine's text console. Only a name is accepted, never a path. A name that is not a keymap leaves the layout as it was, and the service logs why.
This is the keyboard plugged into the machine, on its text consoles. The desktop's keyboard is the browser's, and a terminal over SSH uses the computer it is typed on.
dumpkeys inspects the current map, kbd_mode inspects or changes the
keyboard mode, and fgconsole reports the active virtual console. chvt
switches consoles. Switching to a console does not create a login session.
Package scope #
Installation does not select a keymap or font, or start additional login
prompts. The package ships the console-keymap service as a registry seed,
which does nothing until an image applies it; the Experimental edition
does. Persistent console fonts require separate system configuration.
kbd does not create login prompts. The Experimental edition provides
independent sessions on tty1, tty2 and tty3; see
multiple local consoles
for the login services and how to switch between them.
The PAM-based vlock screen locker and optional XKB-to-console conversion
are disabled. Native kbd keymaps remain available. openvt is a process
launcher for a virtual console. It preserves the caller's Peios token and
the console device's existing security descriptor. It does not grant an
identity or authenticate a user. Its --user mode is explicitly rejected
on Peios; that upstream mode infers a numeric user and invokes login -f. Use peinit and the authenticated login
service for managed login sessions.
The -debuginfo, -debugsource and -source packages provide debugging
symbols, referenced source files and corresponding source respectively.
1.1 What an Event Is
Peios / Using Peios / Events / Introduction
An event is a record that something happened: an access was checked,
a service started, a package was installed, a descriptor was found
corrupt. Events are produced by the component that observed the thing,
and consumed by whatever is watching — usually eventd, sometimes a
tool reading the stream directly.
This book enumerates every event Peios emits, with the fields each one carries. It is a lookup, not an explanation. Where an event's meaning needs the mechanism behind it, the page links to the manual that describes that mechanism.
1.1.1 Where the schemas come from #
Every event type and every field is defined in the evman catalogue:
the .evman fragments each component installs in /usr/share/evman/
(PGSS §6.10). The catalogue is the definition, and most of this book is
generated from it:
- §2 describes the groups, the sets of fields that travel
together, such as
subject. - §3 to §15 have a page per event type, one chapter per root: the
kernel's
kacs,stratafs,lcs,kmesandntfe(§3 to §7), thenpeinit,peipkg,eventd,authd,lpsd,timed,netdandtrustd(§8 to §15). - Appendix A lists every event type in one table, and Appendix B is the field index: every field, its type and values, and every event that carries it.
On a running system, evman <event-type> prints the same definition
from the installed fragments (evman).
1.1.2 Two transports, not one #
Most events travel through KMES, the kernel message event stream: a per-CPU ring buffer that the kernel writes into and userspace reads from. Every KMES event is a binary header followed by a MessagePack payload (§1.2).
Two things in this book never travel through KMES:
- eventd's records about itself (§10) are catalogue events, but
eventd writes them straight into its event store and they never touch
KMES. They carry no sequence number and no
emitter.*stamps. - LCS watch records (§16) are not events at all, and are included because an operator looking for "what does Peios tell me" would otherwise miss them. They are binary records read from a key file descriptor: a notification mechanism, not an audit trail, with a format that has nothing in common with a KMES event.
1.1.3 What is not here #
This book does not cover logs or metrics. Both reach eventd by a different path, are stored in different tables, and are not events. The eventd manual covers them.
Nor does it cover the query language for reading events back. That is PSPU §3, with the operator-facing view in the eventd manual.
1.1.4 Checking generated catalogue pages #
The event, group and field-index pages are generated from .evman fragments
by PKM's tools/gen-events-book.py. Edit the fragments rather than those
pages. --check compares the generated pages without writing; omit it to
regenerate. The PKM pre-push hook checks this output when kernel event
fragments or the generator change.
The generator reads kernel fragments from its own checkout and userspace
fragments from sibling component directories such as peinit/ and eventd/.
Keep the component fragments present when regenerating the complete index:
an absent userspace fragment is reported and omitted from the output. For
the same reason --check skips, naming the missing fragments, unless every
userspace fragment is present.
Documentation defaults to a sibling learn/. For a separate worktree:
PKM_LEARN_ROOT=/path/to/docs-worktree
An invalid explicit path fails. Missing or stale generated pages in the
selected documentation checkout fail --check, including a missing events
book. A standalone PKM checkout without a documentation companion skips the
check. The same path override applies to the pre-push check.
1.2 The Envelope
Peios / Using Peios / Events / Introduction
Every KMES event is a packed binary header followed immediately by its MessagePack payload, delivered as one contiguous byte sequence with no padding anywhere.
The emitter supplies only two things: the event type and the payload. Everything else in the header is stamped by KMES itself, which is what makes the identity fields trustworthy — an emitter cannot forge them.
1.2.1 Fields KMES stamps #
A consumer that exposes header values by name exposes them under these
field paths (PGSS §6.4). They are fields like any other, defined in the
catalogue and listed in the field index under event and emitter, but
they live in the header, and no payload repeats them.
| Field | Header field | Meaning |
|---|---|---|
event.type | type | The event type, as the emitter named it. |
event.time | timestamp | Wall clock at the moment KMES accepted the event, nanoseconds since the Unix epoch. |
event.sequence | sequence | The emitting CPU's per-boot counter. The first event on each CPU gets 1, and the numbers on one ring are contiguous. |
event.cpu | cpu_id | The CPU whose ring buffer holds the event. |
event.boot.guid | — | The boot the record belongs to. Not in the header: the consumer supplies the boot it read the record in. |
emitter.class | origin_class | The emission path that wrote the record: 0 userspace, 1 kmes, 2 kacs, 3 lcs, 4 ntfe. |
emitter.token.guid | effective_token_guid | The effective token of the task that caused the event. |
emitter.true-token.guid | true_token_guid | That task's own token, before any impersonation. |
emitter.process.guid | process_guid | The process the record was written from. |
emitter.class is set by the kernel, never by the emitter. A record
whose type names a kernel root but whose class is 0 was written by a
program, whatever its type says. The class is the path, not the type's
root: StrataFS records are written through KACS and carry class 2.
The identity stamps matter for reading this book: several events carry
no caller in their payload at all, because the envelope already names
one. StrataFS copy-up records are the clearest case — nothing in the
payload names a token, and the caller is recovered from the header. A
record written outside task context has zero GUIDs, and
emitter.stamp-failure in its payload says why.
event_size, header_size and type_len are structural, computed by
KMES during construction, and are not exposed as fields.
1.2.2 Layout #
All fields before the event type string sit at fixed offsets. The type
string begins at offset 77, with its u16 length at offset 75, so the
header is exactly 77 + type_len bytes. The payload runs from
header_size to event_size, and the next event begins at
event_size from the start of the current one.
All multi-byte header integers are little-endian. The identity GUIDs are opaque 16-byte values.
The full field-by-field layout is normative in PSPK §2, the KMES event stream specification. This summary is enough to walk a stream; it is not enough to implement one.
1.2.3 Ordering #
event.time is captured before event.sequence is assigned, so two
events with the same time on the same CPU are ordered by sequence.
event.cpu and event.sequence together identify a record within a
boot, and with event.boot.guid across boots.
Across CPUs there is no global order. Two events on different CPUs with close timestamps may have been observed in either order.
1.3 Encoding Conventions
Peios / Using Peios / Events / Introduction
Every KMES payload in this book is a MessagePack map with UTF-8 string keys. What may appear in it, and what each value means, is defined by the evman catalogue; the rules below are how the catalogue's definitions become bytes (PGSS §6.4 and §6.5).
1.3.1 The catalogue is the definition #
Every event type and every field is defined once, in a .evman
fragment installed in /usr/share/evman/ (PGSS §6.10). An emitter
writes no field the catalogue does not define, and this book's event
pages and field index are generated from the same fragments.
To read a definition on a running system, use
evman: evman kacs.audit.access.checked
prints an event type, evman subject.token.sid a field, and
evman kacs.audit everything beneath a prefix.
1.3.2 Names #
An event type reads as who wrote it, what it is about, and what
happened: <root>.<noun>[.<noun>…].<verb>, all lowercase kebab-case,
ending in a past-tense verb. kacs.audit.access.checked and
stratafs.file.copied-up are event types (PGSS §6.3).
A field is named for the part it plays, then what it is, then which
property of it the value is: subject.token.sid, object.file.path,
outcome.success (PGSS §6.4).
1.3.3 A path is nested maps #
A field's path is the chain of keys that leads to its value. Each segment is one map:
subject.token.sid is { "subject": { "token": { "sid": <bin> } } }
No key contains a .. A key "subject.token.sid" at the top level of a
payload is a different field from subject.token.sid, and not one any
consumer looks for.
A path is either a value or a map, never both. access.granted is a
value, so nothing is ever defined beneath it, and its variants are its
siblings: access.granted-staged, not access.granted.staged.
1.3.4 Absent, not nil #
A field with no value is omitted: the key is not written. An emitter
never writes nil, an empty string or zero to mean "none". A field whose
event page says when outcome.success == false is simply not in a
record that succeeded.
1.3.5 Value representations #
A field's type, given on its event page and in the field index, names its wire form.
| Type | Wire form |
|---|---|
bin.sid | bin holding the binary SID. |
bin.guid | bin, exactly 16 bytes. |
bin.ace | bin holding the binary ACE, copied from the descriptor. |
bin | bin: a digest or other opaque bytes. Shown as hexadecimal. |
uint.luid | uint. |
uint.time | uint, nanoseconds since the Unix epoch. |
uint.duration | uint, nanoseconds. |
uint.bytes | uint, a size in bytes. |
uint.mask | uint, an access mask, decoded against the table for the record's object.kind. |
uint.flags | uint, a set of bits the field's definition names. |
uint.integrity | uint, the integrity RID. |
uint.enum | uint, where the numbers are fixed by another specification. Rendered by name. |
int.errno | int, a negative error number. |
bool | bool. |
str.enum | str, a kebab-case name. |
str.path | str, a path. |
str.ip | str, an address in the textual form for its family. |
str | str, UTF-8 text. |
uint, int | uint or int. |
A type ending [] is an array of that type. Arrays that describe the
same items are parallel: subject.token.group-attributes[i] describes
subject.token.groups[i].
Binary SIDs, GUIDs and ACEs are carried as bytes rather than as text because they are compared as bytes. A principal is carried as its SID only, never with its name beside it: a name is resolved when the record is read.
1.3.6 Enumerations are kebab-case words #
An enumeration is a string naming the value — wrong-type, not 2.
Each one's values are listed in its definition, or by the event that
carries it, together with whether the set is closed. A value of an
open enumeration that a consumer does not recognise is a value, not an
error.
A uint.enum is used only where the numbers are fixed elsewhere — an
impersonation level, an address family, emitter.class — and a consumer
shows it by name.
1.3.7 Errors are negative errno #
outcome.errno is the negative error number the operation returned:
-13 for EACCES. It is omitted on success. Why an action failed, as a
word, is outcome.reason; whether it succeeded at all is
outcome.success (PGSS §6.6).
1.4 Reading an Event
Peios / Using Peios / Events / Introduction
Four rules govern every consumer of this stream.
1.4.1 Ignore unknown keys #
Future versions may add fields to an event without changing the existing ones. A consumer that processes the keys it knows and ignores the rest keeps working across upgrades. A consumer that rejects unrecognised keys breaks on the first addition.
1.4.2 Read absence by the field's presence #
A key that is absent means the emitter had no value for it (§1.3) —
nothing more. Each event page says when each field is present:
required, optional, or when a condition on another field holds.
Read absence against that, not as a signal of its own. A field that is
optional today may become always present later.
1.4.3 Delivery is best-effort #
KMES is a ring buffer. The kernel writes; keeping up is the subscriber's problem.
- A subscriber that falls behind loses events. eventd notices and
records an
eventd.events.lost, which is how a gap becomes visible rather than silent. - There is no replay. An event missed is gone. Nothing can ask for it back.
- Buffers are per-subscriber. One slow reader does not affect another.
- Order is per-subscriber, not global.
For durable audit, read events from eventd's stores rather than from KMES directly. eventd drains its subscription continuously and persists what it reads; from that point the store is the record, not the ring.
1.4.4 Events are not authenticated #
Events are trusted because they came through KMES, not because they are
signed. Nothing in an event carries a signature. What the kernel vouches
for is the header: emitter.class says which path wrote a record, and a
program can write any event type it likes with class 0 (§1.2).
Cryptographic non-repudiation is a userspace concern applied after events leave the kernel. If a deployment needs it, it is added on the far side of eventd, not here.
1.4.5 Versioning #
Event types are not versioned by a field. Some changes to the catalogue are additive and can arrive in any update: a new event type, a new field, a new value of an open enumeration. A consumer that ignores what it does not know, and treats an unknown value of an open enumeration as a value, keeps working across them.
Renaming an event type or a field, moving a field to another path, changing a field's type, or adding a value to a closed enumeration is not additive. Each needs a new version of the event vocabulary, because every query and descriptor written against the old shape stops matching (PGSS §6.11).
2.1 caller group
Peios / Using Peios / Events / Groups
- Group:
caller - Defined in:
lcs.evman
The identity LCS records on an audited operation. A deliberate subset of
subject — the token's durable GUIDs ride in the event header, and the
group SIDs and PIP state that KACS records are absent here.
That absence is a gap rather than a design. Access to a registry key is granted through a group as often as to a user SID directly, so an LCS record cannot answer "which membership let this through" where the equivalent KACS record can. The two subsystems reached the bound on what to serialise independently and landed in different places.
2.1.1 Fields #
| Field | Type | Meaning |
|---|---|---|
subject.token.sid | bin.sid | The user SID of the effective token the operation ran under. |
subject.token.integrity | uint.integrity | The integrity RID of the effective token. |
subject.token.id | uint.luid | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | The impersonation level of the effective token. |
2.1.2 Carried by #
lcs.audit.backup.endedlcs.audit.backup.startedlcs.audit.key.createdlcs.audit.key.deletedlcs.audit.key.descriptor.changedlcs.audit.key.hiddenlcs.audit.key.openedlcs.audit.key.tombstonedlcs.audit.restore.endedlcs.audit.restore.startedlcs.audit.transaction.committedlcs.audit.value.deletedlcs.audit.value.set
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
2.2 subject group
Peios / Using Peios / Events / Groups
- Group:
subject - Defined in:
kernel.evman
The security context an operation ran under, carried by every KACS event that has a causing principal. Three components: the effective token, the process it ran in, and the PIP state in force.
The process is the emitter's, under emitter.process, because a kernel
access check runs in the caller's own context: the process that acted is
the process that wrote the record.
The token and the process are different principals when impersonating.
The token is the client's and the process is the server's, so reading the
pair as one identity will attribute a client's work to a server or the
reverse. subject.token.type is what tells you they have diverged.
The durable GUIDs are not repeated here. emitter.token.guid,
emitter.true-token.guid and emitter.process.guid ride in the event
header, so they are present on every event whether or not this group is.
Privileges, claims, the restricted-SID list, confinement state and the default DACL are all deliberately absent. Each is unbounded and an event embedding them could grow without limit. Code needing full token state queries the token directly while it still exists; this group is for correlation, not reconstruction.
2.2.1 Fields #
| Field | Type | Meaning |
|---|---|---|
subject.token.sid | bin.sid | The user SID of the effective token the operation ran under. |
subject.token.groups | bin.sid[] | The group SIDs carried by the effective token. |
subject.token.group-attributes | uint.flags[] | Per-group attribute bitmasks, positionally parallel to subject.token.groups. |
subject.token.integrity | uint.integrity | The integrity RID of the effective token. |
subject.token.id | uint.luid | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | The impersonation level of the effective token. |
subject.token.uid | uint | The Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data. |
emitter.process.pid | uint | The process ID the record was written from. |
emitter.process.name | str | The kernel's name for the emitting process, typically the executable's basename. |
emitter.process.executable | str.path | The path the emitting process was executed from, resolved at exec with symbolic links already followed. |
subject.pip.type | uint.enum | The Protected Isolated Process type in force for the access check. |
subject.pip.trust | uint | The PIP trust level in force for the access check. |
2.2.2 Carried by #
kacs.audit.access.checkedkacs.audit.descriptor.changedkacs.audit.handle.usedkacs.audit.privilege.usedkacs.caap.policy.changedkacs.caap.sacl.skippedkacs.caap.staging.divergedkacs.descriptor.rejectedkacs.impersonation.revertedkacs.impersonation.startedkacs.mount.policy.changed
Generated from kernel.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
3.1 kacs.audit.access.checked
Peios / Using Peios / Events / Access and Identity Events
- Event type:
kacs.audit.access.checked - Defined in:
kacs.evman - Tier: essential
- Gating: a matching SACL audit ACE, or the token's audit policy; on the AccessCheck syscall a SACL record also needs the caller to hold SeAuditPrivilege, enabled
- Cardinality: once per matching audit ACE, not once per access
The record that an access check completed, and what it decided. The most common event on the system, and the one most investigations start from.
A daemon's own checks need SeAuditPrivilege. A userspace component that
guards its objects with the AccessCheck syscall supplies the descriptor
itself, SACL included. Its records are written only if the calling process
holds SeAuditPrivilege, enabled, on its effective token, as Windows asks of
AccessCheckAndAuditAlarm; without it the check is answered and its SACL
records are silently withheld. The privilege is the caller's, not the
checked client's. Records the checked token's own audit policy forces are
written either way.
It fires against every kind of object KACS protects — files, processes,
tokens, sockets and SysV IPC objects — which is why object.kind is
required. A process opening another process, or a token adjusting another
token, puts an actor and an object of the same kind in one record, so the
subject. and object. fields are never interchangeable.
It is silent by default. Nothing is recorded unless an audit ACE in the object's SACL matches, or the calling token's audit policy forces it. An object with no SACL produces no record on any outcome, including denials.
One access can produce several records. The event is per matching audit
ACE, so an access matching three ACEs produces three records differing only
in trigger.ace.
3.1.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via subject | The user SID of the effective token the operation ran under. |
subject.token.groups | bin.sid[] | via subject | The group SIDs carried by the effective token. |
subject.token.group-attributes | uint.flags[] | via subject | Per-group attribute bitmasks, positionally parallel to subject.token.groups. |
subject.token.integrity | uint.integrity | via subject | The integrity RID of the effective token. |
subject.token.id | uint.luid | via subject | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via subject | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via subject | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via subject | The impersonation level of the effective token. |
subject.token.uid | uint | via subject | The Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data. |
emitter.process.pid | uint | via subject | The process ID the record was written from. |
emitter.process.name | str | via subject | The kernel's name for the emitting process, typically the executable's basename. |
emitter.process.executable | str.path | via subject | The path the emitting process was executed from, resolved at exec with symbolic links already followed. |
subject.pip.type | uint.enum | via subject | The Protected Isolated Process type in force for the access check. |
subject.pip.trust | uint | via subject | The PIP trust level in force for the access check. |
object.kind | str.enum | required | What sort of thing the operation acted on. |
object.file.path | str.path | when object.kind == file | The absolute path of the file the operation acted on, resolved at the enforcement point. |
object.process.pid | uint | when object.kind == process | The process ID of the process the operation acted on. |
object.process.guid | bin.guid | when object.kind == process | The durable GUID of the process the operation acted on. |
object.token.id | uint.luid | when object.kind == token | The LUID of the token the operation acted on, for token-open and token-adjust checks. |
object.token.guid | bin.guid | when object.kind == token | The durable GUID of the token the operation acted on. |
access.requested | uint.mask | required | The access mask the caller asked for, after generic bits have been mapped to type-specific ones. |
access.granted | uint.mask | required | The mask this check granted. |
access.denied-integrity | uint.mask | optional | The requested bits the object's mandatory label withheld — every bit it withheld, for MAXIMUM_ALLOWED. Absent when it withheld none. With access.denied-trust, often the whole explanation of a denial the DACL alone would have allowed. |
access.denied-trust | uint.mask | optional | The requested bits the object's process-trust label withheld, read the same way. |
outcome.success | bool | required | True when every requested bit is in access.granted. A MAXIMUM_ALLOWED request always reports true, because such a request returns whatever is available and cannot fail — so a success rate computed over these records is skewed by them. |
trigger.kind | str.enum | required | Why an audit record exists at all. |
trigger.ace | bin.ace | when trigger.kind == sacl | The exact ACE that caused this record, so a consumer can identify which rule fired. |
fields.attestation.userspace | bool | optional | Set on every record of the AccessCheck syscall, and on no other. There the descriptor, and with it trigger.ace, is always the caller's, as are any audit context and PIP state it passes. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
3.2 kacs.audit.descriptor.changed
Peios / Using Peios / Events / Access and Identity Events
- Event type:
kacs.audit.descriptor.changed - Defined in:
kacs.evman - Tier: essential
- Gating: a change that includes the SACL, always; any other change when the rights it needed overlap the alarm mask on the handle it was made through
- Cardinality: once per authorised change
The record that a caller changed, or was authorised to change and failed to
change, the security descriptor of a file, a token, a process or a System V
IPC object. Registry keys have their own record of the same shape,
lcs.audit.key.descriptor.changed.
A change to a SACL is always recorded, whatever the old or new
descriptor says. Changing a SACL needs ACCESS_SYSTEM_SECURITY, which needs
SeSecurityPrivilege; it is rare; and removing one is how an intruder stops
being watched. Before this record existed a file's SACL could be removed in
silence, because the only audit of a descriptor change was evaluated
against the new SACL, which then had nothing in it.
Any other change — the owner, the group, the DACL, the integrity label — is
recorded when the rights it needed overlap the alarm mask on the handle it
was made through: the rule kacs.audit.handle.used follows. Only a file
handle carries such a mask, so for a token, a process or an IPC object only
a SACL change is recorded.
The descriptors themselves are not carried, only their SHA-256 digests, lengths and owners. Equal digests mean the change rewrote the descriptor without altering it.
3.2.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via subject | The user SID of the effective token the operation ran under. |
subject.token.groups | bin.sid[] | via subject | The group SIDs carried by the effective token. |
subject.token.group-attributes | uint.flags[] | via subject | Per-group attribute bitmasks, positionally parallel to subject.token.groups. |
subject.token.integrity | uint.integrity | via subject | The integrity RID of the effective token. |
subject.token.id | uint.luid | via subject | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via subject | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via subject | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via subject | The impersonation level of the effective token. |
subject.token.uid | uint | via subject | The Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data. |
emitter.process.pid | uint | via subject | The process ID the record was written from. |
emitter.process.name | str | via subject | The kernel's name for the emitting process, typically the executable's basename. |
emitter.process.executable | str.path | via subject | The path the emitting process was executed from, resolved at exec with symbolic links already followed. |
subject.pip.type | uint.enum | via subject | The Protected Isolated Process type in force for the access check. |
subject.pip.trust | uint | via subject | The PIP trust level in force for the access check. |
object.kind | str.enum | required | file, token, process or ipc. |
object.file.path | str.path | optional | On a file, its absolute path, when the handle's path could be resolved. |
object.token.id | uint.luid | when object.kind == token | The LUID of the token the operation acted on, for token-open and token-adjust checks. |
object.token.guid | bin.guid | when object.kind == token | The durable GUID of the token the operation acted on. |
object.process.guid | bin.guid | when object.kind == process | The durable GUID of the process the operation acted on. |
object.ipc.type | str.enum | when object.kind == ipc | Which kind of System V IPC object the operation acted on: a semaphore set, a shared-memory segment or a message queue. |
object.ipc.id | int | when object.kind == ipc | The System V IPC identifier of the object, as semget, shmget or msgget returned it. |
object.sd.components | uint.flags | required | The parts the caller set. A SACL bit means the record was not discretionary: such a change is always recorded. |
object.sd.length | uint.bytes | when outcome.success == true | The byte length of the object's security descriptor. |
object.sd.digest | bin | when outcome.success == true | SHA-256 of the descriptor as written. |
object.sd.owner | bin.sid | optional | The owner of the descriptor written, when it has one. |
object.sd.length-previous | uint.bytes | optional | The byte length of the descriptor a change replaced. |
object.sd.digest-previous | bin | optional | SHA-256 of the descriptor replaced. Absent, with its length and owner, where the object had no stored descriptor to replace. |
object.sd.owner-previous | bin.sid | optional | The owner SID a change replaced. |
access.requested | uint.mask | required | The rights the change needed: WRITE_OWNER, WRITE_DAC, ACCESS_SYSTEM_SECURITY, as the parts set ask. |
access.granted | uint.mask | optional | What the handle the change was made through was opened with. Absent on a change made without one. |
access.audit-mask | uint.mask | optional | The alarm mask on that handle. |
access.matched | uint.mask | optional | The part of access.requested the alarm mask matched, on a change made through a handle: zero when only the SACL made the record exist. |
outcome.success | bool | required | Whether the new descriptor was written. A refused change — one the caller had no right to make — is not this record; it is the access check's. |
outcome.errno | int.errno | when outcome.success == false | The error the operation failed with, as a negative errno. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
3.3 kacs.audit.handle.used
Peios / Using Peios / Events / Access and Identity Events
- Event type:
kacs.audit.handle.used - Defined in:
kacs.evman - Tier: essential
- Gating: the operation's required access overlaps the alarm mask cached on the handle
- Cardinality: once per operation on the handle
The record of what was done with a handle after it was opened. Where
kacs.audit.access.checked records the decision to open something, this
records each subsequent use of it.
The mask is configured by SYSTEM_ALARM* ACEs at the access check that
opened the handle, and the record is produced afterwards by whatever
enforcement point handles the operation. FACS, for file handles, is the
only such point today.
The subject is re-read on every operation. A handle outlives the token that opened it, so a process that starts or stops impersonating keeps its handles and every later operation is recorded under the token current at that moment. An investigation assuming the open-time identity will attribute later operations to the wrong principal.
Three masks appear together and are easy to confuse. Read as a sentence:
the operation needed access.requested, of which access.matched
triggered the audit, against a handle opened with access.granted, and it
succeeded or did not.
3.3.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via subject | The user SID of the effective token the operation ran under. |
subject.token.groups | bin.sid[] | via subject | The group SIDs carried by the effective token. |
subject.token.group-attributes | uint.flags[] | via subject | Per-group attribute bitmasks, positionally parallel to subject.token.groups. |
subject.token.integrity | uint.integrity | via subject | The integrity RID of the effective token. |
subject.token.id | uint.luid | via subject | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via subject | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via subject | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via subject | The impersonation level of the effective token. |
subject.token.uid | uint | via subject | The Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data. |
emitter.process.pid | uint | via subject | The process ID the record was written from. |
emitter.process.name | str | via subject | The kernel's name for the emitting process, typically the executable's basename. |
emitter.process.executable | str.path | via subject | The path the emitting process was executed from, resolved at exec with symbolic links already followed. |
subject.pip.type | uint.enum | via subject | The Protected Isolated Process type in force for the access check. |
subject.pip.trust | uint | via subject | The PIP trust level in force for the access check. |
object.kind | str.enum | required | Always file today, because FACS is the only enforcement point. |
object.file.path | str.path | required | The absolute path of the file the operation acted on, resolved at the enforcement point. |
operation.name | str.enum | required | Which operation was attempted. Values here (open set): file.access · file.mmap · file.mprotect · file.permission · file.write · file.ioctl · file.lock · file.fcntl · file.truncate · file.fallocate. |
access.requested | uint.mask | required | What this operation needs — a read needs FILE_READ_DATA, an mmap with PROT_EXEC needs FILE_EXECUTE. |
access.matched | uint.mask | required | The subset of the requested access that overlapped an audit mask and therefore caused the record to exist. |
access.granted | uint.mask | required | What the handle was opened with, not what this operation was granted. An operation can only succeed if access.requested is a subset of it. |
access.audit-mask | uint.mask | required | The full continuous-audit mask cached on a handle, of which access.matched is the part this operation touched. |
outcome.success | bool | required | Whether the operation itself was allowed. |
outcome.reason | str.enum | when outcome.success == false | From KACS_FSR_*, excluding DECISION (which names the ordinary path rather than a reason) and AUDIT_EMIT_FAIL (which cannot appear, being the failure of this record's own emission).Values here (open set): signed-exec · grant-deny · append-deny · unmanaged-sysfs. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
3.4 kacs.audit.privilege.used
Peios / Using Peios / Events / Access and Identity Events
- Event type:
kacs.audit.privilege.used - Defined in:
kacs.evman - Tier: essential
- Gating: the token's audit policy, success and failure flavours independently; at a capability or volume gate, also the first use of that gate by the process under its current token, and never a CAP_OPT_NOAUDIT check
- Cardinality: once per privilege that contributed access; at a gate, once per process, token and gate
The record that a privilege was spent. Purely token-policy driven — no SACL
is involved, so there is no trigger to record. operation.name says where
it was spent, and the rest of the record follows from it.
access-check is a privilege that contributed access to an access check,
with whether that contribution survived. Success there means the
privilege worked, not that it fired. This is the field most often
misread. A false outcome is not a failed attempt to use a privilege; it is
a privilege that fired and was then overridden by a confinement, a CAAP
rule, or PIP non-dominance — usually the more interesting of the two
records, because it shows a boundary holding. MAXIMUM_ALLOWED requests
produce nothing here. Privilege-use auditing short-circuits on them, even
though SACL auditing was deliberately changed not to — a probing enumerator
therefore trips one and slips the other.
linux-cap is a privilege that satisfied a Linux capability check: KACS
answers every capability check with a privilege, and linux.cap names the
check. volume-mount is a privilege that satisfied the volume-management
gate the mount paths ask instead of CAP_SYS_ADMIN. Each is recorded once
per process, token and gate, not once per check: a TCB process makes
thousands of CAP_SYS_ADMIN checks a second, and the first answers who used
the privilege and for what. The set starts again when the process acts under
another token. Only successful uses are recorded, and only those of a user
process's own checks: a probe with CAP_OPT_NOAUDIT, a kernel thread's
check and a check against another process's credentials record nothing.
The record is written as the process returns to user space, so it follows
the system call that spent the privilege.
A token can carry either audit policy flavour, both, or neither, and each
governs its own outcome independently. The boot SYSTEM token carries
PRIVILEGE_USE_SUCCESS, so on a stock machine the records come from
SYSTEM and the services that inherit its token.
3.4.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via subject | The user SID of the effective token the operation ran under. |
subject.token.groups | bin.sid[] | via subject | The group SIDs carried by the effective token. |
subject.token.group-attributes | uint.flags[] | via subject | Per-group attribute bitmasks, positionally parallel to subject.token.groups. |
subject.token.integrity | uint.integrity | via subject | The integrity RID of the effective token. |
subject.token.id | uint.luid | via subject | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via subject | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via subject | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via subject | The impersonation level of the effective token. |
subject.token.uid | uint | via subject | The Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data. |
emitter.process.pid | uint | via subject | The process ID the record was written from. |
emitter.process.name | str | via subject | The kernel's name for the emitting process, typically the executable's basename. |
emitter.process.executable | str.path | via subject | The path the emitting process was executed from, resolved at exec with symbolic links already followed. |
subject.pip.type | uint.enum | via subject | The Protected Isolated Process type in force for the access check. |
subject.pip.trust | uint | via subject | The PIP trust level in force for the access check. |
operation.name | str.enum | required | Which operation was attempted. Values here (open set): access-check · linux-cap · volume-mount. |
object.kind | str.enum | optional | On an access check, the object checked. Absent at a gate, and on a check made through the AccessCheck syscall with no audit context: the kernel does not know what was checked. |
object.file.path | str.path | when object.kind == file | The absolute path of the file the operation acted on, resolved at the enforcement point. |
object.process.pid | uint | when object.kind == process | The process ID of the process the operation acted on. |
object.process.guid | bin.guid | when object.kind == process | The durable GUID of the process the operation acted on. |
object.token.id | uint.luid | when object.kind == token | The LUID of the token the operation acted on, for token-open and token-adjust checks. |
object.token.guid | bin.guid | when object.kind == token | The durable GUID of the token the operation acted on. |
privilege.name | str.enum | required | The privilege a record is about, by its Windows name. |
privilege.contributed | uint.mask | when operation.name == access-check | The access bits this privilege supplied to the check, intersected with what the caller actually asked for. |
privilege.surviving | uint.mask | when operation.name == access-check | The subset of privilege.contributed that reached the final granted mask. |
linux.cap | str.enum | when operation.name == linux-cap | The Linux capability whose check prompted the record, named as its CAP_* constant with the prefix removed, in kebab case: CAP_NET_ADMIN is net-admin. |
access.requested | uint.mask | when operation.name == access-check | The whole check's requested mask, not this privilege's share of it. |
access.granted | uint.mask | when operation.name == access-check | The whole check's final granted mask. |
outcome.success | bool | required | On an access check, true when privilege.surviving is non-empty: derived rather than independently decided, and kept so that a single query for failed outcomes works across every event type. At a gate, always true. |
fields.attestation.userspace | bool | optional | Set on every record of the AccessCheck syscall, and on no other. There the descriptor, and with it trigger.ace, is always the caller's, as are any audit context and PIP state it passes. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
3.5 kacs.caap.policy.changed
Peios / Using Peios / Events / Access and Identity Events
- Event type:
kacs.caap.policy.changed - Defined in:
kacs.evman - Tier: essential
- Gating: none — every change a caller holding SeTcbPrivilege makes is recorded
- Cardinality: once per kacs_set_caap call that passed the privilege gate
The record that a central access policy was installed, replaced or removed. A policy decides access to every object whose SACL names it, alongside that object's own DACL, so one change here can widen or narrow access across the whole system without touching any object.
Only a caller holding SeTcbPrivilege, enabled, may change a policy; a call refused for want of it is not recorded. A call that passed the gate is recorded whether or not it applied: a malformed specification leaves the old policy in force and the record says so.
The record names the policy, not its rules. Compare two records' effects by the access records that follow them.
3.5.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via subject | The user SID of the effective token the operation ran under. |
subject.token.groups | bin.sid[] | via subject | The group SIDs carried by the effective token. |
subject.token.group-attributes | uint.flags[] | via subject | Per-group attribute bitmasks, positionally parallel to subject.token.groups. |
subject.token.integrity | uint.integrity | via subject | The integrity RID of the effective token. |
subject.token.id | uint.luid | via subject | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via subject | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via subject | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via subject | The impersonation level of the effective token. |
subject.token.uid | uint | via subject | The Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data. |
emitter.process.pid | uint | via subject | The process ID the record was written from. |
emitter.process.name | str | via subject | The kernel's name for the emitting process, typically the executable's basename. |
emitter.process.executable | str.path | via subject | The path the emitting process was executed from, resolved at exec with symbolic links already followed. |
subject.pip.type | uint.enum | via subject | The Protected Isolated Process type in force for the access check. |
subject.pip.trust | uint | via subject | The PIP trust level in force for the access check. |
caap.policy.sid | bin.sid | required | The SID identifying a central access policy. |
operation.name | str.enum | required | set installs a policy, or replaces the one of the same SID whole; remove takes it away, after which objects naming it are decided by the recovery policy.Values here (closed set): set · remove. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.errno | int.errno | when outcome.success == false | The error the operation failed with, as a negative errno. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
3.6 kacs.caap.sacl.skipped
Peios / Using Peios / Events / Access and Identity Events
- Event type:
kacs.caap.sacl.skipped - Defined in:
kacs.evman - Tier: standard
- Gating: an access check that evaluates a central access policy rule whose SACL cannot be used
- Cardinality: once per rule whose SACL failed
The record that a central access policy rule's SACL could not be parsed or evaluated, so the audit it asked for was skipped. The access decision is unaffected — a rule's SACL only adds audit records — but the records that rule should have produced for this access do not exist.
This is the only evidence of the missing audit. An administrator who put an audit ACE in a policy rule expects records from every object the policy covers; while the rule's SACL is broken, none arrive, and nothing else says so.
A failure in a rule's staged SACL (caap.phase is staged-sacl) is
recorded too. It changes nothing in force, and is the warning that
promoting the staged policy would install a SACL that does not work.
3.6.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via subject | The user SID of the effective token the operation ran under. |
subject.token.groups | bin.sid[] | via subject | The group SIDs carried by the effective token. |
subject.token.group-attributes | uint.flags[] | via subject | Per-group attribute bitmasks, positionally parallel to subject.token.groups. |
subject.token.integrity | uint.integrity | via subject | The integrity RID of the effective token. |
subject.token.id | uint.luid | via subject | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via subject | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via subject | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via subject | The impersonation level of the effective token. |
subject.token.uid | uint | via subject | The Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data. |
emitter.process.pid | uint | via subject | The process ID the record was written from. |
emitter.process.name | str | via subject | The kernel's name for the emitting process, typically the executable's basename. |
emitter.process.executable | str.path | via subject | The path the emitting process was executed from, resolved at exec with symbolic links already followed. |
subject.pip.type | uint.enum | via subject | The Protected Isolated Process type in force for the access check. |
subject.pip.trust | uint | via subject | The PIP trust level in force for the access check. |
object.kind | str.enum | required | What sort of thing the operation acted on. |
object.file.path | str.path | when object.kind == file | The absolute path of the file the operation acted on, resolved at the enforcement point. |
object.process.pid | uint | when object.kind == process | The process ID of the process the operation acted on. |
object.process.guid | bin.guid | when object.kind == process | The durable GUID of the process the operation acted on. |
object.token.id | uint.luid | when object.kind == token | The LUID of the token the operation acted on, for token-open and token-adjust checks. |
object.token.guid | bin.guid | when object.kind == token | The durable GUID of the token the operation acted on. |
caap.policy.sid | bin.sid | required | The SID identifying a central access policy. |
caap.rule.index | uint | required | The zero-based position of a rule within the central access policy named by caap.policy.sid. |
caap.phase | str.enum | required | Which phase of a central access policy's audit evaluation a CAAP diagnostic describes. |
outcome.reason | str.enum | required | invalid-caap-sacl means the SACL would not parse; caap-sacl-evaluation-error means it parsed and evaluating it failed.Values here (open set): invalid-caap-sacl · caap-sacl-evaluation-error. |
access.requested | uint.mask | required | The access mask the caller asked for, after generic bits have been mapped to type-specific ones. |
access.granted | uint.mask | required | The effective grant of the check the rule was evaluated in. |
access.granted-staged | uint.mask | required | What a staged central access policy, not yet in force, would have granted. |
fields.attestation.userspace | bool | optional | Set on every record of the AccessCheck syscall, and on no other. There the descriptor, and with it trigger.ace, is always the caller's, as are any audit context and PIP state it passes. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
3.7 kacs.caap.staging.diverged
Peios / Using Peios / Events / Access and Identity Events
- Event type:
kacs.caap.staging.diverged - Defined in:
kacs.evman - Tier: standard
- Gating: a staged central access policy applies to the object, and its result differs from the effective one
- Cardinality: once per access check
The record that a staged central access policy would have decided an access differently from the policy in force. The access itself is decided by the effective policy alone; this records what promoting the staged one would change, for this subject and this object.
It is the event to watch while trialling a policy: stage it, let the system run, and read these records to see who would gain or lose access before anyone does. It is silent unless a staged policy exists, so on most systems it never fires.
The difference is not always in the grant. Staged and effective
evaluation also differ when the per-object results of an object-type
check differ, or when the audit records the two would produce differ, so
a record can carry equal access.granted and access.granted-staged.
3.7.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via subject | The user SID of the effective token the operation ran under. |
subject.token.groups | bin.sid[] | via subject | The group SIDs carried by the effective token. |
subject.token.group-attributes | uint.flags[] | via subject | Per-group attribute bitmasks, positionally parallel to subject.token.groups. |
subject.token.integrity | uint.integrity | via subject | The integrity RID of the effective token. |
subject.token.id | uint.luid | via subject | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via subject | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via subject | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via subject | The impersonation level of the effective token. |
subject.token.uid | uint | via subject | The Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data. |
emitter.process.pid | uint | via subject | The process ID the record was written from. |
emitter.process.name | str | via subject | The kernel's name for the emitting process, typically the executable's basename. |
emitter.process.executable | str.path | via subject | The path the emitting process was executed from, resolved at exec with symbolic links already followed. |
subject.pip.type | uint.enum | via subject | The Protected Isolated Process type in force for the access check. |
subject.pip.trust | uint | via subject | The PIP trust level in force for the access check. |
object.kind | str.enum | required | What sort of thing the operation acted on. |
object.file.path | str.path | when object.kind == file | The absolute path of the file the operation acted on, resolved at the enforcement point. |
object.process.pid | uint | when object.kind == process | The process ID of the process the operation acted on. |
object.process.guid | bin.guid | when object.kind == process | The durable GUID of the process the operation acted on. |
object.token.id | uint.luid | when object.kind == token | The LUID of the token the operation acted on, for token-open and token-adjust checks. |
object.token.guid | bin.guid | when object.kind == token | The durable GUID of the token the operation acted on. |
access.requested | uint.mask | required | The access mask the caller asked for, after generic bits have been mapped to type-specific ones. |
access.granted | uint.mask | required | What the effective policy granted: the access the caller actually got. |
access.granted-staged | uint.mask | required | What the staged policy would have granted instead. |
fields.attestation.userspace | bool | optional | Set on every record of the AccessCheck syscall, and on no other. There the descriptor, and with it trigger.ace, is always the caller's, as are any audit context and PIP state it passes. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
3.8 kacs.config.value.rejected
Peios / Using Peios / Events / Access and Identity Events
- Event type:
kacs.config.value.rejected - Defined in:
kacs.evman - Tier: essential
- Gating: none — every rejected table is recorded
- Cardinality: once per rejected read of the key
The record that KACS read one of its registry-held tables, rejected it, and
carried on with what it had. Today that is the port reservation table under
Machine\System\Network\TcpIp\PortReservations, read whenever the key
changes.
The table is validated whole and is reject-or-keep, never merge: one bad
value refuses all of them, and whatever was in force before — the last good
table from the registry, or the compiled-in fallback — stays in force. That
makes this the only place the divergence is visible: the registry shows what
an administrator wrote, and this record shows that KACS is not using it. The
same reject-or-keep rule as kmes.config.value.rejected and
lcs.config.value.rejected, for a table rather than a single value.
3.8.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
config.key.path | str | required | The registry key the reported value lives under, in canonical form. |
config.name | str | optional | The value at fault, by its registry name: a selector such as tcp:80, or @ for the default reservation. Absent when no single value is at fault. |
outcome.reason | str.enum | required | empty: the key has no values. too-large: the table exceeds the kernel's bound. malformed: the serialised table broke, which is the kernel's fault rather than a value's. bad-selector: a value's name does not parse as a selector. bad-descriptor: a value's data is not a self-relative security descriptor. duplicate-default: the default reservation is given twice. missing-default: there is no default reservation. overlap: two selectors of the same width cover a port, so the most specific is ambiguous.Values here (open set): empty · too-large · malformed · bad-selector · bad-descriptor · duplicate-default · missing-default · overlap. |
outcome.errno | int.errno | required | The error the operation failed with, as a negative errno. |
policy.previous-retained | bool | required | Always true: a rejected table leaves the one in force in place. |
policy.fallback | bool | required | Whether what stays in force is the compiled-in fallback, because no table from the registry was ever accepted this boot. The fallback lets SYSTEM alone bind a port, so a rejected first table leaves every service that binds a port without one. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
3.9 kacs.descriptor.rejected
Peios / Using Peios / Events / Access and Identity Events
- Event type:
kacs.descriptor.rejected - Defined in:
kacs.evman - Tier: standard
- Gating: none — every unreadable stored descriptor is recorded
- Cardinality: one record per occurrence
The record that KACS read an object's stored security descriptor, found it corrupt, and refused to use it. The object is then treated as having a descriptor nobody can satisfy, so every access to it is denied until the descriptor is replaced by someone allowed to write one.
This is often the only explanation for a sudden, universal denial. A file that everyone could open and nobody now can, with no change to its descriptor anyone remembers making, is usually this: the stored bytes no longer parse, whether from disk corruption, a tool writing the extended attribute directly, or an interrupted write.
KACS records the event once per descriptor read, when the object's descriptor cache is filled, not on every denied access that follows. The subject is whoever's access made KACS read the descriptor: the first to touch the file after it was cached, not whoever corrupted it.
3.9.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via subject | The user SID of the effective token the operation ran under. |
subject.token.groups | bin.sid[] | via subject | The group SIDs carried by the effective token. |
subject.token.group-attributes | uint.flags[] | via subject | Per-group attribute bitmasks, positionally parallel to subject.token.groups. |
subject.token.integrity | uint.integrity | via subject | The integrity RID of the effective token. |
subject.token.id | uint.luid | via subject | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via subject | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via subject | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via subject | The impersonation level of the effective token. |
subject.token.uid | uint | via subject | The Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data. |
emitter.process.pid | uint | via subject | The process ID the record was written from. |
emitter.process.name | str | via subject | The kernel's name for the emitting process, typically the executable's basename. |
emitter.process.executable | str.path | via subject | The path the emitting process was executed from, resolved at exec with symbolic links already followed. |
subject.pip.type | uint.enum | via subject | The Protected Isolated Process type in force for the access check. |
subject.pip.trust | uint | via subject | The PIP trust level in force for the access check. |
object.kind | str.enum | required | Always file today: file descriptors are the only stored descriptors KACS reads this way. |
object.file.path | str.path | optional | Not carried today. The descriptor is read through an anchor that has no mount, so no absolute path can be resolved at that point. |
object.file.inode | uint | required | The inode number the object presents. |
object.file.device | uint | required | With object.file.inode, what names the file in place of a path: find it with find <mount> -xdev -inum <inode> on the filesystem of that device. |
object.sd.length | uint.bytes | required | The length of the stored attribute that failed: 0 when it was empty, and more than 65,535 when it was too long to be a descriptor at all. |
outcome.reason | str.enum | required | Why the descriptor was refused. corrupt is the only reason today.Values here (open set): corrupt. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
3.10 kacs.impersonation.reverted
Peios / Using Peios / Events / Access and Identity Events
- Event type:
kacs.impersonation.reverted - Defined in:
kacs.evman - Tier: verbose
- Gating: none — every impersonation that ends is recorded
- Cardinality: once per impersonation ended
The record that a thread stopped impersonating and acts as its own token
again. The subject is the thread's token after the revert; the object is
the impersonation token it gave up, which a kacs.impersonation.started
record named.
Verbose, because it is bookkeeping. An API that impersonates per request reverts once per request too, and the start already carries the security decision.
Not every end is recorded. A thread that exits while impersonating writes nothing: its credentials are released where no record can be built. And installing a new primary token does not end an impersonation — the impersonation is put back over the new token — so it writes nothing either.
3.10.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via subject | The user SID of the effective token the operation ran under. |
subject.token.groups | bin.sid[] | via subject | The group SIDs carried by the effective token. |
subject.token.group-attributes | uint.flags[] | via subject | Per-group attribute bitmasks, positionally parallel to subject.token.groups. |
subject.token.integrity | uint.integrity | via subject | The integrity RID of the effective token. |
subject.token.id | uint.luid | via subject | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via subject | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via subject | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via subject | The impersonation level of the effective token. |
subject.token.uid | uint | via subject | The Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data. |
emitter.process.pid | uint | via subject | The process ID the record was written from. |
emitter.process.name | str | via subject | The kernel's name for the emitting process, typically the executable's basename. |
emitter.process.executable | str.path | via subject | The path the emitting process was executed from, resolved at exec with symbolic links already followed. |
subject.pip.type | uint.enum | via subject | The Protected Isolated Process type in force for the access check. |
subject.pip.trust | uint | via subject | The PIP trust level in force for the access check. |
emitter.thread.tid | uint | required | The thread that wrote the record, where the operation is scoped to a thread rather than to its process. |
object.kind | str.enum | required | Always token. |
object.token.guid | bin.guid | required | The impersonation token given up. |
object.token.sid | bin.sid | required | The user SID of the token the operation acted on. |
operation.name | str.enum | required | revert is an explicit kacs_revert; exec the revert every exec performs before the new image runs; replaced the implicit end of one impersonation when the thread starts another.Values here (open set): revert · exec · replaced. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.errno | int.errno | when outcome.success == false | The error the operation failed with, as a negative errno. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
3.11 kacs.impersonation.started
Peios / Using Peios / Events / Access and Identity Events
- Event type:
kacs.impersonation.started - Defined in:
kacs.evman - Tier: standard
- Gating: none — every impersonation attempt that names a client token is recorded
- Cardinality: once per attempt
The record that a thread took on, or tried to take on, a client's identity by impersonating its token. The subject is the server — the token that acted, as it stood before the switch — and the object is the client token. Successes and refusals are both recorded.
Impersonation is per thread, so emitter.thread.tid names the thread that
changed identity, not emitter.process.pid. Records the thread writes
afterwards carry the client's token in their header, until a
kacs.impersonation.reverted record ends it.
The level can be lowered without the attempt failing.
object.token.impersonation is the level the client token offered, and
object.token.impersonation-permitted the level KACS let the server have.
Where the second is lower — the server lacked SeImpersonatePrivilege for a
different user, or the client's integrity was above the server's — the
impersonation succeeded at Identification, which can query the client's
identity but not act as it.
The GUID is the installed token's. A lowered level installs a new token,
so object.token.guid and object.token.id on a success are that token's,
and they match the emitter.token.guid of the thread's later records.
Exactly when the level was lowered they differ from the GUID of the token
the client handed over.
3.11.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via subject | The user SID of the effective token the operation ran under. |
subject.token.groups | bin.sid[] | via subject | The group SIDs carried by the effective token. |
subject.token.group-attributes | uint.flags[] | via subject | Per-group attribute bitmasks, positionally parallel to subject.token.groups. |
subject.token.integrity | uint.integrity | via subject | The integrity RID of the effective token. |
subject.token.id | uint.luid | via subject | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via subject | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via subject | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via subject | The impersonation level of the effective token. |
subject.token.uid | uint | via subject | The Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data. |
emitter.process.pid | uint | via subject | The process ID the record was written from. |
emitter.process.name | str | via subject | The kernel's name for the emitting process, typically the executable's basename. |
emitter.process.executable | str.path | via subject | The path the emitting process was executed from, resolved at exec with symbolic links already followed. |
subject.pip.type | uint.enum | via subject | The Protected Isolated Process type in force for the access check. |
subject.pip.trust | uint | via subject | The PIP trust level in force for the access check. |
emitter.thread.tid | uint | required | The thread that wrote the record, where the operation is scoped to a thread rather than to its process. |
object.kind | str.enum | required | Always token. |
object.token.guid | bin.guid | required | The durable GUID of the token the operation acted on. |
object.token.id | uint.luid | required | The LUID of the token the operation acted on, for token-open and token-adjust checks. |
object.token.sid | bin.sid | required | The client: whose identity the server took on or asked for. |
object.token.type | str.enum | required | primary only on a refusal: a primary token cannot be impersonated. |
object.token.integrity | uint.integrity | required | The integrity level of the token the operation acted on, as an integrity RID. |
object.token.auth-id | uint.luid | optional | Absent only for a token with no logon session. |
object.token.restricted | bool | required | Whether the token is restricted: whether it carries restricting SIDs that every access check must also satisfy. |
object.token.impersonation | uint.enum | required | The level the client token offered: the level asked for. |
object.token.impersonation-permitted | uint.enum | optional | The level the gate allowed. Absent when the attempt was refused before the gate answered. |
privilege.name | str.enum | optional | SeImpersonatePrivilege, present when the server impersonated a different user, or across a restriction difference, and needed the privilege to do it at the level asked. The privilege is then marked used. |
privilege.held | bool | when privilege.name == SeImpersonatePrivilege | Always true where present: a server without the privilege is not refused but lowered to Identification, which impersonation-permitted shows. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.errno | int.errno | when outcome.success == false | The error the operation failed with, as a negative errno. |
outcome.reason | str.enum | optional | On a refusal, where KACS knows why: the token handed over was a primary token (EINVAL); a restricted server tried to impersonate an unrestricted token of its own user, which would escape its restriction (EPERM); the token handle lacked TOKEN_IMPERSONATE (EACCES); or the identity could not be projected onto the thread's Linux credentials. Absent on a refusal for want of memory.Values here (open set): primary-token · restriction-escape · no-impersonate-access · projection-failed. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
3.12 kacs.mount.policy.changed
Peios / Using Peios / Events / Access and Identity Events
- Event type:
kacs.mount.policy.changed - Defined in:
kacs.evman - Tier: essential
- Gating: none — every change a caller admitted by the volume-management gate makes is recorded
- Cardinality: once per change
The record that a filesystem's KACS mount policy was set: what happens to an
object on it that has no stored descriptor. unmanaged turns access control
off for the whole filesystem; deny-missing refuses such objects; the two
synthesize policies build a descriptor from the parent and the template
the caller supplies.
Setting a policy needs SeManageVolumePrivilege or SeTcbPrivilege. Every other refusal comes before that check, so a record is always of a change made.
The record does not carry the template. A synthesising policy's template
descriptor decides what every unlabelled object on the filesystem grants;
read it back with kacs_get_mount_policy.
3.12.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via subject | The user SID of the effective token the operation ran under. |
subject.token.groups | bin.sid[] | via subject | The group SIDs carried by the effective token. |
subject.token.group-attributes | uint.flags[] | via subject | Per-group attribute bitmasks, positionally parallel to subject.token.groups. |
subject.token.integrity | uint.integrity | via subject | The integrity RID of the effective token. |
subject.token.id | uint.luid | via subject | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via subject | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via subject | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via subject | The impersonation level of the effective token. |
subject.token.uid | uint | via subject | The Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data. |
emitter.process.pid | uint | via subject | The process ID the record was written from. |
emitter.process.name | str | via subject | The kernel's name for the emitting process, typically the executable's basename. |
emitter.process.executable | str.path | via subject | The path the emitting process was executed from, resolved at exec with symbolic links already followed. |
subject.pip.type | uint.enum | via subject | The Protected Isolated Process type in force for the access check. |
subject.pip.trust | uint | via subject | The PIP trust level in force for the access check. |
object.mount.fs-type | str | optional | The filesystem's type, when the kernel knows it. |
object.mount.policy | str.enum | required | The KACS mount policy in force, which decides what happens to an object with no stored descriptor. |
object.mount.policy-previous | str.enum | required | The mount policy a change replaced, beside object.mount.policy, the one it put in force. |
object.mount.policy-generation | uint | required | The generation the change began. Every descriptor cached under an older one is discarded on its next use. |
outcome.success | bool | required | Always true: every refusal is decided before anything changes. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
3.13 kacs.session.destroyed
Peios / Using Peios / Events / Access and Identity Events
- Event type:
kacs.session.destroyed - Defined in:
kacs.evman - Tier: essential
- Gating: none — every logon session that ends is recorded
- Cardinality: one record per occurrence
The record that a logon session ended because its last token went away. KACS destroys a session once no token refers to it, or once the only tokens left are its own linked elevated and filtered pair with nothing else holding them, and writes this record as it does.
A session that never acquired a token ends another way: a holder of
SeTcbPrivilege destroys it explicitly with kacs_destroy_empty_logon_session,
and the same record is written.
A session whose authentication package name is not valid UTF-8 produces no record. KACS cannot encode the name and skips the event rather than write it, and the session is destroyed regardless.
The record follows the teardown, written by a kernel worker. A session usually ends when a freed credential drops its last token, at a point where the record cannot be built, so KACS queues it and a kernel work item writes it moments later. Its header therefore names that kernel worker rather than the process whose exit or revert ended the session, and it can arrive after records that process wrote later.
3.13.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.session.id | uint.luid | required | The LUID of the logon session the operation acted on. |
object.session.user.sid | bin.sid | required | The user SID of the principal the logon session belongs to: whose session it is. |
object.session.logon-type | str.enum | required | What kind of logon the session represents, from the KACS_LOGON_TYPE_* values in uapi/pkm/token.h. |
object.session.auth-package | str | required | The name of the authentication package that established the session, as the creator supplied it. |
object.session.logon-time | uint.time | required | When the session was created. KACS keeps it to the second, so the nanosecond value is always a whole number of seconds. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
3.14 kacs.signature.crypto.failed
Peios / Using Peios / Events / Access and Identity Events
- Event type:
kacs.signature.crypto.failed - Defined in:
kacs.evman - Tier: essential
- Gating: none — recorded whenever the signature machinery cannot run
- Cardinality: one record per occurrence
The record that the machinery for verifying signed executables cannot run. From then on every signed executable is refused: a signature that cannot be verified fails the exec rather than passing as unsigned.
Recorded at boot when the probe for the ML-DSA-65 transform fails. KACS cannot refuse to start over it — a security module that fails to initialise boots with no protection at all, which is worse — so it reports instead, and the refusal happens at each exec. Without this record, the condition would show only as every process running without an integrity label.
3.14.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
signature.crypto-stage | str.enum | required | Always boot-probe today. |
outcome.errno | int.errno | required | The error the operation failed with, as a negative errno. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
4.1 stratafs.file.copied-up
Peios / Using Peios / Events / Filesystem Events
- Event type:
stratafs.file.copied-up - Defined in:
stratafs.evman - Tier: standard
- Gating: none — every copy-up is recorded
- Cardinality: one record per occurrence
The record that StrataFS materialised an object into a writable stratum, successful or not. Copy-up is how a layered mount turns a read-only object into a writable one, so this is the record of a file coming into existence in a place it did not previously exist.
The record is written through KACS, so its emitter.class is kacs.
There is no subject in the payload, and that is not an omission.
Copy-up preserves the source object's descriptor, so the resulting file
records nothing about who caused it to exist. The actor is identified by
the header instead: copy-up runs in the caller's own context, so
emitter.token.guid and emitter.process.guid are the caller's.
That identification is currently weaker than it looks. The header carries GUIDs and nothing in the stream binds a GUID to a SID, an image or a process name, so an operator reading these records can group them by actor but cannot say who the actor was.
ENOTDIR from a failed parent materialisation surfaces here, in
outcome.errno, rather than as a refusal record. Searching
stratafs.mutation.refused for it finds nothing; the record exists under
this type with every field present.
4.1.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.file.path-relative | str.path | optional | Absent only from a reduced record: when the memory for the full record could not be had, the kernel writes the record without its paths rather than lose it. |
source.stratum.index | uint | required | Which stratum an object was read from, by index within the mount's stack. |
source.stratum.path | str.path | optional | Absent only from a reduced record, with the other paths. |
destination.stratum.index | uint | required | Which stratum an object was written into, by index within the mount's stack. |
destination.stratum.path | str.path | optional | Absent only from a reduced record, with the other paths. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.errno | int.errno | when outcome.success == false | The error the operation failed with, as a negative errno. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from stratafs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
4.2 stratafs.mutation.refused
Peios / Using Peios / Events / Filesystem Events
- Event type:
stratafs.mutation.refused - Defined in:
stratafs.evman - Tier: standard
- Gating: none — every arrangement refusal is recorded
- Cardinality: one record per occurrence
The record that a mutation was refused because of how the mount is arranged, rather than because of an access check. Software writing where it cannot, or an arrangement that does not admit an operation someone expected — a mismatch between intent and configuration that is otherwise visible only as an error returned to a caller who may discard it.
The record is written through KACS, so its emitter.class is kacs.
The refusals that count are one explicit list: EROFS, EXDEV, ENOTDIR,
EISDIR, ENOTEMPTY, EEXIST and EINVAL. Call sites cover every
mutating path.
EACCES is deliberately absent. A refusal produced by an access check
is recorded by the mechanism that performed it, so StrataFS does not
duplicate it. Looking here for a permission denial finds nothing — look at
kacs.audit.access.checked.
There is one exception, and it is deliberate. A refused deferred deletion is recorded on any non-zero result, including one refused by an access check, because by the time a deferred deletion fails there is nobody left to receive the error. Rollbacks arrive under this type with the same flag set: a create or link whose outer bookkeeping failed and whose lower object could not be removed again, and a failed publication rollback after a copy-up.
Resolution, revalidation and enumeration record nothing at all. They happen on every path operation, reveal nothing the resulting access check does not, and recording them would produce volume out of all proportion to their significance.
4.2.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.file.path-relative | str.path | optional | Absent only from a reduced record: when the memory for the full record could not be had, the kernel writes the record without its paths rather than lose it. |
operation.name | str.enum | required | StrataFS names its own operations, and the set is disjoint from the file. names an already-open handle uses.Values here (open set): copy-file-range · copy-up-publication-rollback · create · create-rollback · fallocate · link · link-rollback · mmap · remap-file-range · removexattr · rename · rmdir · setattr · setxattr · splice-write · supersede · supersede-removal · tmpfile · truncate · unlink · write. |
source.stratum.index | uint | optional | Absent when the refusal was raised before any provider was determined. |
source.stratum.path | str.path | optional | Present exactly when source.stratum.index is, except in a reduced record, which carries no paths. |
outcome.errno | int.errno | required | The error the operation failed with, as a negative errno. |
outcome.deferred | bool | required | Whether the refusal was deferred — set on rollbacks and on deferred deletions, which are the records where no caller remained to be told. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from stratafs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
5.1 lcs.audit.backup.ended
Peios / Using Peios / Events / Registry Events
- Event type:
lcs.audit.backup.ended - Defined in:
lcs.evman - Tier: essential
- Gating: none — audited unconditionally
- Cardinality: one record per occurrence
The record that a registry subtree backup finished, successfully or
otherwise. Pairs with lcs.audit.backup.started on the same key.
Emission failure here does not affect the result, which has already been determined by the time this is written.
5.1.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via caller | The user SID of the effective token the operation ran under. |
subject.token.integrity | uint.integrity | via caller | The integrity RID of the effective token. |
subject.token.id | uint.luid | via caller | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via caller | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via caller | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via caller | The impersonation level of the effective token. |
object.key.guid | bin.guid | required | The registry key an operation acted on. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.errno | int.errno | when outcome.success == false | The error the operation failed with, as a negative errno. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
5.2 lcs.audit.backup.started
Peios / Using Peios / Events / Registry Events
- Event type:
lcs.audit.backup.started - Defined in:
lcs.evman - Tier: essential
- Gating: none — audited unconditionally
- Cardinality: one record per occurrence
The record that a registry subtree backup began, emitted before any data is read. Backup is a privilege-gated bulk operation that bypasses per-key access checks entirely, so this trail is the only evidence it happened at all — which is why it is audited whatever the target key's SACL says.
The start record exists so that an operation dying partway still leaves evidence that it began. A start with no matching end is itself the finding.
Emission failure fails the operation with EIO, unlike every other LCS
event: an unrecorded bulk read of the configuration store is not permitted
to proceed.
5.2.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via caller | The user SID of the effective token the operation ran under. |
subject.token.integrity | uint.integrity | via caller | The integrity RID of the effective token. |
subject.token.id | uint.luid | via caller | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via caller | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via caller | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via caller | The impersonation level of the effective token. |
object.key.guid | bin.guid | required | The registry key an operation acted on. |
operation.fd | uint | required | The descriptor the backup stream was written to. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
5.3 lcs.audit.key.created
Peios / Using Peios / Events / Registry Events
- Event type:
lcs.audit.key.created - Defined in:
lcs.evman - Tier: essential
- Gating: a matching SACL audit ACE on the parent key, for KEY_CREATE_SUB_KEY
- Cardinality: once per key created
The record that a registry key was created. Creating a key checks only
KEY_CREATE_SUB_KEY on its parent, so the parent's SACL decides, by the
rule lcs.audit.key.opened applies to an open: a success audit ACE on the
parent that matches the caller. No handle exists yet to carry an alarm
mask.
Only a key that was made is recorded. A create that found the key already
there and opened it instead writes nothing here; it is an open, and
lcs.audit.key.opened records it if the key's own SACL asks. The record
is written once the key exists, so a create whose handle then fails to
reach the caller is still recorded, because the key was still made. A
transacted create is recorded when staged.
5.3.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via caller | The user SID of the effective token the operation ran under. |
subject.token.integrity | uint.integrity | via caller | The integrity RID of the effective token. |
subject.token.id | uint.luid | via caller | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via caller | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via caller | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via caller | The impersonation level of the effective token. |
object.kind | str.enum | required | Always key. |
object.key.guid | bin.guid | required | The new key. |
object.key.path | str | required | The new key's full path. |
object.key.layer.name | str | optional | The layer the key was created in. |
object.key.created | bool | required | Always true. |
object.key.volatile | bool | required | Whether a registry key is volatile, meaning it lives only in memory and does not survive a reboot. |
object.key.volatile-requested | bool | required | Equal to object.key.volatile, since only a key that was made is recorded. |
object.key.symlink | bool | required | Whether a registry key is a symbolic link to another key. |
object.sd.length | uint.bytes | required | The new key's descriptor, as built from the parent's inheritable ACEs and the caller's token. |
object.sd.owner | bin.sid | optional | The owner SID on the object's security descriptor. |
access.requested | uint.mask | required | Always KEY_CREATE_SUB_KEY, the right checked on the parent. |
access.granted | uint.mask | required | What the parent check granted. |
transaction.id | uint | optional | The transaction the operation belonged to, registry or package. |
outcome.success | bool | required | Always true. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
5.4 lcs.audit.key.deleted
Peios / Using Peios / Events / Registry Events
- Event type:
lcs.audit.key.deleted - Defined in:
lcs.evman - Tier: essential
- Gating: DELETE overlaps the alarm mask cached on the key handle
- Cardinality: once per delete through a handle whose mask covers it
The record that a registry key was deleted from a layer through its own handle, or that an attempt failed. The key named is the one deleted, the handle's key, by GUID and full path.
Deleting a layer's metadata key, Machine\System\Registry\Layers\<Name>,
deletes the layer itself; the record then also names the layer.
5.4.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via caller | The user SID of the effective token the operation ran under. |
subject.token.integrity | uint.integrity | via caller | The integrity RID of the effective token. |
subject.token.id | uint.luid | via caller | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via caller | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via caller | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via caller | The impersonation level of the effective token. |
object.kind | str.enum | required | Always key. |
object.key.guid | bin.guid | required | The registry key an operation acted on. |
object.key.path | str | required | The resolved absolute path of the registry key, in canonical form, such as Machine\System\KMES. |
object.key.layer.name | str | optional | The layer whose entry was deleted. |
object.layer.name | str | optional | Present when the delete removed a layer, naming it. |
access.requested | uint.mask | required | Always DELETE. |
access.granted | uint.mask | required | The access mask in force. |
access.matched | uint.mask | required | The subset of the requested access that overlapped an audit mask and therefore caused the record to exist. |
access.audit-mask | uint.mask | required | The full continuous-audit mask cached on a handle, of which access.matched is the part this operation touched. |
transaction.id | uint | optional | The transaction the operation belonged to, registry or package. |
request.timed-out | bool | when outcome.success == false | Whether a request's wait for the source's reply ran out before the reply arrived. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.errno | int.errno | when outcome.success == false | The error the operation failed with, as a negative errno. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
5.5 lcs.audit.key.descriptor.changed
Peios / Using Peios / Events / Registry Events
- Event type:
lcs.audit.key.descriptor.changed - Defined in:
lcs.evman - Tier: essential
- Gating: always when the change includes the SACL; otherwise WRITE_DAC or WRITE_OWNER overlaps the alarm mask cached on the key handle
- Cardinality: once per descriptor change it covers
The record that a registry key's security descriptor was changed through a key handle, or that an attempt failed.
A change to the SACL is always recorded, whatever the handle's mask.
It takes ACCESS_SYSTEM_SECURITY, which only SeSecurityPrivilege grants,
such changes are rare, and removing a SACL is exactly how someone hides
what follows. It is recorded once the handle's own access check admits it:
a caller whose handle lacks ACCESS_SYSTEM_SECURITY is refused before any
record, so asking for a SACL change cannot be used to write records.
Owner, group and DACL changes follow the handle's mask, as writes do.
Both descriptors are recorded by length, SHA-256 digest and owner, never
in full. Whether the SACL was part of the change is
object.sd.components.
5.5.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via caller | The user SID of the effective token the operation ran under. |
subject.token.integrity | uint.integrity | via caller | The integrity RID of the effective token. |
subject.token.id | uint.luid | via caller | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via caller | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via caller | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via caller | The impersonation level of the effective token. |
object.kind | str.enum | required | Always key. |
object.key.guid | bin.guid | required | The registry key an operation acted on. |
object.key.path | str | required | The resolved absolute path of the registry key, in canonical form, such as Machine\System\KMES. |
object.sd.components | uint.flags | required | The parts the caller asked to change, its security_info. |
object.sd.length | uint.bytes | optional | The descriptor as written: the merge of the caller's parts into the old one. Absent when the request failed before the merge. |
object.sd.length-previous | uint.bytes | optional | Absent when the request failed before the old descriptor was read. |
object.sd.digest | bin | optional | Present with object.sd.length. |
object.sd.digest-previous | bin | optional | Present with object.sd.length-previous. |
object.sd.owner | bin.sid | optional | Present with object.sd.length. |
object.sd.owner-previous | bin.sid | optional | Present with object.sd.length-previous. |
access.requested | uint.mask | required | The rights the change needs: WRITE_OWNER for the owner or group, WRITE_DAC for the DACL, ACCESS_SYSTEM_SECURITY for the SACL. |
access.granted | uint.mask | required | The access mask in force. |
access.matched | uint.mask | required | Zero when the record exists only because the change included the SACL. |
access.audit-mask | uint.mask | required | The full continuous-audit mask cached on a handle, of which access.matched is the part this operation touched. |
transaction.id | uint | optional | The transaction the operation belonged to, registry or package. |
request.timed-out | bool | when outcome.success == false | True when the source did not answer in time, so the change may have been applied after all. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.errno | int.errno | when outcome.success == false | The error the operation failed with, as a negative errno. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
5.6 lcs.audit.key.hidden
Peios / Using Peios / Events / Registry Events
- Event type:
lcs.audit.key.hidden - Defined in:
lcs.evman - Tier: essential
- Gating: DELETE overlaps the alarm mask cached on the key handle
- Cardinality: once per hide through a handle whose mask covers it
The record that a registry key was hidden in a layer through its own
handle, or that an attempt failed. Hiding writes a marker that masks the
key in every lower layer without deleting it there, so to every reader the
key is gone; it is gated on DELETE, as deleting is.
5.6.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via caller | The user SID of the effective token the operation ran under. |
subject.token.integrity | uint.integrity | via caller | The integrity RID of the effective token. |
subject.token.id | uint.luid | via caller | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via caller | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via caller | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via caller | The impersonation level of the effective token. |
object.kind | str.enum | required | Always key. |
object.key.guid | bin.guid | required | The registry key an operation acted on. |
object.key.path | str | required | The resolved absolute path of the registry key, in canonical form, such as Machine\System\KMES. |
object.key.layer.name | str | optional | The layer the marker was written in. |
access.requested | uint.mask | required | Always DELETE. |
access.granted | uint.mask | required | The access mask in force. |
access.matched | uint.mask | required | The subset of the requested access that overlapped an audit mask and therefore caused the record to exist. |
access.audit-mask | uint.mask | required | The full continuous-audit mask cached on a handle, of which access.matched is the part this operation touched. |
mutation.sequence | uint | optional | The sequence number LCS stamped on a registry mutation. |
transaction.id | uint | optional | The transaction the operation belonged to, registry or package. |
request.timed-out | bool | when outcome.success == false | Whether a request's wait for the source's reply ran out before the reply arrived. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.errno | int.errno | when outcome.success == false | The error the operation failed with, as a negative errno. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
5.7 lcs.audit.key.opened
Peios / Using Peios / Events / Registry Events
- Event type:
lcs.audit.key.opened - Defined in:
lcs.evman - Tier: essential
- Gating: a matching SACL audit ACE on the key
- Cardinality: once per matching open
The record that a registry key was opened and what access the open received. SACL evaluation follows the KACS AccessCheck algorithm, with the SACL evaluated alongside the DACL rather than separately.
A request of MAXIMUM_ALLOWED alone records nothing. It maps to a
desired mask of zero, and the SACL walk tests each audit ACE's mask against
that mask, so no ACE matches. The one request shape that asks for
everything is the one shape that produces no record — a real hole for
anyone auditing key opens, and the same defect KACS's own SACL walk was
changed to close.
Emission failure is handled in two halves. If a valid payload cannot be
constructed, the open fails with EIO and no key descriptor is
published — the audit is a precondition of the access. If a valid payload
cannot be retained by KMES, the decision and the descriptor stand, and
the loss is accounted as a gap in the stream rather than as a failed open.
5.7.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via caller | The user SID of the effective token the operation ran under. |
subject.token.integrity | uint.integrity | via caller | The integrity RID of the effective token. |
subject.token.id | uint.luid | via caller | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via caller | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via caller | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via caller | The impersonation level of the effective token. |
object.kind | str.enum | required | Always key. Names the table the access masks decode against. |
object.key.guid | bin.guid | required | The registry key an operation acted on. |
access.requested | uint.mask | required | After registry generic mapping, with MAXIMUM_ALLOWED re-added when the caller asked for it. |
access.granted | uint.mask | required | Forced to zero on a denial, and enforced rather than merely intended — a denied record carrying a non-zero granted mask is rejected as malformed. |
outcome.success | bool | required | Whether the open was allowed. Replaces the former decision string, which carried the same two states as words. |
trigger.sacl-match | uint.flags | required | Which flavour of audit ACE matched. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
5.8 lcs.audit.key.tombstoned
Peios / Using Peios / Events / Registry Events
- Event type:
lcs.audit.key.tombstoned - Defined in:
lcs.evman - Tier: essential
- Gating: KEY_SET_VALUE overlaps the alarm mask cached on the key handle
- Cardinality: once per blanket tombstone change through a handle whose mask covers it
The record that a key's blanket tombstone was set or cleared in a layer, or
that an attempt failed. A blanket tombstone hides every value lower layers
hold for the key at once, so setting one is a bulk value change and is
gated on KEY_SET_VALUE, as value writes are.
5.8.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via caller | The user SID of the effective token the operation ran under. |
subject.token.integrity | uint.integrity | via caller | The integrity RID of the effective token. |
subject.token.id | uint.luid | via caller | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via caller | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via caller | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via caller | The impersonation level of the effective token. |
object.kind | str.enum | required | Always key. |
object.key.guid | bin.guid | required | The registry key an operation acted on. |
object.key.path | str | required | The resolved absolute path of the registry key, in canonical form, such as Machine\System\KMES. |
object.key.layer.name | str | optional | The layer the blanket belongs to. |
operation.name | str.enum | optional | Whether the blanket was set or cleared. Absent only when the request failed before it was read. Values here (closed set): set · clear. |
access.requested | uint.mask | required | Always KEY_SET_VALUE. |
access.granted | uint.mask | required | The access mask in force. |
access.matched | uint.mask | required | The subset of the requested access that overlapped an audit mask and therefore caused the record to exist. |
access.audit-mask | uint.mask | required | The full continuous-audit mask cached on a handle, of which access.matched is the part this operation touched. |
mutation.sequence | uint | optional | Present when a blanket was set: clearing one stamps no sequence. |
transaction.id | uint | optional | The transaction the operation belonged to, registry or package. |
request.timed-out | bool | when outcome.success == false | Whether a request's wait for the source's reply ran out before the reply arrived. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.errno | int.errno | when outcome.success == false | The error the operation failed with, as a negative errno. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
5.9 lcs.audit.restore.ended
Peios / Using Peios / Events / Registry Events
- Event type:
lcs.audit.restore.ended - Defined in:
lcs.evman - Tier: essential
- Gating: none — audited unconditionally
- Cardinality: one record per occurrence
The record that a registry subtree restore finished, successfully or
otherwise. Pairs with lcs.audit.restore.started on the same key.
5.9.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via caller | The user SID of the effective token the operation ran under. |
subject.token.integrity | uint.integrity | via caller | The integrity RID of the effective token. |
subject.token.id | uint.luid | via caller | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via caller | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via caller | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via caller | The impersonation level of the effective token. |
object.key.guid | bin.guid | required | The registry key an operation acted on. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.errno | int.errno | when outcome.success == false | The error the operation failed with, as a negative errno. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
5.10 lcs.audit.restore.started
Peios / Using Peios / Events / Registry Events
- Event type:
lcs.audit.restore.started - Defined in:
lcs.evman - Tier: essential
- Gating: none — audited unconditionally
- Cardinality: one record per occurrence
The record that a registry subtree restore began, emitted before any state is modified. As with backup, this is a privilege-gated bulk operation that bypasses per-key access checks, and is audited regardless of the target's SACL.
A restore rewrites configuration wholesale, so a start with no matching end means the store may be in a partially rewritten state.
Emission failure fails the operation with EIO.
5.10.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via caller | The user SID of the effective token the operation ran under. |
subject.token.integrity | uint.integrity | via caller | The integrity RID of the effective token. |
subject.token.id | uint.luid | via caller | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via caller | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via caller | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via caller | The impersonation level of the effective token. |
object.key.guid | bin.guid | required | The registry key an operation acted on. |
operation.fd | uint | required | The descriptor the restore stream was read from. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
5.11 lcs.audit.transaction.committed
Peios / Using Peios / Events / Registry Events
- Event type:
lcs.audit.transaction.committed - Defined in:
lcs.evman - Tier: essential
- Gating: the transaction staged at least one recorded write
- Cardinality: once per transaction that staged a recorded write
The record of how a registry transaction ended, written for a transaction
that staged at least one write some other record covers. Transacted
writes are recorded when they are staged; this says whether they took
effect, joined to them on transaction.id.
It is written once, at the end: when REG_IOC_COMMIT leaves the
transaction committed or otherwise finished, when it times out, or when
its fd is closed without a commit. A commit that fails and leaves the
transaction open writes nothing yet.
A commit that timed out may have taken effect. The commit was sent
and not answered; transaction.commit-outstanding true says so, and the
source may have applied it before the reply was lost.
The subject depends on how it ended. On REG_IOC_COMMIT it is the
subject who committed, not who staged the writes. A transaction that ended
by timeout or by its fd closing has nobody acting, and names the subject
that staged its first recorded write.
5.11.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via caller | The user SID of the effective token the operation ran under. |
subject.token.integrity | uint.integrity | via caller | The integrity RID of the effective token. |
subject.token.id | uint.luid | via caller | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via caller | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via caller | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via caller | The impersonation level of the effective token. |
transaction.id | uint | required | The transaction the operation belonged to, registry or package. |
transaction.state | str.enum | required | The state it ended in: committed on success. |
transaction.commit-outstanding | bool | when outcome.success == false | Whether a transaction's commit had been sent to its source and not yet answered. |
outcome.success | bool | required | True only when the transaction committed. |
outcome.errno | int.errno | optional | Present when a REG_IOC_COMMIT call failed; absent for a transaction that ended without one. |
outcome.reason | str.enum | when outcome.success == false | aborted when its fd was closed without a commit or a layer it wrote was deleted, timed-out when it passed its deadline, source-error when its source failed or went away.Values here (open set): aborted · timed-out · source-error. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
5.12 lcs.audit.value.deleted
Peios / Using Peios / Events / Registry Events
- Event type:
lcs.audit.value.deleted - Defined in:
lcs.evman - Tier: essential
- Gating: KEY_SET_VALUE overlaps the alarm mask cached on the key handle
- Cardinality: once per delete through a handle whose mask covers it
The record that a registry value was deleted through a key handle, or that
an attempt to delete one failed. Gated, staged and fixed at open exactly as
lcs.audit.value.set is.
What was removed is recorded by type, length and SHA-256 digest: the effective value before the delete, read in the transaction's own view when the delete is transacted.
5.12.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via caller | The user SID of the effective token the operation ran under. |
subject.token.integrity | uint.integrity | via caller | The integrity RID of the effective token. |
subject.token.id | uint.luid | via caller | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via caller | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via caller | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via caller | The impersonation level of the effective token. |
object.kind | str.enum | required | Always key. |
object.key.guid | bin.guid | required | The registry key an operation acted on. |
object.key.path | str | required | The resolved absolute path of the registry key, in canonical form, such as Machine\System\KMES. |
object.key.layer.name | str | optional | The layer whose entry was deleted. Absent only when the request failed before its layer was read. |
object.key.value.name | str | optional | Absent only when the request failed before its name was read. |
object.key.value.type-previous | uint.enum | optional | Absent when no value was found to delete. |
object.key.value.length-previous | uint.bytes | optional | Present with object.key.value.type-previous. |
object.key.value.digest-previous | bin | optional | Present with object.key.value.type-previous. |
access.requested | uint.mask | required | Always KEY_SET_VALUE. |
access.granted | uint.mask | required | The access mask in force. |
access.matched | uint.mask | required | The subset of the requested access that overlapped an audit mask and therefore caused the record to exist. |
access.audit-mask | uint.mask | required | The full continuous-audit mask cached on a handle, of which access.matched is the part this operation touched. |
transaction.id | uint | optional | The transaction the operation belonged to, registry or package. |
request.timed-out | bool | when outcome.success == false | Whether a request's wait for the source's reply ran out before the reply arrived. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.errno | int.errno | when outcome.success == false | The error the operation failed with, as a negative errno. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
5.13 lcs.audit.value.set
Peios / Using Peios / Events / Registry Events
- Event type:
lcs.audit.value.set - Defined in:
lcs.evman - Tier: essential
- Gating: KEY_SET_VALUE overlaps the alarm mask cached on the key handle
- Cardinality: once per write through a handle whose mask covers it
The record that a registry value was written through a key handle, or that
an attempt to write one failed. The key's SYSTEM_ALARM ACEs configure it:
the ACEs that match the caller when the key is opened give the handle a
continuous-audit mask, and every later write whose right overlaps the mask
is recorded, as kacs.audit.handle.used does for files.
The mask is fixed when the key is opened. Removing the alarm ACE does not silence a handle already open, so the write that removes it is still recorded; equally, adding one does not reach handles opened before.
A transacted write is recorded when it is staged, not when it lands.
outcome.success then means the source accepted it into the transaction;
whether the transaction took effect is the matching
lcs.audit.transaction.committed, joined on transaction.id.
A timed-out write may have been applied later. The source is not told
to cancel, and when its reply arrives the kernel still applies the write's
effects, with nothing recording who made it. Such a record says
outcome.success false with request.timed-out true: read it as "may
have been applied".
The data itself is never recorded, only its type, length and SHA-256 digest, with the same three for the value it replaced.
5.13.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | via caller | The user SID of the effective token the operation ran under. |
subject.token.integrity | uint.integrity | via caller | The integrity RID of the effective token. |
subject.token.id | uint.luid | via caller | The token's own LUID, identifying this specific token rather than the logon session it belongs to. |
subject.token.auth-id | uint.luid | via caller | The LUID of the logon session the effective token belongs to. |
subject.token.type | str.enum | via caller | Whether the effective token is a primary token or an impersonation token. |
subject.token.impersonation | uint.enum | via caller | The impersonation level of the effective token. |
object.kind | str.enum | required | Always key. |
object.key.guid | bin.guid | required | The registry key an operation acted on. |
object.key.path | str | required | The resolved absolute path of the registry key, in canonical form, such as Machine\System\KMES. |
object.key.layer.name | str | optional | The layer written, base when the caller named none. Absent only when the request failed before its layer was read. |
object.key.value.name | str | optional | Absent only when the request failed before its name was read. |
object.key.value.type | uint.enum | optional | The type written. Present once the data was read from the caller. |
object.key.value.length | uint.bytes | optional | Present with object.key.value.type. |
object.key.value.digest | bin | optional | Present with object.key.value.type. |
object.key.value.type-previous | uint.enum | optional | The effective value before the write. Only on a write outside a transaction that found one: a transacted write never reads it. |
object.key.value.length-previous | uint.bytes | optional | Present with object.key.value.type-previous. |
object.key.value.digest-previous | bin | optional | Present with object.key.value.type-previous. |
access.requested | uint.mask | required | Always KEY_SET_VALUE. |
access.granted | uint.mask | required | What the handle was opened with. |
access.matched | uint.mask | required | The subset of the requested access that overlapped an audit mask and therefore caused the record to exist. |
access.audit-mask | uint.mask | required | The full continuous-audit mask cached on a handle, of which access.matched is the part this operation touched. |
mutation.sequence | uint | optional | The sequence stamped on the write. Absent only when the request failed before one was allocated. |
mutation.sequence-expected | uint | optional | Present on a compare-and-swap write. |
transaction.id | uint | optional | Present when the write was staged in a transaction. |
request.timed-out | bool | when outcome.success == false | True when the source did not answer in time, so the write may have been applied after all. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.errno | int.errno | when outcome.success == false | The error the operation failed with, as a negative errno. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
5.14 lcs.config.value.rejected
Peios / Using Peios / Events / Registry Events
- Event type:
lcs.config.value.rejected - Defined in:
lcs.evman - Tier: essential
- Gating: none — every rejected configuration value is recorded
- Cardinality: once per rejected value
The record that LCS read one of its own configuration values, rejected it,
and carried on with what it had. Identical in shape and meaning to
kmes.config.value.rejected, differing only in which key the values live
under.
Validation is reject-or-keep and never clamps, so this event is the only place where what an administrator wrote and what LCS is running on can be seen to differ.
Emission failure preserves the retained configuration — a failure to record the rejection must not change which value is in force.
5.14.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
config.key.path | str | required | The registry key the reported value lives under, in canonical form. |
config.name | str | required | The name of the configuration value being reported on, within config.key.path. |
config.expected.type | uint.enum | required | The registry type the value is required to have. |
config.expected.min | uint | required | The smallest value accepted for this setting. |
config.expected.max | uint | required | The largest value accepted for this setting. |
config.received.kind | str.enum | required | How the stored value failed validation. |
config.received.type | uint.enum | when config.received.kind == wrong-type | The registry type the value actually had. |
config.received.value | uint | when config.received.kind == out-of-range | The value that was stored and rejected. |
config.value | uint | required | The value retained. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
5.15 lcs.source.response.rejected
Peios / Using Peios / Events / Registry Events
- Event type:
lcs.source.response.rejected - Defined in:
lcs.evman - Tier: essential
- Gating: none — every rejection is recorded
- Cardinality: one record per occurrence
The record that LCS rejected data a registry source sent it. Sources are userspace backends serving configuration into the kernel, so this is the boundary where the kernel refuses to believe what userspace told it.
This is the one LCS event with no caller. It describes a source misbehaving rather than a principal acting, and there is frequently no principal involved — the data arrived on a source channel, not through a syscall.
The failure classes distinguish malformed structure from semantic impossibility: a security descriptor that will not parse is a different problem from a sequence number from the future or two layers claiming the same winning sequence.
5.15.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
source.rsi.slot | uint | required | Which registry source slot supplied the data, by index. |
source.rsi.hive | str | optional | The hive the offending source data belonged to, where the failure could be attributed to one. |
request.id | uint | optional | The identifier of the source request this record concerns, for joining a record to the exchange that produced it. |
request.op-code | uint | optional | The source-protocol operation code of the request this record concerns. |
object.key.guid | bin.guid | optional | Present when the rejected data could be attributed to a specific key. |
outcome.reason | str.enum | required | The source-validation failure class. Values here (open set): malformed-security-descriptor · malformed-layer-name · unknown-rsi-status-code · future-sequence-number · duplicate-winning-sequence-tie · malformed-layer-metadata-security-descriptor · malformed-key-name · malformed-value-name · malformed-response-payload · malformed-key-metadata · malformed-value-payload · malformed-delete-layer-orphan-list. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
6.1 kmes.buffer.swap.failed
Peios / Using Peios / Events / Event Stream Events
- Event type:
kmes.buffer.swap.failed - Defined in:
kmes.evman - Tier: standard
- Gating: none — every failed swap is recorded, whatever the error
- Cardinality: once per failed swap
The record that KMES could not resize its per-CPU ring buffers and kept the capacity it had. A capacity change is a swap — new buffers at the new size, surviving events copied across, writers switched over — and this is what happens when that swap cannot be completed.
The failure is usually memory pressure, which makes it self-reinforcing in a way worth recognising: an administrator typically raises capacity because the system is dropping events, and the allocation most likely to fail is the one made while the system is already short of memory.
The other failures are rarer and more serious. EIO means the migration
found a ring whose framing was corrupt and abandoned the swap rather than
copy it; any other error is the kernel refusing to quiesce the CPUs for
the switch-over. In every case the old rings stay live and nothing was
lost by the attempt.
The swap is system-wide rather than per-ring, so no CPU or ring is named.
6.1.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
buffer.capacity-requested | uint.bytes | required | The per-CPU ring buffer capacity that was asked for. |
buffer.capacity | uint.bytes | required | The capacity kept. |
outcome.errno | int.errno | required | -ENOMEM when the new rings could not be allocated, -EIO when the migration met a corrupt ring. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kmes.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
6.2 kmes.config.applied
Peios / Using Peios / Events / Event Stream Events
- Event type:
kmes.config.applied - Defined in:
kmes.evman - Tier: standard
- Gating: none — every configuration read that reaches the commit is recorded
- Cardinality: once per read of
Machine\System\KMES
The record that KMES read its configuration key and committed what it found. Emitted after every read that gets as far as the commit: at boot once the registry is available, and again each time a value under the key changes.
The four counts account for every setting the read saw, so a record whose
config.counts.applied is zero is a read that changed nothing, and one
with config.counts.retained-invalid above zero comes with a
kmes.config.value.rejected for each value kept back. A read whose
BufferCapacity swap failed is still recorded here, because the other
three settings commit without it; buffer.capacity is then the capacity
kept, beside the kmes.buffer.swap.failed that says why.
6.2.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
config.key.path | str | required | Always Machine\System\KMES. |
config.counts.applied | uint | required | How many settings in a configuration refresh were applied. |
config.counts.retained-missing | uint | required | How many settings kept their previous value because they were absent from the registry. |
config.counts.retained-invalid | uint | required | How many settings kept their previous value because the stored value was rejected, of the wrong type or out of range. |
config.counts.ignored-unknown | uint | required | How many registry values were ignored because their names match no setting. |
buffer.capacity | uint.bytes | required | The per-CPU ring capacity in force after the commit. |
emission.rate-limit | uint | required | The per-process emission ceiling in force after the commit. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kmes.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
6.3 kmes.config.refresh.failed
Peios / Using Peios / Events / Event Stream Events
- Event type:
kmes.config.refresh.failed - Defined in:
kmes.evman - Tier: essential
- Gating: none — every failed re-read is recorded
- Cardinality: once per failed read
The record that KMES could not read one of its configuration keys and kept
running on what it had. Two keys are read this way: Machine\System\KMES,
KMES's own settings, and Machine\Generic\Events, the emission policy
that decides which kernel event types are written.
A failed read changes nothing. KMES keeps the configuration it last committed, or the compiled-in defaults if it never committed one, and the emission policy keeps the switches it last computed, or the tier defaults. Until the next change under the key, the registry and the running kernel disagree, and this record is the only sign of it: an administrator who switched an event type off and finds it still written is looking for one of these.
The usual cause is the registry source failing to answer in time. A
configuration rejected value by value is not a failed read; that is
kmes.config.value.rejected.
6.3.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
config.key.path | str | required | Machine\System\KMES or Machine\Generic\Events. |
outcome.errno | int.errno | required | The error the operation failed with, as a negative errno. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kmes.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
6.4 kmes.config.value.rejected
Peios / Using Peios / Events / Event Stream Events
- Event type:
kmes.config.value.rejected - Defined in:
kmes.evman - Tier: essential
- Gating: none — every rejected configuration value is recorded
- Cardinality: once per rejected value
The record that KMES read a configuration value, rejected it, and carried on with what it had. Emitted when the stored value is missing, of the wrong type, or out of range.
Validation is reject-or-keep, never clamp: a valid value is applied and an invalid one is ignored, with the last known-good value retained. That makes this event the only place the divergence is visible — the registry records what an administrator wrote, and this records what KMES is running on. An operator reading only the registry will believe a change took effect that did not.
These records are emitted before the configuration commit and are deliberately not part of the commit decision: a failure to construct one must never roll back a hot-swap that otherwise succeeded.
6.4.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
config.key.path | str | required | The registry key the reported value lives under, in canonical form. |
config.name | str | required | The name of the configuration value being reported on, within config.key.path. |
config.expected.type | uint.enum | required | The registry type the value is required to have. |
config.expected.min | uint | required | The smallest value accepted for this setting. |
config.expected.max | uint | required | The largest value accepted for this setting. |
config.received.kind | str.enum | required | How the stored value failed validation. |
config.received.type | uint.enum | when config.received.kind == wrong-type | The registry type the value actually had. |
config.received.value | uint | when config.received.kind == out-of-range | The value that was stored and rejected. |
config.value | uint | required | The value retained. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from kmes.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
7.1 ntfe.policy.published
Peios / Using Peios / Events / Network Policy Events
- Event type:
ntfe.policy.published - Defined in:
ntfe.evman - Tier: standard
- Gating: none — every generation of rules that goes into force is recorded
- Cardinality: once per published generation of rules
The record that a new generation of traffic rules went into force. The
kernel reads its own policy from Machine\System\Network\Rules, a short
while after the registry changes, and publishes it whole: from this point
every traversal is judged by these rules, and every flow judged under the
old ones is judged again on its next packet.
A re-read that finds the policy exactly as it was published publishes
nothing and writes no record, so writing a rule back unchanged, or netd
writing its inventory, is silent. A refused policy writes
ntfe.policy.rejected instead, and the generation before it stays.
Not every generation has one of these. A change to the network context
(which network each interface is on, and its trust) advances the same
generation counter without a new rule set, and writes no record. A verdict
whose policy.generation falls between two of these records was judged by
the rules of the earlier one.
7.1.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
policy.generation | uint | required | The generation this policy now runs as. Verdict records carry the same number. |
policy.generation-previous | uint | required | The generation of the rule set this one replaced, or 0 when this is the first since boot. Context changes advance the counter too, so it can be lower than one less than policy.generation. |
policy.layers | str.enum[] | required | The rules layers the active generation loaded a forest for. |
policy.report-threshold | uint | required | The machine's reporting threshold, CurrentReportingLevel: a rule's report fires only when its level is at least this. |
policy.report-threshold-previous | uint | required | 1, the default, when this is the first generation since boot. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from ntfe.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
7.2 ntfe.policy.rejected
Peios / Using Peios / Events / Network Policy Events
- Event type:
ntfe.policy.rejected - Defined in:
ntfe.evman - Tier: essential
- Gating: a refusal unlike the last one recorded
- Cardinality: once per distinct refusal
The record that the kernel read a traffic policy and refused it, so the policy an administrator wrote is not the policy in force. NTFE accepts or refuses a policy whole: one bad rule anywhere refuses every layer, and the previous generation goes on judging traffic exactly as before. Nothing is half-applied. The registry shows what was written; this shows that it did not take.
The kernel reads the policy again whenever anything under
Machine\System\Network changes, including netd's inventory, and a broken
policy is refused by every read until it is fixed. The same refusal of the
same policy is recorded once: a new record means the policy or the reason
changed. A policy that is accepted, or found unchanged, starts the count
again.
The rule, where there is one, is the place to look: outcome.reason says
what is wrong with it.
7.2.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
policy.generation | uint | required | The generation still in force, which the refused policy did not replace. |
policy.previous-retained | bool | required | Always true: NTFE never leaves part of a refused policy in force. |
outcome.errno | int.errno | when outcome.reason != no-rules-key | The error the operation failed with, as a negative errno. |
outcome.reason | str.enum | required | Why the policy was refused. A rule's own content: unknown-fact (a condition names a fact the language does not have), bad-operator (an operator the fact does not support), bad-pattern (a match value that does not parse for its fact), bad-counter-view (a Counter.<n> key that does not parse), bad-actions-value (Actions missing or not a list of strings), bad-action (an action that does not parse; rule.action-error says how), prompt-chain-too-deep, bad-priority (not an integer), bad-enabled (not 0 or 1), bad-rule-name, present-never-at-layer (a Present condition on a fact the layer never has), key-not-at-layer and action-not-at-layer (a condition or action the rule's layer does not speak). How it was stored: bad-value-type (a registry type rules do not use), bad-value-length (an integer of the wrong size), not-utf8. The policy as a whole: rule-too-deep (more than 13 levels), too-many-rules (more than 4096 in a layer), bad-reporting-level (CurrentReportingLevel not an integer from 1 to 6), tag-hash-collision and stream-hash-collision (two names that hash alike), counter-never-written (a counter no rule writes), tag-downward-read (a rule reading a tag a higher layer writes), counter-store-refused (the counter tables could not be built). And the read itself: registry-read-failed, out-of-memory, and no-rules-key, which is no policy at all — recorded once each time the key goes missing.Values here (open set): out-of-memory · unknown-fact · bad-operator · bad-pattern · bad-counter-view · bad-actions-value · bad-action · prompt-chain-too-deep · bad-priority · bad-enabled · bad-rule-name · tag-hash-collision · stream-hash-collision · counter-never-written · tag-downward-read · present-never-at-layer · key-not-at-layer · action-not-at-layer · rule-too-deep · too-many-rules · bad-value-type · bad-value-length · not-utf8 · bad-reporting-level · counter-store-refused · registry-read-failed · no-rules-key. |
rule.name | str | optional | The rule the refusal is in, or names, as its path relative to its layer key. Absent when the refusal is not one rule's: two names hashing alike, the reporting level, a registry read that failed outside any rule, or no Rules key. Held to 255 bytes. |
rule.name-truncated | bool | optional | Whether rule.name was cut short because the rule's path did not fit in the record. |
rule.layer | str.enum | optional | The layer the named rule is in. Absent with rule.name, and also for the checks that span layers (counter-never-written, tag-downward-read), which name a rule but not its layer. |
rule.action-error | str.enum | when outcome.reason == bad-action | Why one action expression in a rule's Actions value failed to parse. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from ntfe.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
7.3 ntfe.verdict.reported
Peios / Using Peios / Events / Network Policy Events
- Event type:
ntfe.verdict.reported - Defined in:
ntfe.evman - Tier: essential
- Gating: an explicit report directive in the matching rule
- Cardinality: once per matching evaluation
The record of a traffic policy decision that a rule explicitly asked to be reported. Not a record of every decision — the engine evaluates constantly and reports only where a rule says to.
A stock machine emits none of these. The shipped baseline rule set contains no report directives, so this event exists and never fires until an administrator adds one. Absence of these records says nothing about whether traffic was evaluated.
The engine's own record of every verdict goes to a private ring read by the userspace policy daemon, not to the event stream, and is overwritten when no reader is attached. So the complete decision history is neither in the event stream nor durable.
Address, port and protocol fields are conditional on how far up the stack evaluation reached: traffic matched at the raw-packet layer has no addresses to record.
7.3.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
rule.name | str | required | The policy rule that produced this record, by name. |
rule.hash | uint | required | The FNV-1a-64 hash of the rule's whole registry path. |
rule.name-truncated | bool | optional | Whether rule.name was cut short because the rule's path did not fit in the record. |
rule.report-level | uint | required | The reporting level the matching rule asked for. |
rule.layer | str.enum | required | Which evaluation layer the rule sat in. |
rule.seat | str.enum | required | Where in the path evaluation happened. |
outcome.verdict | str.enum | required | The decision a policy engine reached, where the decision has more than two states and outcome.success cannot express it. |
outcome.reason | str.enum | when outcome.verdict == reject | Which flavour of explicit rejection the rule asked for: the story the REJECT tells the sender. refused says nothing is listening and reveals no policy; prohibited says policy refused the traffic. When two REJECTs tie on priority, refused wins.Values here (closed set): prohibited · refused. |
network.direction | str.enum | required | Which way the traffic was moving relative to this machine. |
network.interface.name | str | optional | Absent when the traffic was evaluated with no device attached, in which case network.interface.index is 0. |
network.interface.index | uint | required | The kernel's index for that interface. |
network.ether-type | uint | required | The link-layer protocol number of the frame. |
network.family | uint.enum | required | The address family of the addresses in this record. |
network.protocol | uint.enum | when network.family != 0 | The transport protocol number, from the IANA protocol numbers registry. |
source.address | str.ip | when network.family != 0 | The address the traffic came from, rendered in the textual form for its family. |
destination.address | str.ip | when network.family != 0 | The address the traffic was going to, rendered in the textual form for its family. |
source.port | uint | optional | Present when the protocol has ports. |
destination.port | uint | optional | Present when the protocol has ports. |
flow.state | str.enum | optional | Present when flow tracking applied. |
network.length | uint.bytes | required | The length of the traffic unit evaluated. |
policy.generation | uint | required | The generation counter of the rule set in force when the decision was made. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from ntfe.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.1 peinit.boot.downgraded
Peios / Using Peios / Events / Service Events
- Event type:
peinit.boot.downgraded - Defined in:
peinit.evman - Tier: essential
- Gating: none
- Cardinality: one per finding that forced the downgrade, at most once a boot in all
A full boot was downgraded to safe mode: the machine is running a reduced service set. The services named are not marked failed, because safe mode never meant to start them, so this record is the only one that names them as the cause. Essential because it is rare and changes what the whole machine is running.
Formerly boot.safe_mode_downgrade.
8.1.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
outcome.reason | str.enum | required | The finding that forced the downgrade. Values here (open set): critical-cycle · critical-boot-conflict. |
graph.services | str[] | when outcome.reason == critical-cycle | The services a graph finding involves. |
object.service.name | str | when outcome.reason == critical-boot-conflict | The name of the service the event is about, as its definition names it. |
object.service.conflict.name | str | when outcome.reason == critical-boot-conflict | The name of the service this one conflicts with, on a finding that two services both triggered at boot declare a conflict and so cannot both start. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.2 peinit.cgroup.leaked
Peios / Using Peios / Events / Service Events
- Event type:
peinit.cgroup.leaked - Defined in:
peinit.evman - Tier: standard
- Gating: none
- Cardinality: once per cgroup, the first time it is found still populated after its post-kill deadline
A cgroup could not be reclaimed: its processes do not respond to the kernel, and the cgroup is leaked. It is one of a service's sub-cgroups, or the whole cgroup of a submitted job.
Formerly cgroup.leaked.
8.2.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.service.name | str | optional | The service whose cgroup leaked. Absent for a submitted job's cgroup, which belongs to no service. |
object.job.guid | bin.guid | optional | The submitted job whose cgroup leaked. Exactly one of object.service.name and this field is present. |
object.cgroup.path | str.path | required | The cgroup the event is about, as its full path under /sys/fs/cgroup/peinit. |
object.cgroup.type | str.enum | required | service-tree for a submitted job's cgroup, which is the whole of its containment. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.3 peinit.config.reload.applied
Peios / Using Peios / Events / Service Events
- Event type:
peinit.config.reload.applied - Defined in:
peinit.evman - Tier: standard
- Gating: none
- Cardinality: once per explicit
reload-config, and once a boot at most when the boot window closes with reloads deferred
A configuration reload ran: from now on the registry as it then stood is
the running configuration, or, if it failed, nothing changed. Written for
an explicit reload-config, and for the one reload that applies what the
boot window deferred. A reload a registry watch starts writes none.
A failed validation also writes one peinit.graph.validation.failed per
finding, under the reload-config phase.
Formerly config.reload_coalesced, which an explicit reload did not write.
8.3.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
outcome.success | bool | required | Whether the operation succeeded. |
outcome.detail | str | optional | Why the reload failed, for a person. Present only when outcome.success is false. |
graph.services | str[] | optional | The services whose definitions the boot window had deferred. Present exactly on the reload that applies them, which is how it is told from an explicit reload-config. |
graph.counts.added | uint | when outcome.success == true | How many service definitions a configuration reload added: names that had no definition before. |
graph.counts.updated | uint | when outcome.success == true | How many existing service definitions a configuration reload changed. |
graph.counts.restored | uint | when outcome.success == true | How many definitions a configuration reload brought back after an earlier reload had removed them while their services were still running. |
graph.counts.marked-removed | uint | when outcome.success == true | How many definitions a configuration reload withdrew from services that were still running. |
graph.counts.discarded | uint | when outcome.success == true | How many definitions a configuration reload withdrew and dropped at once, because their services were not running. |
graph.counts.undecodable | uint | when outcome.success == true | How many definitions a configuration reload found in the registry but could not decode. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.4 peinit.config.reload.deferred
Peios / Using Peios / Events / Service Events
- Event type:
peinit.config.reload.deferred - Defined in:
peinit.evman - Tier: verbose
- Gating: none
- Cardinality: once per reload during the boot window that left definitions pending
A configuration reload during the boot window changed definitions of
services the boot plan had not yet launched. The boot runs against its
snapshot, so those services start from the plan, and the change waits for
peinit.config.reload.applied.
Formerly config.reload_deferred.
8.4.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
graph.services | str[] | required | The services whose definitions were deferred. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.5 peinit.critical-service.failed
Peios / Using Peios / Events / Service Events
- Event type:
peinit.critical-service.failed - Defined in:
peinit.evman - Tier: essential
- Gating: none
- Cardinality: once per Critical failure that drives the machine to reboot
A service marked Critical failed, and peinit took the machine to its reboot final action. Essential because it is rare and reboots the machine.
Formerly critical.failure.
8.5.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.service.name | str | required | The name of the service the event is about, as its definition names it. |
outcome.reason | str.enum | required | How the Critical service failed. Values here (open set): service-main-terminal · health-check-failure · watchdog-timeout · restart-budget-exhausted. |
shutdown.finalization-state | str.enum | required | How far the system shutdown had got when the event was written. |
outcome.detail | str | when shutdown.finalization-state == failed | Why finalisation failed. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.6 peinit.event.dropped
Peios / Using Peios / Events / Service Events
- Event type:
peinit.event.dropped - Defined in:
peinit.evman - Tier: essential
- Gating: none
- Cardinality: once per event the ring refused
The ring refused one of peinit's events, and it is gone. Written so that
the trail records the gap rather than, for example, a job with a
peinit.job.started and no peinit.job.ended. Small by construction, and
if the ring refuses this one too peinit treats it as fatal. Essential
because it is the record of a hole in the audit trail.
There is no count of drops on the record: count the records. The
truncated notice that used to follow a cut job.ended is gone, because
peinit.job.ended says itself when its arguments were cut.
Formerly event.oversized, with action dropped.
8.6.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
emission.type | str | required | The event type an emitter was trying to emit, as it supplied it. |
emission.payload-length | uint.bytes | required | The byte length of an event's MessagePack payload, without the header or the event type. |
object.service.name | str | optional | The service the refused event was about, or whose job sent it. |
object.job.guid | bin.guid | optional | The job the refused event was about, or whose process sent it. |
outcome.errno | int.errno | optional | The error the emit returned, where it gave a number. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.7 peinit.fd-store.rejected
Peios / Using Peios / Events / Service Events
- Event type:
peinit.fd-store.rejected - Defined in:
peinit.evman - Tier: standard
- Gating: none — every refused descriptor is recorded
- Cardinality: one record per occurrence
peinit refused a file descriptor a service asked it to keep.
Formerly fd_store.rejected.
8.7.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.service.name | str | required | The name of the service the event is about, as its definition names it. |
object.fd-store.name | str | required | The name a service gave a file descriptor it asked peinit to keep, with FDNAME=. |
outcome.reason | str.enum | required | disabled when the service's definition does not enable an fd store; full when it is at its limit.Values here (open set): disabled · full. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.8 peinit.graph.operation.ended
Peios / Using Peios / Events / Service Events
- Event type:
peinit.graph.operation.ended - Defined in:
peinit.evman - Tier: verbose
- Gating: none — every member of a graph context is recorded
- Cardinality: one record per occurrence
An operation that is a member of a graph execution context reached its
terminal outcome as the graph sees it: satisfied, so that what depends on
it may proceed, or failed. The operation's own peinit.operation.ended
records the same end from the operation's side.
Formerly graph.operation_terminal.
8.8.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
graph.context | uint | required | The graph execution context an event belongs to: one boot's start of its service set, or one on-demand start of a service and everything it needs. |
object.operation.guid | bin.guid | required | The peinit operation the event is about: a start, stop, restart, reload or reset of one service, from the moment it is requested to the moment it ends. |
object.service.name | str | required | The name of the service the event is about, as its definition names it. |
outcome.success | bool | required | True when the graph counts the member satisfied, false when it failed. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.9 peinit.graph.validation.failed
Peios / Using Peios / Events / Service Events
- Event type:
peinit.graph.validation.failed - Defined in:
peinit.evman - Tier: standard
- Gating: none — every finding is recorded
- Cardinality: one per finding; a validation with several findings writes several
The service graph failed validation for the reason in outcome.reason. At
boot the services named are marked and the boot continues; at
reload-config the whole reload is rejected; at phase2-boot the boot
plan could not be built.
Formerly graph.validation_error.
8.9.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
graph.phase | str.enum | required | When the service graph was validated. |
outcome.reason | str.enum | required | The finding. Values here (open set): invalid-service-name · duplicate-service · missing-service-definition · missing-hard-dependency · hard-dependency-blocked · cycle · conflicting-boot-services · invalid-health-check-restart-window · unschedulable-health-check · invalid-timer-schedule · validation-error. |
object.service.name | str | optional | The service the finding is about. Absent on cycle, which names several. |
graph.services | str[] | when outcome.reason == cycle | The services a graph finding involves. |
object.service.dependency.name | str | optional | On missing-hard-dependency and hard-dependency-blocked. |
object.service.dependency.kind | str.enum | optional | Present exactly when object.service.dependency.name is. |
object.service.conflict.name | str | when outcome.reason == conflicting-boot-services | The name of the service this one conflicts with, on a finding that two services both triggered at boot declare a conflict and so cannot both start. |
object.service.health-check.interval | uint.duration | when outcome.reason == invalid-health-check-restart-window | The configured interval between health checks, in nanoseconds. |
object.service.health-check.retries | uint | when outcome.reason == invalid-health-check-restart-window | The configured number of consecutive health-check failures at which peinit stops reporting the service unhealthy and acts on it. |
object.service.health-check.restart-window | uint.duration | when outcome.reason == invalid-health-check-restart-window | The configured restart window of a health check, in nanoseconds. |
object.service.type | str.enum | when outcome.reason == unschedulable-health-check | How the service's main process is supervised. |
object.service.timer.schedule | str | when outcome.reason == invalid-timer-schedule | The service's timer schedule, exactly as its definition wrote it. |
outcome.detail | str | optional | On invalid-timer-schedule, the parser's message; on validation-error, why the definition was refused. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.10 peinit.graph.validation.warned
Peios / Using Peios / Events / Service Events
- Event type:
peinit.graph.validation.warned - Defined in:
peinit.evman - Tier: standard
- Gating: none — every warning is recorded
- Cardinality: one per warning
The service graph passed validation with a warning: nothing was refused, but something will not behave as its author probably expects.
Formerly graph.validation_warning.
8.10.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
graph.phase | str.enum | required | When the service graph was validated. |
outcome.reason | str.enum | required | The warning. Values here (open set): alive-readiness-with-hard-dependents · unfilled-role. |
object.service.name | str | when outcome.reason == alive-readiness-with-hard-dependents | The name of the service the event is about, as its definition names it. |
object.service.dependents | str[] | when outcome.reason == alive-readiness-with-hard-dependents | The services that hard-depend on this one, by name. |
graph.role | str | when outcome.reason == unfilled-role | The role no service fills. |
graph.services | str[] | when outcome.reason == unfilled-role | The services a graph finding involves. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.11 peinit.internal-error.contained
Peios / Using Peios / Events / Service Events
- Event type:
peinit.internal-error.contained - Defined in:
peinit.evman - Tier: standard
- Gating: none — every contained internal error is recorded
- Cardinality: one record per occurrence
peinit could not carry out a step on a service's behalf, and contained the fault to that service. The fault is peinit's, not the service's; the job, operation and state change it caused have their own events.
The error itself is not carried: every path that contains one has it only as Rust Debug output, which no event field admits. peinit's console line and the failed operation's result keep it.
Formerly service.internal_error.
8.11.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
operation.stage | str.enum | required | The step peinit could not carry out. Values here (open set): job-terminal · process-setup · lifecycle-deadline · notify. |
object.service.name | str | optional | The name of the service the event is about, as its definition names it. |
object.job.guid | bin.guid | optional | The job the event is about. |
object.service.failed | bool | optional | Present exactly when object.service.name is. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.12 peinit.job.created
Peios / Using Peios / Events / Service Events
- Event type:
peinit.job.created - Defined in:
peinit.evman - Tier: verbose
- Gating: none — every job peinit creates is recorded
- Cardinality: one record per occurrence
A job exists: peinit has decided to run a process and recorded it, before
the process exists. The gap to peinit.job.started is time spent queued,
and peinit.job.ended repeats everything here, which is why this one is
verbose.
Formerly job.created.
8.12.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.job.guid | bin.guid | required | The job the event is about. |
object.job.type | str.enum | required | What the job is for. |
object.job.state | str.enum | required | Always created. |
object.service.name | str | when object.job.type != submitted | The name of the service the event is about, as its definition names it. |
object.job.activation-generation | uint | when object.job.type != submitted | The activation generation of the service the job belongs to, at the time the job was created. |
object.operation.guid | bin.guid | optional | The operation the job serves. Absent when none asked for it, as for a submitted job. |
object.job.executable | str.path | required | The path of the executable the job runs. |
object.job.token.sid | bin.sid | optional | A service's token is minted when its process is set up, after this record, so here it is the user the job is to run as when peinit knows that SID in advance — a well-known identity such as SYSTEM — and absent for an identity peinit has only by name. A submitted job's token exists from the start. |
object.job.token.groups | bin.sid[] | optional | Present once peinit holds the job's token: a submitted job's at once, a service job's from peinit.job.started. |
object.job.token.privileges | uint.flags | optional | Present exactly when object.job.token.groups is. |
object.job.token.privileges-enabled | uint.flags | optional | Present exactly when object.job.token.groups is. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.13 peinit.job.ended
Peios / Using Peios / Events / Service Events
- Event type:
peinit.job.ended - Defined in:
peinit.evman - Tier: standard
- Gating: none — every job that ends is recorded
- Cardinality: one record per occurrence
A job is over: its process exited or was killed, or the job ended without its process ever starting. It carries the whole job record, so it is the one job event to read when only one is kept.
Formerly job.ended.
8.13.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.job.guid | bin.guid | required | The job the event is about. |
object.job.type | str.enum | required | What the job is for. |
object.job.state | str.enum | required | The terminal state: completed, failed or abandoned. |
outcome.success | bool | required | True exactly when object.job.state is completed. |
outcome.detail | str | optional | Why the job did not complete, in peinit's words, such as ProcessCrash: signal 9. Present only when outcome.success is false and peinit recorded a cause. Free text until it is given an enumeration. |
object.service.name | str | when object.job.type != submitted | The name of the service the event is about, as its definition names it. |
object.job.activation-generation | uint | when object.job.type != submitted | The activation generation of the service the job belongs to, at the time the job was created. |
object.operation.guid | bin.guid | optional | The peinit operation the event is about: a start, stop, restart, reload or reset of one service, from the moment it is requested to the moment it ends. |
object.process.pid | uint | optional | Absent when the process never started. |
object.process.exit-code | int | optional | The exit code of the job's process, as it passed it to exit. |
object.process.exit-signal | int | optional | The number of the signal that ended the job's process. |
object.job.executable | str.path | required | The path of the executable the job runs. |
object.job.arguments | str[] | required | The job's argument vector, including argv[0]. |
object.job.arguments-truncated | bool | required | Whether object.job.arguments was cut to fit the record. |
object.job.arguments-count | uint | required | How many arguments the job was given, before any were cut. |
object.job.created-time | uint.time | required | When peinit created the job, in nanoseconds since the Unix epoch. |
object.job.started-time | uint.time | optional | When the job's process started, in nanoseconds since the Unix epoch. |
object.job.duration | uint.duration | required | How long the job lasted, in nanoseconds, from its creation to its end. |
object.cgroup.path | str.path | required | The cgroup the event is about, as its full path under /sys/fs/cgroup/peinit. |
object.cgroup.generation | uint | required | The cgroup generation of the service the cgroup belongs to. |
object.job.token.sid | bin.sid | optional | As on peinit.job.started, for a job whose process started. A service's job that ended without starting had no token yet, and is as on peinit.job.created. |
object.job.token.groups | bin.sid[] | optional | Present exactly when the job's process started, or it is a submitted job. |
object.job.token.privileges | uint.flags | optional | Present exactly when object.job.token.groups is. |
object.job.token.privileges-enabled | uint.flags | optional | Present exactly when object.job.token.groups is. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.14 peinit.job.output.dropped
Peios / Using Peios / Events / Service Events
- Event type:
peinit.job.output.dropped - Defined in:
peinit.evman - Tier: standard
- Gating: none
- Cardinality: once per job, on the first line dropped
A submitted job's submitter stopped draining its output, and peinit began dropping the submitter's copy of the job's lines. The copy sent to the log store is unaffected.
Formerly output.dropped.
8.14.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.job.guid | bin.guid | required | The job the event is about. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.15 peinit.job.started
Peios / Using Peios / Events / Service Events
- Event type:
peinit.job.started - Defined in:
peinit.evman - Tier: standard
- Gating: none — every job whose process starts is recorded
- Cardinality: one record per occurrence
A job's process exists and has been executed: the moment a process is
running as object.job.token.sid on peinit's behalf.
Formerly job.started.
8.15.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.job.guid | bin.guid | required | The job the event is about. |
object.job.type | str.enum | required | What the job is for. |
object.job.state | str.enum | required | Always running. |
object.service.name | str | when object.job.type != submitted | The name of the service the event is about, as its definition names it. |
object.job.activation-generation | uint | when object.job.type != submitted | The activation generation of the service the job belongs to, at the time the job was created. |
object.operation.guid | bin.guid | optional | The peinit operation the event is about: a start, stop, restart, reload or reset of one service, from the moment it is requested to the moment it ends. |
object.process.pid | uint | required | The job's process. |
object.cgroup.path | str.path | required | The cgroup the job's process runs in. |
object.job.token.sid | bin.sid | required | The user SID of the token the job's process runs as. |
object.job.token.groups | bin.sid[] | required | The group SIDs of the token the job's process runs as, in the token's order. |
object.job.token.privileges | uint.flags | required | The privileges present on the token the job's process runs as, as the flags of the 64-bit privilege word of uapi/pkm/token.h, one KACS_SE_*_PRIVILEGE bit each. |
object.job.token.privileges-enabled | uint.flags | required | Which of the privileges in object.job.token.privileges are enabled, as flags in the same 64-bit word. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.16 peinit.job.status.reported
Peios / Using Peios / Events / Service Events
- Event type:
peinit.job.status.reported - Defined in:
peinit.evman - Tier: verbose
- Gating: none — every change a submitted job reports is recorded, within the rate bound
- Cardinality: at most one per job per second; a report inside the window updates the job view and writes nothing
A submitted job reported STATUS= or PROGRESS= on the notification
channel, and this is what peinit retained after it.
Formerly job.status.
8.16.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.job.guid | bin.guid | required | The job the event is about. |
object.job.submitter.sid | bin.sid | required | The user SID of the client that submitted the job. |
notify.status | str | optional | The text a service sent as STATUS=: free text describing what it is doing. |
notify.progress.current | uint | optional | How far a service or submitted job has got, as it reported with PROGRESS=: the count before any /. |
notify.progress.total | uint | optional | The end notify.progress.current is counting towards, as reported after the / of PROGRESS=. |
notify.progress.bounded | bool | optional | Whether the progress a service or submitted job reported declares an end: true for PROGRESS=N/ and PROGRESS=N/M, false for a bare PROGRESS=N, which counts with no end (PSPU §4.19). |
notify.progress.unit | str.enum | optional | What notify.progress.current and notify.progress.total count, as reported with PROGRESS_UNIT=. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.17 peinit.notify.errno.reported
Peios / Using Peios / Events / Service Events
- Event type:
peinit.notify.errno.reported - Defined in:
peinit.evman - Tier: standard
- Gating: none — every authenticated
ERRNO=is recorded - Cardinality: one record per occurrence
A service's job sent ERRNO=: its own claim about an error, which peinit
neither acts on nor retains.
Formerly notify.errno.
8.17.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.service.name | str | required | The service whose job sent a notification message. |
subject.job.guid | bin.guid | required | The job whose process sent a notification message: the service's main process, or a submitted job. |
subject.job.activation-generation | uint | required | The activation generation of the service whose job sent a notification message, as object.job.activation-generation describes it. |
subject.operation.guid | bin.guid | optional | The peinit operation the sending job was serving when it sent a notification message, such as the start that a readiness message completes. |
notify.errno | int.errno | optional | Absent when the text sent was not a positive number. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.18 peinit.notify.exit-status.reported
Peios / Using Peios / Events / Service Events
- Event type:
peinit.notify.exit-status.reported - Defined in:
peinit.evman - Tier: standard
- Gating: none — every authenticated
EXIT_STATUS=is recorded - Cardinality: one record per occurrence
A service's job sent EXIT_STATUS=, informationally; it is not the exit
of any process.
Formerly notify.exit_status.
8.18.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.service.name | str | required | The service whose job sent a notification message. |
subject.job.guid | bin.guid | required | The job whose process sent a notification message: the service's main process, or a submitted job. |
subject.job.activation-generation | uint | required | The activation generation of the service whose job sent a notification message, as object.job.activation-generation describes it. |
subject.operation.guid | bin.guid | optional | The peinit operation the sending job was serving when it sent a notification message, such as the start that a readiness message completes. |
notify.exit-status | int | optional | Absent when the text sent was not a number. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.19 peinit.notify.progress.reported
Peios / Using Peios / Events / Service Events
- Event type:
peinit.notify.progress.reported - Defined in:
peinit.evman - Tier: verbose
- Gating: none — every change of progress is recorded, within the rate bound
- Cardinality: at most one per service activation per second
A service's job sent PROGRESS= or PROGRESS_UNIT=, and this is the
progress peinit retained after it.
Formerly notify.progress.
8.19.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.service.name | str | required | The service whose job sent a notification message. |
subject.job.guid | bin.guid | required | The job whose process sent a notification message: the service's main process, or a submitted job. |
subject.job.activation-generation | uint | required | The activation generation of the service whose job sent a notification message, as object.job.activation-generation describes it. |
subject.operation.guid | bin.guid | optional | The peinit operation the sending job was serving when it sent a notification message, such as the start that a readiness message completes. |
notify.progress.current | uint | optional | How far a service or submitted job has got, as it reported with PROGRESS=: the count before any /. |
notify.progress.total | uint | optional | The end notify.progress.current is counting towards, as reported after the / of PROGRESS=. |
notify.progress.bounded | bool | optional | Whether the progress a service or submitted job reported declares an end: true for PROGRESS=N/ and PROGRESS=N/M, false for a bare PROGRESS=N, which counts with no end (PSPU §4.19). |
notify.progress.unit | str.enum | optional | What notify.progress.current and notify.progress.total count, as reported with PROGRESS_UNIT=. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.20 peinit.notify.rejected
Peios / Using Peios / Events / Service Events
- Event type:
peinit.notify.rejected - Defined in:
peinit.evman - Tier: standard
- Gating: none — every refused notification datagram is recorded
- Cardinality: one record per occurrence
peinit refused a datagram on the notification channel and applied none of it: the sender could not be authenticated, the message did not parse, or it made no sense for the job's state. A stream of these from one process is worth reading.
Formerly notify.rejected.
8.20.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
outcome.reason | str.enum | required | Why the datagram was refused. internal-error is peinit unable to act on it, not anything wrong with the datagram; shutdown-refused is a timeout extension (EXTEND_TIMEOUT_USEC=) the shutdown in progress would not grant.Values here (open set): invalid-utf8 · malformed-line · truncated · unauthenticated-sender · missing-service · job-not-running · missing-process · pidfd-mismatch · process-verification-failed · generation-mismatch · missing-start-operation · missing-reload-operation · unsupported-reload-operation · unsupported-ready-state · shutdown-refused · internal-error. |
subject.process.pid | uint | optional | The sending process, when the kernel gave its credentials. |
subject.service.name | str | optional | The sender's service, when peinit could attribute it. |
subject.job.guid | bin.guid | optional | The job whose process sent a notification message: the service's main process, or a submitted job. |
subject.job.activation-generation | uint | optional | The activation generation of the service whose job sent a notification message, as object.job.activation-generation describes it. |
subject.operation.guid | bin.guid | optional | The peinit operation the sending job was serving when it sent a notification message, such as the start that a readiness message completes. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.21 peinit.notify.status.reported
Peios / Using Peios / Events / Service Events
- Event type:
peinit.notify.status.reported - Defined in:
peinit.evman - Tier: verbose
- Gating: none — every authenticated
STATUS=is recorded - Cardinality: one record per occurrence
A service's job sent STATUS= on the notification channel. The text is the
service's own words, not peinit's. Not rate-bounded: a service decides how
often it sends one.
Formerly notify.status.
8.21.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.service.name | str | required | The service whose job sent a notification message. |
subject.job.guid | bin.guid | required | The job whose process sent a notification message: the service's main process, or a submitted job. |
subject.job.activation-generation | uint | required | The activation generation of the service whose job sent a notification message, as object.job.activation-generation describes it. |
subject.operation.guid | bin.guid | optional | The peinit operation the sending job was serving when it sent a notification message, such as the start that a readiness message completes. |
notify.status | str | required | The text a service sent as STATUS=: free text describing what it is doing. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.22 peinit.notify.stopping.reported
Peios / Using Peios / Events / Service Events
- Event type:
peinit.notify.stopping.reported - Defined in:
peinit.evman - Tier: standard
- Gating: none — every authenticated
STOPPING=1is recorded - Cardinality: one record per occurrence
A service's job sent STOPPING=1, so peinit will not send it SIGTERM. The
record exists because the effect is an absence: without it, a service that
rightly got no SIGTERM looks the same afterwards as one that wrongly did
not.
Formerly notify.stopping.
8.22.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.service.name | str | required | The service whose job sent a notification message. |
subject.job.guid | bin.guid | required | The job whose process sent a notification message: the service's main process, or a submitted job. |
subject.job.activation-generation | uint | required | The activation generation of the service whose job sent a notification message, as object.job.activation-generation describes it. |
subject.operation.guid | bin.guid | optional | The peinit operation the sending job was serving when it sent a notification message, such as the start that a readiness message completes. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.23 peinit.on-failure.suppressed
Peios / Using Peios / Events / Service Events
- Event type:
peinit.on-failure.suppressed - Defined in:
peinit.evman - Tier: standard
- Gating: none — every suppressed handler is recorded
- Cardinality: one record per occurrence
peinit declined to start an OnFailure handler, because starting it would have looped or gone too deep, so the failure it was meant to handle went unhandled.
Formerly on_failure.loop_suppressed.
8.23.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.service.name | str | required | The service whose failure the chain was handling. |
object.service.on-failure.name | str | required | The OnFailure handler peinit declined to start, because starting it would have looped or gone too deep. |
object.service.on-failure-chain | str[] | required | The OnFailure handlers already started in the chain that peinit cut short, in the order they were started. |
outcome.reason | str.enum | required | Why the operation resolved the way it did. Values here (closed set): cycle · max-depth. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.24 peinit.operation.ended
Peios / Using Peios / Events / Service Events
- Event type:
peinit.operation.ended - Defined in:
peinit.evman - Tier: standard
- Gating: none — every operation that ends, other than by merging, is recorded
- Cardinality: one record per occurrence
An operation reached a terminal state other than merged: it completed,
failed, was cancelled while still pending, or was aborted while running.
One type for the four, because they are outcomes of one action (PGSS
§6.6); object.operation.state says which.
Formerly operation.completed, operation.failed, operation.cancelled
and operation.aborted.
8.24.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.operation.guid | bin.guid | required | The peinit operation the event is about: a start, stop, restart, reload or reset of one service, from the moment it is requested to the moment it ends. |
object.operation.type | str.enum | required | What the operation does to its service. |
object.operation.source | str.enum | required | Who or what asked for the operation. |
object.operation.state | str.enum | required | completed, failed, cancelled or aborted. |
outcome.success | bool | required | True exactly when object.operation.state is completed. |
outcome.detail | str | optional | The operation's result as peinit words it for a client, such as inactive or startup killed by shutdown. Free text until it is given an enumeration. |
object.operation.duration | uint.duration | optional | From the request, not from the start. Absent on a cancelled operation, which never ran. |
object.service.name | str | required | The name of the service the event is about, as its definition names it. |
subject.token.sid | bin.sid | optional | The user SID of the effective token the operation ran under. |
subject.token.groups | bin.sid[] | optional | The group SIDs carried by the effective token. |
subject.token.privileges | uint.flags | optional | The privileges the effective token holds, as the flags of its 64-bit privilege word from uapi/pkm/token.h: bit n is the privilege whose LUID is n, so SeDebugPrivilege is bit 20. |
subject.token.privileges-enabled | uint.flags | optional | Which of the privileges in subject.token.privileges are enabled, as flags in the same bit layout. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.25 peinit.operation.merged
Peios / Using Peios / Events / Service Events
- Event type:
peinit.operation.merged - Defined in:
peinit.evman - Tier: verbose
- Gating: none — every merge is recorded
- Cardinality: one record per occurrence
An operation was absorbed into another before it ran, and has no outcome
of its own: follow object.operation.merged-into.guid to the operation
whose peinit.operation.ended decides it.
Formerly operation.merged.
8.25.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.operation.guid | bin.guid | required | The peinit operation the event is about: a start, stop, restart, reload or reset of one service, from the moment it is requested to the moment it ends. |
object.operation.type | str.enum | required | What the operation does to its service. |
object.operation.source | str.enum | required | Who or what asked for the operation. |
object.operation.state | str.enum | required | Always merged. |
object.operation.merged-into.guid | bin.guid | required | The operation that absorbed this one. |
object.service.name | str | required | The name of the service the event is about, as its definition names it. |
subject.token.sid | bin.sid | optional | The user SID of the effective token the operation ran under. |
subject.token.groups | bin.sid[] | optional | The group SIDs carried by the effective token. |
subject.token.privileges | uint.flags | optional | The privileges the effective token holds, as the flags of its 64-bit privilege word from uapi/pkm/token.h: bit n is the privilege whose LUID is n, so SeDebugPrivilege is bit 20. |
subject.token.privileges-enabled | uint.flags | optional | Which of the privileges in subject.token.privileges are enabled, as flags in the same bit layout. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.26 peinit.operation.requested
Peios / Using Peios / Events / Service Events
- Event type:
peinit.operation.requested - Defined in:
peinit.evman - Tier: standard
- Gating: none — every operation is recorded
- Cardinality: one record per occurrence
An operation on a service — a start, stop, restart, reload or reset — was
created. A client's command creates one with object.operation.source
admin; every other source is peinit acting for its own reasons. Every
operation in a graph context is requested when the context is built.
Formerly operation.requested.
8.26.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.operation.guid | bin.guid | required | The peinit operation the event is about: a start, stop, restart, reload or reset of one service, from the moment it is requested to the moment it ends. |
object.operation.type | str.enum | required | What the operation does to its service. |
object.operation.source | str.enum | required | Who or what asked for the operation. |
object.operation.state | str.enum | required | Always pending. |
object.service.name | str | required | The name of the service the event is about, as its definition names it. |
subject.token.sid | bin.sid | optional | The client whose command created the operation. Absent for an operation peinit created itself. |
subject.token.groups | bin.sid[] | optional | Written only when peinit holds the client's whole token. For a control-channel caller it holds the user SID alone, so today an operation's subject is subject.token.sid and nothing more. |
subject.token.privileges | uint.flags | optional | Present exactly when subject.token.groups is. |
subject.token.privileges-enabled | uint.flags | optional | Present exactly when subject.token.groups is. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.27 peinit.operation.started
Peios / Using Peios / Events / Service Events
- Event type:
peinit.operation.started - Defined in:
peinit.evman - Tier: verbose
- Gating: none — every operation that begins to run is recorded
- Cardinality: one record per occurrence
An operation left the queue and began to run.
Formerly operation.started.
8.27.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.operation.guid | bin.guid | required | The peinit operation the event is about: a start, stop, restart, reload or reset of one service, from the moment it is requested to the moment it ends. |
object.operation.type | str.enum | required | What the operation does to its service. |
object.operation.source | str.enum | required | Who or what asked for the operation. |
object.operation.state | str.enum | required | Always running. |
object.service.name | str | required | The name of the service the event is about, as its definition names it. |
subject.token.sid | bin.sid | optional | The user SID of the effective token the operation ran under. |
subject.token.groups | bin.sid[] | optional | The group SIDs carried by the effective token. |
subject.token.privileges | uint.flags | optional | The privileges the effective token holds, as the flags of its 64-bit privilege word from uapi/pkm/token.h: bit n is the privilege whose LUID is n, so SeDebugPrivilege is bit 20. |
subject.token.privileges-enabled | uint.flags | optional | Which of the privileges in subject.token.privileges are enabled, as flags in the same bit layout. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.28 peinit.recovery.entered
Peios / Using Peios / Events / Service Events
- Event type:
peinit.recovery.entered - Defined in:
peinit.evman - Tier: essential
- Gating: none
- Cardinality: once, when peinit gives up on the boot and starts the recovery shell
peinit dropped the machine to a recovery shell, for the reason in
outcome.reason. Written before the registry or eventd may exist, into the
ring, which is where the record of a boot that failed this early survives.
Essential because it is rare and means the machine did not boot.
Formerly recovery.entered.
8.28.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
outcome.reason | str.enum | required | Why the operation resolved the way it did. Values here (open set): kernel-command-line · privileges · boot-attempt-counter · forced-by-kernel-command-line · boot-attempt-threshold-reached · root-writable · virtual-filesystems · machine-id · rtc-clock · registryd · provisioning · infrastructure · phase2 · runtime. |
outcome.detail | str | optional | What went wrong, in the error's own words, such as peinit is missing required privilege(s): SeAuditPrivilege. Absent for forced-by-kernel-command-line, which has nothing to add, and for phase2, whose findings are their own peinit.graph.validation.failed records. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.29 peinit.service.abandoned
Peios / Using Peios / Events / Service Events
- Event type:
peinit.service.abandoned - Defined in:
peinit.evman - Tier: standard
- Gating: none
- Cardinality: once per service abandoned
A service's processes survived SIGKILL and its post-kill timeout, so peinit
gave up on them and moved the service to abandoned: its cgroup is still
populated, unkillably. Written only on the shutdown path so far; the
name also covers an abandonment anywhere else.
Formerly shutdown.abandoned.
8.29.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.service.name | str | required | The name of the service the event is about, as its definition names it. |
object.cgroup.path | str.path | required | The cgroup the event is about, as its full path under /sys/fs/cgroup/peinit. |
object.service.state-previous | str.enum | required | The service's state before the event, with the same values as object.service.state. |
object.service.state | str.enum | required | Always abandoned. |
object.service.transition-cause | str.enum | required | Why the service's state changed. |
object.service.generation | uint | required | The service's activation generation: a counter peinit advances each time the service enters starting, so that every incarnation of a service has its own number. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
8.30 peinit.service.reload.timed-out
Peios / Using Peios / Events / Service Events
- Event type:
peinit.service.reload.timed-out - Defined in:
peinit.evman - Tier: standard
- Gating: none — every reload left unconfirmed past its deadline is recorded
- Cardinality: one record per occurrence
A service announced RELOADING=1 and never confirmed the reload by its
deadline: it wedged mid-reload, or lost its handler. Recorded because the
default reload does not wait, so nothing else would say so.
Formerly service.reload_unconfirmed.
8.30.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.service.name | str | required | The name of the service the event is about, as its definition names it. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
9.1 peipkg.action.authorised
Peios / Using Peios / Events / Package Events
- Event type:
peipkg.action.authorised - Defined in:
peipkg.evman - Tier: essential
- Gating: none — every authorisation is recorded
- Cardinality: once per elevated action put to the operator, and once per stale repository proceeded with
The record that the operator was asked to authorise an elevated action
specifically, apart from the routine proceed prompt, and what they answered.
outcome.success false means they declined. The routine prompt and its
--yes never answer one of these. Asked about several actions, the
operator answers them in turn, and the first refusal ends the questions.
stale-trust-state and stale-index are answered by --allow-stale
rather than a prompt, so they are only ever recorded as authorised: without
the flag the command fails instead, and writes no record of this type.
Replaces peipkg.authorisation.
9.1.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The user SID of the effective token the operation ran under. |
operation.name | str.enum | required | Which elevated action was put to the operator. Values here (open set): low-trust-provides · foreign-replaces · downgrade · remove-modified-file · stale-trust-state · stale-index. |
object.package.name | str | optional | The package the action concerns: the package substituted by provides for low-trust-provides, the successor for foreign-replaces, the package moving backward for downgrade, and the package being uninstalled for remove-modified-file. |
object.file.path | str.path | when operation.name == remove-modified-file | The modified file the uninstall would remove, as the package database records it: absolute within the root peipkg operated on. |
object.repository.name | str | optional | The repository, for stale-trust-state and stale-index. |
outcome.success | bool | required | False when the operator declined. For remove-modified-file, keeping the file and aborting the transaction both decline the removal. |
outcome.detail | str | optional | The action as it was put to the operator. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peipkg.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
9.2 peipkg.claim.changed
Peios / Using Peios / Events / Package Events
- Event type:
peipkg.claim.changed - Defined in:
peipkg.evman - Tier: standard
- Gating: none
- Cardinality: once per claim grant or revoke
The record that peipkg granted a role to a package or revoked it, or tried
to and failed. A grant names the new holder; a revoke leaves
object.claim.holder out. A change the operator declines at the proceed
prompt writes nothing.
Replaces peipkg.claim.
9.2.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The user SID of the effective token the operation ran under. |
transaction.id | uint | optional | The transaction the operation belonged to, registry or package. |
object.claim.role | str | required | The role a claim event changed the holder of. |
object.claim.holder | str | optional | The package that holds object.claim.role after the event, by name. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.reason | str.enum | when outcome.success == false | Why the operation resolved the way it did. Values here (open set): stale · busy · denied · unowned · alternate-upgrade · unresolvable · untrusted · failed. |
outcome.detail | str | optional | Free text about the outcome, for a person to read. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peipkg.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
9.3 peipkg.package.installed
Peios / Using Peios / Events / Package Events
- Event type:
peipkg.package.installed - Defined in:
peipkg.evman - Tier: essential
- Gating: none — every installation is recorded
- Cardinality: once per package a transaction installs; once per named package on a rejected request
The record that peipkg installed a package, or tried to and failed. A
transaction writes one record for each package its plan installs, whichever
command ran it: an upgrade that pulls in a new dependency writes this
event for the dependency, and undoing an uninstall writes it for the
package restored. A request refused before any transaction opened writes
one failed record for each package it named.
Essential because a package installation changes what the system runs, and
it is rare. Replaces peipkg.install, and the install half of
peipkg.transaction-failed.
9.3.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The operator: the user SID of the token peipkg ran under. This is peipkg's own word; the kernel-stamped emitter.token.guid is the identity it cannot forge. |
transaction.id | uint | optional | The peipkg transaction, for every record of one transaction. Absent when the request was refused before a transaction opened, or when a cross-root transaction failed while preparing its roots. In a cross-root transaction, the transaction of the root this package is installed in. |
object.package.name | str | optional | Absent only on a refused request that named no package. |
object.package.version | str | optional | The version installed. Absent on a refused request that named no version. |
object.package.architecture | str | optional | The architecture of the package an event is about: an identifier of PSPU §5.8, such as x86_64, aarch64 or noarch. |
source.repository.name | str | optional | Absent for an install from a local file. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.reason | str.enum | when outcome.success == false | peipkg's stable error code for the failure, the one its driven mode reports. Whether anything was rolled back follows from transaction.id: a failure with one was rolled back, a failure without one was refused before it began.Values here (open set): stale · busy · denied · unowned · alternate-upgrade · unresolvable · untrusted · failed. |
outcome.detail | str | optional | The error as peipkg reported it to the operator. Absent on success. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peipkg.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
9.4 peipkg.package.uninstalled
Peios / Using Peios / Events / Package Events
- Event type:
peipkg.package.uninstalled - Defined in:
peipkg.evman - Tier: essential
- Gating: none — every uninstallation is recorded
- Cardinality: once per package a transaction removes; once per named package on a rejected request
The record that peipkg removed a package, or tried to and failed. A transaction writes one record for each package its plan removes, including a package removed to resolve a conflict during an install, and a package removed by undoing its install.
Replaces peipkg.uninstall, and the uninstall half of
peipkg.transaction-failed.
9.4.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The user SID of the effective token the operation ran under. |
transaction.id | uint | optional | Absent when the request was refused before a transaction opened. |
object.package.name | str | required | The name of the package the event is about. |
object.package.version | str | optional | The version removed. Absent on a refused request. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.reason | str.enum | when outcome.success == false | Why the operation resolved the way it did. Values here (open set): stale · busy · denied · unowned · alternate-upgrade · unresolvable · untrusted · failed. |
outcome.detail | str | optional | Free text about the outcome, for a person to read. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peipkg.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
9.5 peipkg.package.upgraded
Peios / Using Peios / Events / Package Events
- Event type:
peipkg.package.upgraded - Defined in:
peipkg.evman - Tier: essential
- Gating: none — every upgrade, downgrade and undo is recorded
- Cardinality: once per package a transaction moves to another version; once per named package on a rejected request
The record that peipkg moved an installed package to another version, or
tried to and failed. A downgrade and an undo are recorded here too:
comparing object.package.version with object.package.version-previous
by PSPU §5.6 tells which way the package moved. A refused downgrade or undo
is recorded here as well, with the version it asked for.
Replaces peipkg.upgrade, and the upgrade half of
peipkg.transaction-failed.
9.5.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The user SID of the effective token the operation ran under. |
transaction.id | uint | optional | Absent when the request was refused before a transaction opened, or when a cross-root transaction failed while preparing its roots. |
object.package.name | str | optional | Absent only on a refused request that named no package, such as an upgrade of everything. |
object.package.version | str | optional | The version the package moved to. |
object.package.version-previous | str | optional | The version it moved from. Absent on a refused request. |
object.package.architecture | str | optional | The architecture of the package an event is about: an identifier of PSPU §5.8, such as x86_64, aarch64 or noarch. |
source.repository.name | str | optional | Absent for an upgrade from a local file. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.reason | str.enum | when outcome.success == false | Why the operation resolved the way it did. Values here (open set): stale · busy · denied · unowned · alternate-upgrade · unresolvable · untrusted · failed. |
outcome.detail | str | optional | Free text about the outcome, for a person to read. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peipkg.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
9.6 peipkg.repository.added
Peios / Using Peios / Events / Package Events
- Event type:
peipkg.repository.added - Defined in:
peipkg.evman - Tier: essential
- Gating: none — every repository trust ceremony is recorded
- Cardinality: once per repository add
The record that peipkg added a repository and established trust in it, or
tried to and failed. Adding a repository decides whose packages the system
will install, which is why it is essential. A command refused before it
names a base URL, such as repo add <name> for a repository with no
configuration, writes nothing.
Replaces peipkg.repo-add.
9.6.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The user SID of the effective token the operation ran under. |
object.repository.name | str | required | The repository the event is about, by the name it is configured under. |
object.repository.url | str | required | The base URL of the repository the event is about, which is what identifies it (PSPU §5.36). |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.reason | str.enum | when outcome.success == false | Why the operation resolved the way it did. Values here (open set): stale · busy · denied · unowned · alternate-upgrade · unresolvable · untrusted · failed. |
outcome.detail | str | optional | The error as peipkg reported it to the operator. Absent on success. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peipkg.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
9.7 peipkg.repository.reconfigured
Peios / Using Peios / Events / Package Events
- Event type:
peipkg.repository.reconfigured - Defined in:
peipkg.evman - Tier: essential
- Gating: none — every trust-relevant change is recorded
- Cardinality: once per changed setting
The record that a repository add changed one of an existing repository's
trust-relevant settings. One record for each setting that changed, written
after the add's own peipkg.repository.added. The settings are
signature_policy, allow_insecure_transport, max_trusted_age_days,
min_index_version, max_index_staleness_days and trust_anchors.
Only a change made through the flags of repo add is seen. An operator who
edits the repository file directly and re-runs the ceremony leaves nothing
to compare.
Replaces peipkg.config-change.
9.7.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The user SID of the effective token the operation ran under. |
object.repository.name | str | required | The repository the event is about, by the name it is configured under. |
config.name | str | required | The setting's name as written in the repository file. |
config.text | str | when config.name == signature_policy | A setting's value in force, where the value is not an integer: a string, or the textual rendering of a value of another registry type. |
config.text-previous | str | when config.name == signature_policy | The text value a change replaced, beside config.text. |
config.value | uint | optional | The setting in force, for every setting except signature_policy: a number of days or an index version as written, 0 for a setting left at its default; 1 or 0 for allow_insecure_transport; for trust_anchors, how many anchors are configured, so replacing one anchor with another records equal counts. |
config.value-previous | uint | optional | The setting it replaced, in the same form as config.value. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peipkg.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
9.8 peipkg.repository.refreshed
Peios / Using Peios / Events / Package Events
- Event type:
peipkg.repository.refreshed - Defined in:
peipkg.evman - Tier: standard
- Gating: none
- Cardinality: once per refresh command
The record that peipkg refreshed the metadata of its configured repositories. One refresh covers every repository it was asked to; a failure of one does not stop the others, so a refresh can partly succeed. A refresh that fails before it reaches any repository writes nothing.
Replaces peipkg.refresh.
9.8.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The user SID of the effective token the operation ran under. |
operation.succeeded-count | uint | required | Repositories refreshed. |
operation.failed-count | uint | required | Repositories whose refresh failed. |
outcome.success | bool | required | False when any repository failed to refresh. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peipkg.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
9.9 peipkg.repository.removed
Peios / Using Peios / Events / Package Events
- Event type:
peipkg.repository.removed - Defined in:
peipkg.evman - Tier: essential
- Gating: none — every repository removal is recorded
- Cardinality: once per repository remove
The record that peipkg removed a repository from its configuration and database, or tried to and failed. Removing a repository that is not configured succeeds.
Replaces peipkg.repo-remove.
9.9.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The user SID of the effective token the operation ran under. |
object.repository.name | str | required | The repository the event is about, by the name it is configured under. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.detail | str | optional | The error as peipkg reported it to the operator. Absent on success. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peipkg.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
9.10 peipkg.transaction.recovered
Peios / Using Peios / Events / Package Events
- Event type:
peipkg.transaction.recovered - Defined in:
peipkg.evman - Tier: standard
- Gating: none
- Cardinality: once per run of peipkg recover
The record that peipkg recover reconciled interrupted transactions,
rolling back each one it found pending, or failed to. Every run writes it,
one that found nothing to recover included. An automatic recovery at the
start of an ordinary operation does not write it.
Replaces peipkg.recovery.
9.10.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The user SID of the effective token the operation ran under. |
operation.succeeded-count | uint | required | Interrupted transactions rolled back, before the failure on a run that failed. Zero when there were none. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.detail | str | optional | The error, on a run that failed. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from peipkg.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
10.1 eventd.config.changed
Peios / Using Peios / Events / Event Store Events
- Event type:
eventd.config.changed - Defined in:
eventd.evman - Tier: essential
- Gating: none — every applied change is recorded
- Cardinality: once per configuration value whose change was applied
The record that eventd applied a change to one of its configuration values at runtime. A change to retention or to the stores is a change to what the audit trail keeps, which is why this is essential.
Every value eventd reloads at runtime is a REG_DWORD or a REG_QWORD, so
each side of a change is an integer or absent. A side is absent when no
value is stored, even though eventd's compiled default then applies. A
stored value of the wrong type is reported as the expected type, holding
the integer eventd actually uses.
Replaces synthetic.config_change (PEI-617). Its values were decimal
strings and its absent values nil.
10.1.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
config.key.path | str | required | Always Machine\System\eventd. |
config.name | str | required | The name of the configuration value being reported on, within config.key.path. |
config.type | uint.enum | optional | Absent when the change removed the value. Present exactly when config.value is. |
config.type-previous | uint.enum | optional | Absent when the value was not set before. Present exactly when config.value-previous is. |
config.value | uint | optional | The value in force after the event. |
config.value-previous | uint | optional | The value that was in force before the change. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from eventd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
10.2 eventd.daemon.started
Peios / Using Peios / Events / Event Store Events
- Event type:
eventd.daemon.started - Defined in:
eventd.evman - Tier: essential
- Gating: none — written on every start
- Cardinality: once per start of eventd
The record that eventd started, attached to KMES and decided where to resume
reading each CPU's ring. Like the other four types eventd writes about
itself (eventd.daemon.stopped, eventd.events.lost,
eventd.store.quarantined and eventd.config.changed), eventd writes it
straight into its event store and never through KMES. It
therefore has no event.sequence and no emitter.*: only event.type,
event.time (when eventd wrote it) and event.boot.guid.
Essential because the audit store starting is rare, and a start with no
matching stop is the evidence that eventd crashed. No emission policy
applies to it or to the other four: eventd is their emitter, and an
essential type is always written (PGSS §6.9). No KMES event of any of the
five types is stored, so event.type alone marks a record eventd wrote.
Replaces synthetic.startup (PEI-617). It carries no boot ID of its own,
because that is always the record's event.boot.guid (PEI-1390).
10.2.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
store.restarted | bool | required | Whether this boot's events were already in the store when eventd started, which is to say eventd had already run during this boot and this start is a restart. |
store.shard-count | uint | required | How many active event-store shards eventd is running with, after resolving its configured shard count. |
store.resume.cpus | uint[] | required | The CPUs whose KMES rings eventd resumed reading at startup, one entry per logical CPU in ascending order. |
store.resume.sequences | uint[] | required | For each CPU in store.resume.cpus, the highest event sequence of this boot already committed with no gap before it, which is where eventd resumed reading that CPU's ring. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from eventd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
10.3 eventd.daemon.stopped
Peios / Using Peios / Events / Event Store Events
- Event type:
eventd.daemon.stopped - Defined in:
eventd.evman - Tier: essential
- Gating: none — written on every graceful shutdown
- Cardinality: once per graceful shutdown
The record that eventd shut down gracefully, written after it stopped reading the rings and committed everything it had read. A crash writes nothing, which is how a crash is told apart from a shutdown.
Replaces synthetic.shutdown (PEI-617).
10.3.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
store.committed.cpus | uint[] | optional | Absent when eventd could not read its record of what it had committed (PEI-1394), rather than written as zeroes. |
store.committed.sequences | uint[] | optional | Present exactly when store.committed.cpus is. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from eventd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
10.4 eventd.events.lost
Peios / Using Peios / Events / Event Store Events
- Event type:
eventd.events.lost - Defined in:
eventd.evman - Tier: essential
- Gating: none — every gap found is recorded
- Cardinality: once per contiguous run of missing sequence numbers on one ring
The record that eventd found events missing from a CPU's ring: sequence numbers it never read and never stored. KMES overwrote them before eventd read them, or eventd consumed them while it had nowhere to store them.
event.time is the time of the ring event that revealed the gap, so
loss.preceding-time and event.time bound when the lost events were
written, and the record sorts beside that event. It is not when eventd
noticed, which after a restart can be much later. Where eventd consumed the
events itself while it had nowhere to store them, it is the time of the
last of them. The record also carries the ring's CPU as event.cpu.
Replaces synthetic.gap (PEI-617). Its cpu_id became buffer.cpu
(PEI-1390), and its revealing_timestamp the record's event.time.
10.4.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
buffer.cpu | uint | required | The logical CPU whose ring buffer a record concerns. |
loss.sequence | uint | required | The first sequence number a loss covers. |
loss.sequence-last | uint | required | The last sequence number of a gap, where loss.sequence is the first. |
loss.count | uint | required | The number of events lost in one occurrence. |
loss.preceding-time | uint.time | optional | The time of the last record received before a gap, in nanoseconds since the Unix epoch. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from eventd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
10.5 eventd.store.quarantined
Peios / Using Peios / Events / Event Store Events
- Event type:
eventd.store.quarantined - Defined in:
eventd.evman - Tier: essential
- Gating: none — every quarantine is recorded
- Cardinality: once per store or shard quarantined
The record that eventd found one of its stores corrupt, moved it aside and started a new one in its place. Whatever the quarantined file held is no longer queryable. This is what an operator alerts on.
It is written only for a quarantine. A store that is full, or a write that fails for any other reason, writes none. The record goes to shard 0, else to the lowest-numbered writable event shard, and never to a failed shard until it has been replaced.
Replaces synthetic.storage_error (PEI-617).
10.5.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
store.kind | str.enum | required | Which of eventd's stores a record is about: the sharded event store, the log store or the metric store. |
store.shard | uint | when store.kind == event | Absent for the log and metric stores, which are not sharded (PEI-1394). |
outcome.detail | str | required | The error eventd found, as it describes it. Its wording is not stable. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from eventd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
11.1 authd.logon.attempted
Peios / Using Peios / Events / Authentication Events
- Event type:
authd.logon.attempted - Defined in:
authd.evman - Tier: essential
- Gating: none — every logon that reaches a principal source's question, or is refused before one, is recorded
- Cardinality: once per
LogonStartauthd reads; a connection that opens with anything else writes none
The record that something asked authd to sign a principal in, and whether it did. Written once per logon conversation, when it ends: with a token minted and handed over, or with the denial authd sent the originator. Success and failure are one type, so "every failed logon" is one query.
The principal's name is never recorded. On a failure authd does not
know who was signing in: a principal source answers "authentication
failed" alike for a name it does not hold and for a wrong credential, and
the name the originator sent is the caller's input, not an identity. The
source's own record, such as lpsd.credential.verified, says what the
wire hides. On a success the principal is subject.token.sid.
Essential because a logon is the most important thing an operating system records about who uses it, and a machine signs people in rarely enough for every attempt to be kept.
11.1.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
source.token.sid | bin.sid | required | The originator: the principal of the process on the other end of /run/logon.sock that asked for the logon. |
source.address | str.ip | optional | The remote peer the originator says the logon is for, where it named one and it is an IP address. It is the originator's word, as login -h or sshd passes it; a host name is not recorded. |
object.session.logon-type | str.enum | required | The logon type the originator asked for, whether or not it was granted. |
object.session.auth-package | str | optional | The principal source authd asked to verify the credential. Absent when the logon was refused before a source was chosen. |
subject.token.sid | bin.sid | when outcome.success == true | The principal signed in: the user SID of the token minted. |
object.session.id | uint.luid | optional | The logon session created for the sign-on. Every record of what the principal does in it carries the same value as subject.token.auth-id. Present whenever a token was minted: on a success, and on an undelivered failure. |
object.token.id | uint.luid | optional | The token minted for the originator, present exactly when object.session.id is. |
outcome.success | bool | required | True only when the token reached the originator. |
outcome.reason | str.enum | when outcome.success == false | The denial authd sent the originator, in kebab-case, whether authd decided it or relayed it from the source; authentication-failed covers an unknown principal and a wrong credential alike. abandoned is a conversation that ended without a terminal answer, because the originator's or the source's connection failed. undelivered is a token minted that could not be handed over; its session is reaped once nothing holds it.Values here (open set): malformed-request · unsupported-version · permission-denied · authentication-failed · logon-type-not-permitted · account-restricted · authority-unavailable · conversation-limit · internal · abandoned · undelivered. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from authd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
11.2 authd.service.attested
Peios / Using Peios / Events / Authentication Events
- Event type:
authd.service.attested - Defined in:
authd.evman - Tier: standard
- Gating: none — every service attestation that names a service is recorded
- Cardinality: once per
ServiceAttestauthd reads
The record that the service manager asked authd for a service's token, with no credential behind it, and whether authd minted one. The service manager is PID 1 running as SYSTEM, and authd takes the service name on its word, because the process the token is for does not exist yet (PGSS Logon §2.19).
Standard rather than essential because every service with an identity other than SYSTEM is attested on every start, so a machine writes these in proportion to its service restarts.
11.2.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The principal that asked: the user SID of the peer's token, SYSTEM for the service manager. |
object.service.name | str | optional | The service the token is for, as the service manager named it. Absent only when the request named no service. |
object.token.sid | bin.sid | optional | The identity attested: the service's virtual account, a built-in service identity such as LocalService, or a principal designated for service logon. Present whenever a token was minted: on a success, and on an undelivered failure. |
object.session.id | uint.luid | optional | The logon session created for the service, present exactly when object.token.sid is. |
object.token.id | uint.luid | optional | The token minted, present exactly when object.token.sid is. |
object.token.privileges | uint.flags | optional | The privileges local policy granted the token, all of them enabled. Present exactly when object.token.sid is. |
outcome.success | bool | required | True only when the token reached the service manager. |
outcome.reason | str.enum | when outcome.success == false | The denial authd sent. permission-denied is a peer that is not the service manager; account-restricted an identity no source holds, or one not designated for service logon, which authd does not tell apart; undelivered a token minted that could not be handed over.Values here (open set): permission-denied · malformed-request · account-restricted · internal · undelivered. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from authd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
11.3 authd.session.ended
Peios / Using Peios / Events / Authentication Events
- Event type:
authd.session.ended - Defined in:
authd.evman - Tier: essential
- Gating: none — every session end authd carries out is recorded; a refused request and a query are not
- Cardinality: once per
SessionEndauthd acts on
The record that authd ended a logon session at someone's request, by
ending every process whose primary token belongs to it (PGSS Logon
§2.22). The kernel destroys the session when the last reference drops,
and records that as kacs.session.destroyed; this record adds who asked,
which the kernel cannot know.
A refusal is not recorded here. Who may end another principal's session is
decided by an access check against the SessionEndSecurity descriptor,
which KACS records as kacs.audit.access.checked with object.kind
authd-session-end when the descriptor's SACL asks it to.
Essential because ending somebody's session is rare and is an administrative act on another principal.
11.3.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The principal that asked: themselves, signing out, or someone the descriptor permits to end other principals' sessions. |
object.session.id | uint.luid | required | The LUID of the logon session the operation acted on. |
object.session.user.sid | bin.sid | required | Whose session it was. |
object.session.logon-type | str.enum | required | What kind of logon the session represents, from the KACS_LOGON_TYPE_* values in uapi/pkm/token.h. |
outcome.success | bool | required | True when no process was left holding the session. |
outcome.reason | str.enum | when outcome.success == false | Some processes survived every round, or could not be examined. The session lives on until they exit. Values here (open set): processes-remaining. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from authd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
12.1 lpsd.account.created
Peios / Using Peios / Events / Local Principal Events
- Event type:
lpsd.account.created - Defined in:
lpsd.evman - Tier: essential
- Gating: none — every request to create an account is recorded
- Cardinality: once per request
The record that an administrator created a local account with lps, or
tried to. The first account on a new machine is created this way too, by
the boot-time service that provisions it.
Essential because a new account is a new way in, and it is rare.
12.1.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The administrator: the user SID of the token that asked. |
object.account.sid | bin.sid | when outcome.success == true | The new account. |
outcome.success | bool | required | True once the account is durable. |
outcome.reason | str.enum | when outcome.success == false | The failure lps was told: not-found names a group that does not exist, exists an account name already in use, invalid a value the store refuses, not-saved a change that could not be made durable.Values here (open set): not-found · exists · invalid · internal · not-saved. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lpsd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
12.2 lpsd.account.deleted
Peios / Using Peios / Events / Local Principal Events
- Event type:
lpsd.account.deleted - Defined in:
lpsd.evman - Tier: essential
- Gating: none — every request to delete an account is recorded
- Cardinality: once per request
The record that an administrator deleted a local account with lps, or
tried to.
Essential because deleting an account is irreversible and rare: its SID is never issued again, and every descriptor naming it names nobody.
12.2.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The administrator. |
object.account.sid | bin.sid | optional | The account. Absent when no account had the name asked for. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.reason | str.enum | when outcome.success == false | invalid is a deletion the store refuses, such as of the last administrator.Values here (open set): not-found · invalid · internal · not-saved. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lpsd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
12.3 lpsd.account.modified
Peios / Using Peios / Events / Local Principal Events
- Event type:
lpsd.account.modified - Defined in:
lpsd.evman - Tier: essential
- Gating: none — every administrative change to an account, and every change of one's own display name, is recorded; a request that changes nothing is not
- Cardinality: once per request that changes something or fails
The record that an account was changed: by an administrator with lps,
or by the principal themselves where the self socket allows it. Which
change it was is operation.name.
A request that would leave the account as it was, such as enabling an enabled account, changes nothing and writes no record.
Essential because an account's settings decide who can sign in and as what, and they change rarely.
12.3.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The principal that asked: an administrator, or the account itself for set-display-name. |
object.account.sid | bin.sid | optional | The account changed. Absent when no account had the name asked for. |
object.group.sid | bin.sid | optional | For set-primary-group, the group made the account's primary group. |
operation.name | str.enum | required | The change, as lps names its command. set-display-name is the principal's own, through the self socket.Values here (open set): set-enabled · set-password · set-profile · set-display-name · set-primary-group · set-claim · remove-claim · rename · set-logon-types · key-add · key-remove · set-credential-policy. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.reason | str.enum | when outcome.success == false | denied is a disabled account changing its own display name.Values here (open set): not-found · exists · invalid · internal · denied · not-saved. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lpsd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
12.4 lpsd.credential.changed
Peios / Using Peios / Events / Local Principal Events
- Event type:
lpsd.credential.changed - Defined in:
lpsd.evman - Tier: essential
- Gating: none — every change of an account's own credential is recorded, and every refusal once the account is known
- Cardinality: once per change conversation that reaches the account
The record that a principal changed their own password, or added or
removed one of their own SSH keys, through authd (PSPU §2.21, §2.23), or
tried to and was refused. A change an administrator makes with lps is
lpsd.account.modified.
Essential because a changed credential is how an account changes hands, and it is rare.
12.4.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The principal that asked, as authd vouched for them from their token. The same SID as object.account.sid: only one's own credential can be changed this way. |
object.account.sid | bin.sid | required | The SID of the account a record is about: a principal a principal source holds, whose credential was checked or changed, or which was created, deleted or modified. |
operation.name | str.enum | required | Which operation was attempted. Values here (open set): password · ssh-key-added · ssh-key-removed. |
outcome.success | bool | required | True once the change is durable. |
outcome.reason | str.enum | when outcome.success == false | no-credential is an account with no password to prove itself with; superseded a password that changed between the rounds of the conversation; rejected a new password or key the store refused; conversation-limit too many unusable new passwords; not-saved a change the store could not make durable, so it was not applied.Values here (open set): no-such-account · disabled · no-credential · wrong-credential · superseded · rejected · conversation-limit · not-saved. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lpsd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
12.5 lpsd.credential.verified
Peios / Using Peios / Events / Local Principal Events
- Event type:
lpsd.credential.verified - Defined in:
lpsd.evman - Tier: standard
- Gating: none — every credential lpsd checks for a logon is recorded
- Cardinality: once per logon lpsd decides; probing which SSH keys an account would accept writes none
The record that lpsd checked a credential for a logon authd relayed to it,
and what it found. It says what the wire must not: authd and the
originator are told only "authentication failed" whether the account does
not exist or the credential was wrong, and outcome.reason here tells
them apart for whoever may read the audit trail.
The account is named by SID when it exists. The name the caller typed is
never recorded, so a failure against a name lpsd does not hold carries no
account at all; authd.logon.attempted is the same logon as the authority
saw it.
Standard rather than essential because a machine exposed to a network can be asked to check credentials far more often than it signs anyone in.
12.5.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | lpsd itself, the principal that checked the credential: the user SID of lpsd's own token, which is its service account. |
object.account.sid | bin.sid | optional | The account the credential was checked for. Absent when lpsd holds no account by the name it was asked about. |
operation.name | str.enum | required | What was checked. none is an account whose policy is that it signs in with no credential, asserted without asking for one.Values here (open set): password · ssh-key · none. |
outcome.success | bool | required | True when lpsd told authd who the principal is. |
outcome.reason | str.enum | when outcome.success == false | not-permitted is an account whose credential policy does not allow the kind of credential offered, or a key the account does not hold.Values here (open set): no-such-account · wrong-credential · disabled · not-permitted. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lpsd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
12.6 lpsd.group.created
Peios / Using Peios / Events / Local Principal Events
- Event type:
lpsd.group.created - Defined in:
lpsd.evman - Tier: essential
- Gating: none — every request to create a group is recorded
- Cardinality: once per request
The record that an administrator created a local group with lps, or
tried to.
Essential because groups are what descriptors grant to, and they are created rarely.
12.6.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The administrator. |
object.group.sid | bin.sid | when outcome.success == true | The new group. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.reason | str.enum | when outcome.success == false | Why the operation resolved the way it did. Values here (open set): exists · invalid · internal · not-saved. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lpsd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
12.7 lpsd.group.deleted
Peios / Using Peios / Events / Local Principal Events
- Event type:
lpsd.group.deleted - Defined in:
lpsd.evman - Tier: essential
- Gating: none — every request to delete a group is recorded
- Cardinality: once per request
The record that an administrator deleted a local group with lps, or
tried to.
Essential because deleting a group removes every grant made to it, and it is rare.
12.7.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The administrator. |
object.group.sid | bin.sid | optional | The group. Absent when no group had the name asked for. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.reason | str.enum | when outcome.success == false | Why the operation resolved the way it did. Values here (open set): not-found · invalid · internal · not-saved. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lpsd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
12.8 lpsd.group.member.added
Peios / Using Peios / Events / Local Principal Events
- Event type:
lpsd.group.member.added - Defined in:
lpsd.evman - Tier: essential
- Gating: none — every request to add a membership is recorded; one that already held is not
- Cardinality: once per request that changes something or fails
The record that an administrator added an account to a group with lps,
or tried to. The group may be a well-known one: adding an account to
BUILTIN\Administrators is recorded here.
Essential because a membership is a grant of everything granted to the group, and memberships change rarely.
12.8.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The administrator. |
object.account.sid | bin.sid | optional | The account. Absent when no account had the name asked for. |
object.group.sid | bin.sid | optional | The group. Absent when no group had the name asked for. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.reason | str.enum | when outcome.success == false | Why the operation resolved the way it did. Values here (open set): not-found · invalid · internal · not-saved. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lpsd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
12.9 lpsd.group.member.removed
Peios / Using Peios / Events / Local Principal Events
- Event type:
lpsd.group.member.removed - Defined in:
lpsd.evman - Tier: essential
- Gating: none — every request to remove a membership is recorded; one that did not hold is not
- Cardinality: once per request that changes something or fails
The record that an administrator removed an account from a group with
lps, or tried to.
Essential for the reason lpsd.group.member.added is: a membership is a
grant, and taking one away is as much an access change as giving it.
12.9.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
subject.token.sid | bin.sid | required | The administrator. |
object.account.sid | bin.sid | optional | The account. Absent when no account had the name asked for. |
object.group.sid | bin.sid | optional | The group. Absent when no group had the name asked for. |
outcome.success | bool | required | Whether the operation succeeded. |
outcome.reason | str.enum | when outcome.success == false | invalid includes removing the last administrator from BUILTIN\Administrators.Values here (open set): not-found · invalid · internal · not-saved. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from lpsd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
13.1 timed.clock.stepped
Peios / Using Peios / Events / Time Events
- Event type:
timed.clock.stepped - Defined in:
timed.evman - Tier: essential
- Gating: none — every step timed makes is recorded
- Cardinality: one record per occurrence
timed moved the system clock at once by an arbitrary amount, rather than adjusting its rate. Every timestamp written after this record is on the new clock, including the times of every other event in the store, so this is the record that explains a jump in them.
timed slews small errors by adjusting the clock's rate after each poll, and those corrections are not recorded. Only a step is.
13.1.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
operation.name | str.enum | required | Why timed stepped the clock. automatic is its own decision: its sources agreed the clock was further off than it is willing to slew. manual is a request on its control socket (clock set), which timed accepts only while Automatic is 0. boot-floor is the clock raised to the build's timestamp at startup, because it read earlier than that; the real time is not yet known when this is written.Values here (closed set): automatic · manual · boot-floor. |
clock.time | uint.time | required | Never before the build's timestamp: timed refuses a step that would land there. |
clock.time-previous | uint.time | optional | The wall clock's reading just before timed moved it, the counterpart of clock.time. |
subject.token.sid | bin.sid | required | The principal that acted. On a manual step, the user of the token that asked for the clock to be set; timed refuses a request whose user it cannot read. On an automatic step and the boot-floor, which timed decides on its own authority, timed's own user: its service SID. Search operation.name, not this field, to tell timed's own steps from a person's. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from timed.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
14.1 netd.hostname.changed
Peios / Using Peios / Events / Network Configuration Events
- Event type:
netd.hostname.changed - Defined in:
netd.evman - Tier: essential
- Gating: none — every change netd makes to the kernel's hostname is recorded
- Cardinality: one record per occurrence
netd changed the machine's hostname. The name is what other machines, log lines and certificate requests know the machine by, so a change of it is always recorded.
netd takes the name from Machine\System\Network Hostname, or else from a
lease that offers one where the interface's profile accepts it, and
passes it to the kernel. A call that leaves the kernel's name as it was,
as when netd restarts on a machine that already has the name, records
nothing. A name the kernel refuses is not a change and is not recorded.
14.1.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
config.name | str | required | Always Hostname, the setting the name is held in, whether the value in force came from the registry or from a lease. |
config.text | str | required | The name now in force, as netd passed it to the kernel. |
config.text-previous | str | optional | The kernel's name just before the change, as the kernel held it. On the first change after boot this is the kernel's own (none). Absent when it could not be read. |
subject.token.sid | bin.sid | required | The principal that acted. When the change was made during the pass a reconcile request on netd's control socket asked for, the user of the requester's token; netd refuses a request whose user it cannot read. Otherwise netd changed the name on its own authority, after the registry value changed, when a lease arrived or at startup, and this is netd's own user, SYSTEM. Who wrote the registry value is LCS's to record, where the key's SACL audits the write. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from netd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
15.1 trustd.root.added
Peios / Using Peios / Events / Trust Events
- Event type:
trustd.root.added - Defined in:
trustd.evman - Tier: standard
- Gating: none — every root that enters the set in force is recorded
- Cardinality: one per root that entered the set
A root certificate authority entered the set this machine trusts. trustd recomposes the set whenever the registry's additions or distrusts change, or the shipped bundle is replaced, and writes one record for each root that is in the new set and was not in the one before: an addition, a distrust lifted, or a root a bundle upgrade brought in.
The set trustd composes when it starts is the baseline these records count from, and is not recorded. A change made while trustd was not running is seen only as part of that baseline.
No subject. PGSS §6.4 asks an event about an action for the principal that
acted, and the action here is not trustd's: trustd only recomposes the set
from what others decided, and it is never told who they were. The
principal that acted is whoever wrote the addition or the distrust, which
the registry records (LCS, where the key is audited), or whoever upgraded
the bundle, which peipkg records. Naming trustd's own SID instead would
answer "who distrusted this root?" with trustd, which is wrong. The writer
is on the registry's record of the write under
Machine\System\Trust\Certificates, just before this one.
15.1.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.certificate.digest | bin | required | The SHA-256 digest of the certificate's DER encoding: its fingerprint, the name a distrust gives it and the one trust list prints, carried as 32 bytes rather than as hexadecimal. |
object.certificate.name | str | required | The certificate's subject distinguished name, in the form trust show prints it, such as CN=DigiCert TLS ECC P384 Root G5,O=DigiCert\, Inc.,C=US. |
object.certificate.purposes | str.enum[] | required | The purposes the root is trusted for now. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from trustd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
15.2 trustd.root.distrusted
Peios / Using Peios / Events / Trust Events
- Event type:
trustd.root.distrusted - Defined in:
trustd.evman - Tier: standard
- Gating: none — every root a distrust takes out of the set in force is recorded
- Cardinality: one per root that left the set
A distrust took a root certificate authority out of the set this machine trusts. A distrust applies to shipped roots and to additions alike. A distrust naming a certificate the machine does not have takes nothing out and writes no record; if that certificate arrives later it is refused, and never enters the set to leave it.
No subject, as for trustd.root.added.
15.2.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.certificate.digest | bin | required | The SHA-256 digest of the certificate's DER encoding: its fingerprint, the name a distrust gives it and the one trust list prints, carried as 32 bytes rather than as hexadecimal. |
object.certificate.name | str | required | The certificate's subject distinguished name, in the form trust show prints it, such as CN=DigiCert TLS ECC P384 Root G5,O=DigiCert\, Inc.,C=US. |
object.certificate.purposes | str.enum[] | required | The purposes the root was trusted for until it was distrusted. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from trustd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
15.3 trustd.root.removed
Peios / Using Peios / Events / Trust Events
- Event type:
trustd.root.removed - Defined in:
trustd.evman - Tier: standard
- Gating: none — every root that leaves the set in force, other than by distrust, is recorded
- Cardinality: one per root that left the set
A root certificate authority left the set this machine trusts, and no
distrust names it: its addition was withdrawn, or a bundle upgrade dropped
it. A root that leaves because it was distrusted is
trustd.root.distrusted instead.
No subject, as for trustd.root.added.
15.3.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.certificate.digest | bin | required | The SHA-256 digest of the certificate's DER encoding: its fingerprint, the name a distrust gives it and the one trust list prints, carried as 32 bytes rather than as hexadecimal. |
object.certificate.name | str | required | The certificate's subject distinguished name, in the form trust show prints it, such as CN=DigiCert TLS ECC P384 Root G5,O=DigiCert\, Inc.,C=US. |
object.certificate.purposes | str.enum[] | required | The purposes the root was trusted for until it left. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from trustd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
16.1 gxwid.certificate.renewed
Peios / Using Peios / Events / Desktop Server Events
- Event type:
gxwid.certificate.renewed - Defined in:
gxwid.evman - Tier: essential
- Gating: none — every renewal of the machine's GXWI certificate is recorded
- Cardinality: once per renewal, which is about once a year
gxwid made the machine a new certificate for its GXWI key and serves it from now on, in place of the one before. A certificate lasts a year and is renewed once fewer than 30 days of it remain, which gxwid looks for as it starts, once a day, and when it is reloaded; one made by an older gxwid that lasts longer, or one not valid yet, is renewed at the first look.
The key is the same, so the machine's identity is: what changes is the
certificate's fingerprint, which a browser that trusted the old
certificate does not know. Everyone who reaches the desktop is shown the
browser's warning once more, and anyone who imported the old certificate
imports the new one, /var/state/gxwi/certificate.pem. This record says
which fingerprint they should now see.
Essential because it changes what everyone who connects is shown, and is rare. The new certificate is served to every connection made after it; those already made, and everyone signed in, carry on.
No subject: gxwid renews the certificate of its own accord, on time.
16.1.1 Fields #
| Field | Type | Presence | Meaning |
|---|---|---|---|
object.certificate.digest | bin | required | The new certificate's SHA-256 digest. |
object.certificate.digest-previous | bin | required | The SHA-256 digest of the DER encoding of the certificate this one replaced: its fingerprint, as 32 bytes, in the form of object.certificate.digest. |
object.certificate.name | str | required | The new certificate's subject, CN= the machine's name when it was made and O=Peios. |
Every record also carries the header fields of the envelope, which no payload repeats.
Generated from gxwid.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
17.1 Watch Records
Peios / Using Peios / Events / Registry Watch Records
Watch records are not KMES events. They are binary records read from
a key file descriptor with read(), and they exist to tell a watcher
that something under a key changed.
They are documented here because an operator asking what the registry reports would otherwise miss them, but nothing else in this book applies to them: no msgpack, no envelope, no identity stamps, no audit meaning.
17.1.1 Reading #
A single read() returns as many complete records as fit in the
caller's buffer. A record is never split across two calls.
If the buffer cannot hold even the first queued record, read() fails
with EINVAL — the buffer is too small to make progress, and the caller
retries with a larger one. On an armed fd with an empty queue, read()
blocks, or returns EAGAIN under O_NONBLOCK.
Only records copied out in full are dequeued.
17.1.2 Layout #
Every record begins with the same four fields.
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | total_len |
| 4 | 2 | event_type |
| 6 | 2 | name_len |
| 8 | name_len | name, UTF-8 |
All integers are little-endian. total_len is the whole record
including this header, it is how a consumer advances to the next record,
and it is the only safe way to do so.
17.1.3 Subtree watches carry a path #
A subtree watch's records carry additional fields after the name, locating the key the change happened on relative to the watched key.
| Size | Field |
|---|---|
| 2 | path_depth |
2 + n each | path_components: a u16 length, then that many UTF-8 bytes |
path_depth is the number of components from the watched key down to
the changed key. Zero means the change was on the watched key itself.
The components are length-prefixed rather than joined by a separator because registry names can contain any Unicode character and value names can contain backslashes. A concatenated path string would be ambiguous.
OVERFLOW records are emitted in the bare eight-byte form, with no path
even on a subtree watch — there is no single key to name when the queue
itself overflowed.
The header offsets are ABI, named in uapi/pkm/lcs.h and listed in the
Peios Kernel TRM §5.A.
Appendix A All Event Types
Peios / Using Peios / Events
Every event type in the evman catalogue, generated from the fragments by pkm/tools/gen-events-book.py: 101 event types and 594 fields, from kacs.evman, kernel.evman, kmes.evman, lcs.evman, ntfe.evman, stratafs.evman, peinit.evman, peipkg.evman, eventd.evman, authd.evman, lpsd.evman, timed.evman, netd.evman, trustd.evman, gxwid.evman.
The tier sets whether an event type is recorded by default (PGSS §6.8). Each type links to its page; each page's fields link to the field index.
A.1 Access and Identity Events #
| Event type | Tier | Fragment | Summary |
|---|---|---|---|
kacs.audit.access.checked | essential | kacs.evman | The record that an access check completed, and what it decided. |
kacs.audit.descriptor.changed | essential | kacs.evman | The record that a caller changed, or was authorised to change and failed to change, the security descriptor of a file, a token, a process or a System V IPC object. |
kacs.audit.handle.used | essential | kacs.evman | The record of what was done with a handle after it was opened. |
kacs.audit.privilege.used | essential | kacs.evman | The record that a privilege was spent. |
kacs.caap.policy.changed | essential | kacs.evman | The record that a central access policy was installed, replaced or removed. |
kacs.caap.sacl.skipped | standard | kacs.evman | The record that a central access policy rule's SACL could not be parsed or evaluated, so the audit it asked for was skipped. |
kacs.caap.staging.diverged | standard | kacs.evman | The record that a staged central access policy would have decided an access differently from the policy in force. |
kacs.config.value.rejected | essential | kacs.evman | The record that KACS read one of its registry-held tables, rejected it, and carried on with what it had. |
kacs.descriptor.rejected | standard | kacs.evman | The record that KACS read an object's stored security descriptor, found it corrupt, and refused to use it. |
kacs.impersonation.reverted | verbose | kacs.evman | The record that a thread stopped impersonating and acts as its own token again. |
kacs.impersonation.started | standard | kacs.evman | The record that a thread took on, or tried to take on, a client's identity by impersonating its token. |
kacs.mount.policy.changed | essential | kacs.evman | The record that a filesystem's KACS mount policy was set: what happens to an object on it that has no stored descriptor. |
kacs.session.destroyed | essential | kacs.evman | The record that a logon session ended because its last token went away. |
kacs.signature.crypto.failed | essential | kacs.evman | The record that the machinery for verifying signed executables cannot run. |
A.2 Filesystem Events #
| Event type | Tier | Fragment | Summary |
|---|---|---|---|
stratafs.file.copied-up | standard | stratafs.evman | The record that StrataFS materialised an object into a writable stratum, successful or not. |
stratafs.mutation.refused | standard | stratafs.evman | The record that a mutation was refused because of how the mount is arranged, rather than because of an access check. |
A.3 Registry Events #
| Event type | Tier | Fragment | Summary |
|---|---|---|---|
lcs.audit.backup.ended | essential | lcs.evman | The record that a registry subtree backup finished, successfully or otherwise. |
lcs.audit.backup.started | essential | lcs.evman | The record that a registry subtree backup began, emitted before any data is read. |
lcs.audit.key.created | essential | lcs.evman | The record that a registry key was created. |
lcs.audit.key.deleted | essential | lcs.evman | The record that a registry key was deleted from a layer through its own handle, or that an attempt failed. |
lcs.audit.key.descriptor.changed | essential | lcs.evman | The record that a registry key's security descriptor was changed through a key handle, or that an attempt failed. |
lcs.audit.key.hidden | essential | lcs.evman | The record that a registry key was hidden in a layer through its own handle, or that an attempt failed. |
lcs.audit.key.opened | essential | lcs.evman | The record that a registry key was opened and what access the open received. |
lcs.audit.key.tombstoned | essential | lcs.evman | The record that a key's blanket tombstone was set or cleared in a layer, or that an attempt failed. |
lcs.audit.restore.ended | essential | lcs.evman | The record that a registry subtree restore finished, successfully or otherwise. |
lcs.audit.restore.started | essential | lcs.evman | The record that a registry subtree restore began, emitted before any state is modified. |
lcs.audit.transaction.committed | essential | lcs.evman | The record of how a registry transaction ended, written for a transaction that staged at least one write some other record covers. |
lcs.audit.value.deleted | essential | lcs.evman | The record that a registry value was deleted through a key handle, or that an attempt to delete one failed. |
lcs.audit.value.set | essential | lcs.evman | The record that a registry value was written through a key handle, or that an attempt to write one failed. |
lcs.config.value.rejected | essential | lcs.evman | The record that LCS read one of its own configuration values, rejected it, and carried on with what it had. |
lcs.source.response.rejected | essential | lcs.evman | The record that LCS rejected data a registry source sent it. |
A.4 Event Stream Events #
| Event type | Tier | Fragment | Summary |
|---|---|---|---|
kmes.buffer.swap.failed | standard | kmes.evman | The record that KMES could not resize its per-CPU ring buffers and kept the capacity it had. |
kmes.config.applied | standard | kmes.evman | The record that KMES read its configuration key and committed what it found. |
kmes.config.refresh.failed | essential | kmes.evman | The record that KMES could not read one of its configuration keys and kept running on what it had. |
kmes.config.value.rejected | essential | kmes.evman | The record that KMES read a configuration value, rejected it, and carried on with what it had. |
A.5 Network Policy Events #
| Event type | Tier | Fragment | Summary |
|---|---|---|---|
ntfe.policy.published | standard | ntfe.evman | The record that a new generation of traffic rules went into force. |
ntfe.policy.rejected | essential | ntfe.evman | The record that the kernel read a traffic policy and refused it, so the policy an administrator wrote is not the policy in force. |
ntfe.verdict.reported | essential | ntfe.evman | The record of a traffic policy decision that a rule explicitly asked to be reported. |
A.6 Service Events #
| Event type | Tier | Fragment | Summary |
|---|---|---|---|
peinit.boot.downgraded | essential | peinit.evman | A full boot was downgraded to safe mode: the machine is running a reduced service set. |
peinit.cgroup.leaked | standard | peinit.evman | A cgroup could not be reclaimed: its processes do not respond to the kernel, and the cgroup is leaked. |
peinit.config.reload.applied | standard | peinit.evman | A configuration reload ran: from now on the registry as it then stood is the running configuration, or, if it failed, nothing changed. |
peinit.config.reload.deferred | verbose | peinit.evman | A configuration reload during the boot window changed definitions of services the boot plan had not yet launched. |
peinit.critical-service.failed | essential | peinit.evman | A service marked Critical failed, and peinit took the machine to its reboot final action. |
peinit.event.dropped | essential | peinit.evman | The ring refused one of peinit's events, and it is gone. |
peinit.fd-store.rejected | standard | peinit.evman | peinit refused a file descriptor a service asked it to keep. |
peinit.graph.operation.ended | verbose | peinit.evman | An operation that is a member of a graph execution context reached its terminal outcome as the graph sees it: satisfied, so that what depends on it may proceed, or failed. |
peinit.graph.validation.failed | standard | peinit.evman | The service graph failed validation for the reason in outcome.reason. |
peinit.graph.validation.warned | standard | peinit.evman | The service graph passed validation with a warning: nothing was refused, but something will not behave as its author probably expects. |
peinit.internal-error.contained | standard | peinit.evman | peinit could not carry out a step on a service's behalf, and contained the fault to that service. |
peinit.job.created | verbose | peinit.evman | A job exists: peinit has decided to run a process and recorded it, before the process exists. |
peinit.job.ended | standard | peinit.evman | A job is over: its process exited or was killed, or the job ended without its process ever starting. |
peinit.job.output.dropped | standard | peinit.evman | A submitted job's submitter stopped draining its output, and peinit began dropping the submitter's copy of the job's lines. |
peinit.job.started | standard | peinit.evman | A job's process exists and has been executed: the moment a process is running as object.job.token.sid on peinit's behalf. |
peinit.job.status.reported | verbose | peinit.evman | A submitted job reported STATUS= or PROGRESS= on the notification channel, and this is what peinit retained after it. |
peinit.notify.errno.reported | standard | peinit.evman | A service's job sent ERRNO=: its own claim about an error, which peinit neither acts on nor retains. |
peinit.notify.exit-status.reported | standard | peinit.evman | A service's job sent EXIT_STATUS=, informationally; it is not the exit of any process. |
peinit.notify.progress.reported | verbose | peinit.evman | A service's job sent PROGRESS= or PROGRESS_UNIT=, and this is the progress peinit retained after it. |
peinit.notify.rejected | standard | peinit.evman | peinit refused a datagram on the notification channel and applied none of it: the sender could not be authenticated, the message did not parse, or it made no sense for the job's state. |
peinit.notify.status.reported | verbose | peinit.evman | A service's job sent STATUS= on the notification channel. |
peinit.notify.stopping.reported | standard | peinit.evman | A service's job sent STOPPING=1, so peinit will not send it SIGTERM. |
peinit.on-failure.suppressed | standard | peinit.evman | peinit declined to start an OnFailure handler, because starting it would have looped or gone too deep, so the failure it was meant to handle went unhandled. |
peinit.operation.ended | standard | peinit.evman | An operation reached a terminal state other than merged: it completed, failed, was cancelled while still pending, or was aborted while running. |
peinit.operation.merged | verbose | peinit.evman | An operation was absorbed into another before it ran, and has no outcome of its own: follow object.operation.merged-into.guid to the operation whose peinit.operation.ended decides it. |
peinit.operation.requested | standard | peinit.evman | An operation on a service — a start, stop, restart, reload or reset — was created. |
peinit.operation.started | verbose | peinit.evman | An operation left the queue and began to run. |
peinit.recovery.entered | essential | peinit.evman | peinit dropped the machine to a recovery shell, for the reason in outcome.reason. |
peinit.service.abandoned | standard | peinit.evman | A service's processes survived SIGKILL and its post-kill timeout, so peinit gave up on them and moved the service to abandoned: its cgroup is still populated, unkillably. |
peinit.service.reload.timed-out | standard | peinit.evman | A service announced RELOADING=1 and never confirmed the reload by its deadline: it wedged mid-reload, or lost its handler. |
A.7 Package Events #
| Event type | Tier | Fragment | Summary |
|---|---|---|---|
peipkg.action.authorised | essential | peipkg.evman | The record that the operator was asked to authorise an elevated action specifically, apart from the routine proceed prompt, and what they answered. |
peipkg.claim.changed | standard | peipkg.evman | The record that peipkg granted a role to a package or revoked it, or tried to and failed. |
peipkg.package.installed | essential | peipkg.evman | The record that peipkg installed a package, or tried to and failed. |
peipkg.package.uninstalled | essential | peipkg.evman | The record that peipkg removed a package, or tried to and failed. |
peipkg.package.upgraded | essential | peipkg.evman | The record that peipkg moved an installed package to another version, or tried to and failed. |
peipkg.repository.added | essential | peipkg.evman | The record that peipkg added a repository and established trust in it, or tried to and failed. |
peipkg.repository.reconfigured | essential | peipkg.evman | The record that a repository add changed one of an existing repository's trust-relevant settings. |
peipkg.repository.refreshed | standard | peipkg.evman | The record that peipkg refreshed the metadata of its configured repositories. |
peipkg.repository.removed | essential | peipkg.evman | The record that peipkg removed a repository from its configuration and database, or tried to and failed. |
peipkg.transaction.recovered | standard | peipkg.evman | The record that peipkg recover reconciled interrupted transactions, rolling back each one it found pending, or failed to. |
A.8 Event Store Events #
| Event type | Tier | Fragment | Summary |
|---|---|---|---|
eventd.config.changed | essential | eventd.evman | The record that eventd applied a change to one of its configuration values at runtime. |
eventd.daemon.started | essential | eventd.evman | The record that eventd started, attached to KMES and decided where to resume reading each CPU's ring. |
eventd.daemon.stopped | essential | eventd.evman | The record that eventd shut down gracefully, written after it stopped reading the rings and committed everything it had read. |
eventd.events.lost | essential | eventd.evman | The record that eventd found events missing from a CPU's ring: sequence numbers it never read and never stored. |
eventd.store.quarantined | essential | eventd.evman | The record that eventd found one of its stores corrupt, moved it aside and started a new one in its place. |
A.9 Authentication Events #
| Event type | Tier | Fragment | Summary |
|---|---|---|---|
authd.logon.attempted | essential | authd.evman | The record that something asked authd to sign a principal in, and whether it did. |
authd.service.attested | standard | authd.evman | The record that the service manager asked authd for a service's token, with no credential behind it, and whether authd minted one. |
authd.session.ended | essential | authd.evman | The record that authd ended a logon session at someone's request, by ending every process whose primary token belongs to it (PGSS Logon §2.22). |
A.10 Local Principal Events #
| Event type | Tier | Fragment | Summary |
|---|---|---|---|
lpsd.account.created | essential | lpsd.evman | The record that an administrator created a local account with lps, or tried to. |
lpsd.account.deleted | essential | lpsd.evman | The record that an administrator deleted a local account with lps, or tried to. |
lpsd.account.modified | essential | lpsd.evman | The record that an account was changed: by an administrator with lps, or by the principal themselves where the self socket allows it. |
lpsd.credential.changed | essential | lpsd.evman | The record that a principal changed their own password, or added or removed one of their own SSH keys, through authd (PSPU §2.21, §2.23), or tried to and was refused. |
lpsd.credential.verified | standard | lpsd.evman | The record that lpsd checked a credential for a logon authd relayed to it, and what it found. |
lpsd.group.created | essential | lpsd.evman | The record that an administrator created a local group with lps, or tried to. |
lpsd.group.deleted | essential | lpsd.evman | The record that an administrator deleted a local group with lps, or tried to. |
lpsd.group.member.added | essential | lpsd.evman | The record that an administrator added an account to a group with lps, or tried to. |
lpsd.group.member.removed | essential | lpsd.evman | The record that an administrator removed an account from a group with lps, or tried to. |
A.11 Time Events #
| Event type | Tier | Fragment | Summary |
|---|---|---|---|
timed.clock.stepped | essential | timed.evman | timed moved the system clock at once by an arbitrary amount, rather than adjusting its rate. |
A.12 Network Configuration Events #
| Event type | Tier | Fragment | Summary |
|---|---|---|---|
netd.hostname.changed | essential | netd.evman | netd changed the machine's hostname. |
A.13 Trust Events #
| Event type | Tier | Fragment | Summary |
|---|---|---|---|
trustd.root.added | standard | trustd.evman | A root certificate authority entered the set this machine trusts. |
trustd.root.distrusted | standard | trustd.evman | A distrust took a root certificate authority out of the set this machine trusts. |
trustd.root.removed | standard | trustd.evman | A root certificate authority left the set this machine trusts, and no distrust names it: its addition was withdrawn, or a bundle upgrade dropped it. |
A.14 Desktop Server Events #
| Event type | Tier | Fragment | Summary |
|---|---|---|---|
gxwid.certificate.renewed | essential | gxwid.evman | gxwid made the machine a new certificate for its GXWI key and serves it from now on, in place of the one before. |
A.15 Not in the catalogue #
The registry's watch records are not events, so no fragment defines them and this table does not list them.
Generated from the evman catalogue by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.1 access.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under access, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.1.1 access.audit-mask #
- Type:
uint.mask - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The full continuous-audit mask cached on a handle, of which
access.matched is the part this operation touched. Present so an operator
can see what else on that handle would fire, which cannot be derived from
the intersection alone.
Carried by:
kacs.audit.descriptor.changedkacs.audit.handle.usedlcs.audit.key.deletedlcs.audit.key.descriptor.changedlcs.audit.key.hiddenlcs.audit.key.tombstonedlcs.audit.value.deletedlcs.audit.value.set
B.1.2 access.bypass #
- Type:
str.enum - Values:
priv·si-fromkernel - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Why a signal was delivered with no access check at all. priv is a signal
the kernel sent as itself (SEND_SIG_PRIV); si-fromkernel is one whose
si_code marks it as kernel-originated. Either is a total bypass of the
process-access model, keyed off the signal's origin, so a spoofed origin
would be silent; absent when the signal was checked.
Carried by:
No event carries this field yet.
B.1.3 access.decided #
- Type:
uint.mask - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The bits the DACL evaluation reached a conclusion on, granted or denied
by an ACE. A requested bit outside this set was never mentioned by any ACE
and was denied because nothing granted it — a different finding from an
explicit deny, and one access.granted alone cannot show.
Carried by:
No event carries this field yet.
B.1.4 access.default-descriptor #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether an authorisation decision used the compiled-in default security descriptor rather than one an administrator configured. When true, the policy that decided was built into the kernel and is not visible anywhere in the registry. The case that produces it today is a write to the base layer when the base layer has no metadata row of its own.
Carried by:
No event carries this field yet.
B.1.5 access.denied-integrity #
- Type:
uint.mask - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The bits mandatory integrity control denied: the rights the object's
mandatory label withholds from a caller of lower integrity, whatever its
DACL says. On an access-check record these are the requested bits, or every
bit for a MAXIMUM_ALLOWED request, and the field is absent when there were
none.
Carried by:
B.1.6 access.denied-trust #
- Type:
uint.mask - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The bits the process-trust label denied: the rights the object's trust
label withholds from a caller whose PIP does not dominate it. Read as
access.denied-integrity is, and can be non-zero in the same evaluation.
Carried by:
B.1.7 access.disallowed #
- Type:
uint.mask - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The requested bits that fall outside the set the operation permits at all, whatever the descriptor says. Such a request is refused before any access check.
Carried by:
No event carries this field yet.
B.1.8 access.granted #
- Type:
uint.mask - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The access mask in force. What that means narrows per event — the rights this check granted, or the rights a handle was opened with — so read the event's own note.
Carried by:
kacs.audit.access.checkedkacs.audit.descriptor.changedkacs.audit.handle.usedkacs.audit.privilege.usedkacs.caap.sacl.skippedkacs.caap.staging.divergedlcs.audit.key.createdlcs.audit.key.deletedlcs.audit.key.descriptor.changedlcs.audit.key.hiddenlcs.audit.key.openedlcs.audit.key.tombstonedlcs.audit.value.deletedlcs.audit.value.set
B.1.9 access.granted-staged #
- Type:
uint.mask - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
What a staged central access policy, not yet in force, would have granted.
Carried beside access.granted, the grant the policy in force produced;
the two differing is the record of what a policy change will alter before
it is made.
Carried by:
B.1.10 access.matched #
- Type:
uint.mask - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The subset of the requested access that overlapped an audit mask and therefore caused the record to exist. The bits that triggered the audit, not the bits that were used.
Carried by:
kacs.audit.descriptor.changedkacs.audit.handle.usedlcs.audit.key.deletedlcs.audit.key.descriptor.changedlcs.audit.key.hiddenlcs.audit.key.tombstonedlcs.audit.value.deletedlcs.audit.value.set
B.1.11 access.maximum-allowed #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the request was made in MAXIMUM_ALLOWED mode, asking for
whatever the descriptor would grant rather than for named rights. Changes
how SACL audit ACEs match, and such a request cannot fail, so it is needed
to read both access.granted and outcome.success.
Carried by:
No event carries this field yet.
B.1.12 access.missing #
- Type:
uint.mask - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The rights an operation needed that the handle did not have. Carried on a
refusal of an operation on an already-open handle: the handle's
access.granted, and what it lacked.
Carried by:
No event carries this field yet.
B.1.13 access.privileged #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the check was satisfied by a privilege rather than by the
descriptor. True on a descriptor written with SeRestorePrivilege over a
missing or corrupt one, which is a complete bypass of the object's access
control and not a decision the descriptor made.
Carried by:
No event carries this field yet.
B.1.14 access.ptrace-mode #
- Type:
uint.flags - Values:
0x1 PTRACE_MODE_READ·0x2 PTRACE_MODE_ATTACH·0x4 PTRACE_MODE_NOAUDIT·0x8 PTRACE_MODE_FSCREDS·0x10 PTRACE_MODE_REALCREDS·0x20 PTRACE_MODE_GETFD·0x40 PTRACE_MODE_PIDFD_OPEN·0x80 PTRACE_MODE_PROC_QUERY_LIMITED·0x100 PTRACE_MODE_PROC_QUERY_INFORMATION - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The ptrace access mode of a cross-process check. The last four bits are
Peios additions that let KACS tell a pidfd open, a descriptor theft and a
/proc read apart from a generic ptrace check. The mode decides the
process right required: PTRACE_MODE_GETFD, taking another process's file
descriptor, needs PROCESS_DUP_HANDLE.
Carried by:
No event carries this field yet.
B.1.15 access.requested #
- Type:
uint.mask - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The access mask the caller asked for, after generic bits have been mapped to type-specific ones. Generic bits never survive into an event.
Carried by:
kacs.audit.access.checkedkacs.audit.descriptor.changedkacs.audit.handle.usedkacs.audit.privilege.usedkacs.caap.sacl.skippedkacs.caap.staging.divergedlcs.audit.key.createdlcs.audit.key.deletedlcs.audit.key.descriptor.changedlcs.audit.key.hiddenlcs.audit.key.openedlcs.audit.key.tombstonedlcs.audit.value.deletedlcs.audit.value.set
B.1.16 access.requested-minimum #
- Type:
uint.mask - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The smallest access a POSIX open genuinely needed, before the kernel
widened the request for compatibility. Carried beside access.requested:
the difference is access the caller was given without asking for it.
Carried by:
No event carries this field yet.
B.1.17 access.self #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the operation targeted the caller itself. Several cross-process
checks exempt a process acting on itself structurally — raise() must work
even when a restricted token would fail its own process's descriptor — so
true means no descriptor was consulted.
Carried by:
No event carries this field yet.
B.1.18 access.vfs-mask #
- Type:
uint.flags - Values:
0x1 MAY_EXEC·0x2 MAY_WRITE·0x4 MAY_READ·0x8 MAY_APPEND·0x10 MAY_ACCESS·0x20 MAY_OPEN·0x40 MAY_CHDIR·0x80 MAY_NOT_BLOCK - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The VFS permission mask the kernel passed to the check. A different
vocabulary from access.requested, which is the KACS rights the mask was
mapped to; a record carrying both shows the mapping.
Carried by:
No event carries this field yet.
Generated from kernel.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.2 buffer.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under buffer, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.2.1 buffer.capacity #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The per-CPU ring buffer capacity in force. After a failed swap, the capacity that was kept.
Carried by:
B.2.2 buffer.capacity-expected #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The ring capacity a record expected to find, where the ring it found has
another. Every per-CPU ring is meant to share one capacity, because KMES
sizes an incoming event against one ring's capacity before writing it to
another. A ring whose buffer.capacity differs from this has broken that
check: an event judged to fit may not fit the ring it lands in.
Carried by:
No event carries this field yet.
B.2.3 buffer.capacity-previous #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The per-CPU ring capacity before a capacity swap: what buffer.capacity
was until the swap committed. A swap to a smaller capacity copies the
surviving events into the new rings and skips any that no longer fit, so a
record where this exceeds buffer.capacity is one where the swap itself
can have lost events.
Carried by:
No event carries this field yet.
B.2.4 buffer.capacity-requested #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The per-CPU ring buffer capacity that was asked for.
Carried by:
B.2.5 buffer.cpu #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The logical CPU whose ring buffer a record concerns. KMES keeps one ring
per CPU and writes each event to the ring of the CPU it was emitted on, so
this is the numbering event.cpu uses on records read from that ring. It
identifies a ring, not a security subject, and it is the only thing that
does.
Carried by:
B.2.6 buffer.cpu-claimed #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The CPU a ring's own metadata says it belongs to, carried when that
disagrees with buffer.cpu, the CPU it is installed for. A disagreement
means the ring topology is internally inconsistent, and KMES drops the
event rather than trust the ring. It consumes no sequence number and
counts no loss, so no gap appears in event.sequence, and a record
carrying this field is the only evidence that the event existed.
Carried by:
No event carries this field yet.
B.2.7 buffer.fill #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
How full a ring was, as the bytes of events it held: buffer.head minus
buffer.tail. Compare it with buffer.capacity for a proportion. A full
ring loses its oldest events to every new one, so a fill near capacity is
the warning that comes before loss rather than loss itself.
Carried by:
No event carries this field yet.
B.2.8 buffer.generation #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The generation of the ring in force. It starts at 1 and goes up by one with each capacity swap, which replaces every ring with a new one. The old ring is stamped with the new generation as it is retired, so a consumer still mapping it can tell that its view is stale; it is the only staleness signal a mapped ring has.
Carried by:
No event carries this field yet.
B.2.9 buffer.generation-previous #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The ring generation before a capacity swap, normally one less than
buffer.generation.
Carried by:
No event carries this field yet.
B.2.10 buffer.head #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The ring's write position, as a running byte count. It only increases and
never wraps: it is not an offset into the buffer, which is this value
modulo buffer.capacity. With buffer.tail it gives the ring's fill
directly.
Carried by:
No event carries this field yet.
B.2.11 buffer.slots #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The number of per-CPU ring slots the kernel exposes: one for every CPU
the system could have, numbered as buffer.cpu is. A consumer that means
to see every event attaches to every slot. A slot for a CPU that is not
present holds no ring.
Carried by:
No event carries this field yet.
B.2.12 buffer.tail #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The ring's tail position: where its oldest retained event begins, as a
running byte count in the same terms as buffer.head. It moves forward
when the ring is full and new events overwrite the oldest, so a tail that
has passed the point a consumer was reading from means that consumer lost
events.
Carried by:
No event carries this field yet.
B.2.13 buffer.threshold-direction #
- Type:
str.enum - Values:
rising·falling - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
Whether a ring's fill crossed a watermark on the way up or on the way
down. rising is the warning that a consumer is falling behind; falling
is its recovery. Without it, a record of recovery cannot be told from a
record of onset.
Carried by:
No event carries this field yet.
Generated from kmes.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.3 caap.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under caap, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.3.1 caap.phase #
- Type:
str.enum - Values:
effective-sacl·staged-sacl - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
Which phase of a central access policy's audit evaluation a CAAP
diagnostic describes. effective-sacl is a rule's SACL in the policy in
force, whose audit records are real. staged-sacl is the SACL of a rule's
staged, not-yet-effective version, evaluated alongside it only so that an
administrator can see what rolling it out would do; a failure there
changes nothing about the access or its audit.
Absent on a diagnostic about the whole check rather than one rule's SACL, such as a disagreement between the staged and effective results.
Carried by:
B.3.2 caap.policy.sid #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The SID identifying a central access policy. An object's SACL names the policies that apply to it by this SID, and the kernel's policy cache is keyed on it, so it ties a record to the policy an administrator installed. It identifies a policy, never a user or group, even though it has the form of a SID.
Carried by:
B.3.3 caap.rule.index #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The zero-based position of a rule within the central access policy named
by caap.policy.sid. Meaningful only against the policy as it stood when
the record was made: replacing a policy's specification replaces its rule
list, so an index in an older record can point at a different rule today.
Carried by:
B.3.4 caap.staged-differs #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
Whether evaluating the staged policies gave a different result from the effective ones for this access check. True when the staged grant differs from the effective grant, when the per-object results of an object-type check differ, or when the staged audit records differ from those actually produced.
A difference changes nothing about the access itself, which the effective policies decided. It is the signal that promoting the staged policy would change the outcome for this subject and object.
Carried by:
No event carries this field yet.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.4 clock.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under clock, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.4.1 clock.time #
- Type:
uint.time - Asserted: no
- Carried in: the payload
- Defined in:
timed.evman
The wall clock's reading just after timed moved it. Every record written after this one is stamped on the clock as it now reads, so this is the point from which their times count.
Read just after the change, not computed from it, so it includes the few microseconds the change itself took.
Carried by:
B.4.2 clock.time-previous #
- Type:
uint.time - Asserted: no
- Carried in: the payload
- Defined in:
timed.evman
The wall clock's reading just before timed moved it, the counterpart of
clock.time. The difference between the two is the size of the step, and
records stamped between clock.time-previous and clock.time on either
side of this one were not written in the order their times suggest.
Absent when the clock read before the Unix epoch, which a uint.time
cannot hold; that happens on a machine whose hardware clock has failed,
and it is exactly the case the boot floor exists for.
Carried by:
Generated from timed.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.5 config.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under config, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.5.1 config.coalesced #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether change notifications were coalesced before they were acted on. Network policy coalesces and the registry and KMES do not, which is why one registry edit produces different numbers of records from different subsystems.
Carried by:
No event carries this field yet.
B.5.2 config.counts.applied #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many settings in a configuration refresh were applied. One of four counters read together, which between them account for every setting the refresh saw.
Carried by:
B.5.3 config.counts.ignored-unknown #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many registry values were ignored because their names match no setting. A misspelt setting name lands here and nowhere else, so a non-zero count is usually an administrator's typo.
Carried by:
B.5.4 config.counts.retained-invalid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many settings kept their previous value because the stored value was rejected, of the wrong type or out of range.
Carried by:
B.5.5 config.counts.retained-missing #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many settings kept their previous value because they were absent from the registry.
Carried by:
B.5.6 config.expected.max #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The largest value accepted for this setting.
Carried by:
B.5.7 config.expected.min #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The smallest value accepted for this setting.
Carried by:
B.5.8 config.expected.type #
- Type:
uint.enum - Values:
0 REG_NONE·1 REG_SZ·2 REG_EXPAND_SZ·3 REG_BINARY·4 REG_DWORD·5 REG_DWORD_BIG_ENDIAN·6 REG_LINK·7 REG_MULTI_SZ·8 REG_RESOURCE_LIST·9 REG_FULL_RESOURCE_DESCRIPTOR·10 REG_RESOURCE_REQUIREMENTS_LIST·11 REG_QWORD - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The registry type the value is required to have.
Carried by:
B.5.9 config.key.path #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The registry key the reported value lives under, in canonical form.
Carried by:
eventd.config.changedkacs.config.value.rejectedkmes.config.appliedkmes.config.refresh.failedkmes.config.value.rejectedlcs.config.value.rejected
B.5.10 config.limit #
- Type:
str.enum - Values:
request-timeout-ms·transaction-timeout-ms·notification-queue-size·symlink-depth-limit·max-value-size·max-key-depth·max-path-component-length·max-total-path-length·max-layers-per-value·max-bound-transactions-per-source·max-read-only-transactions-per-source·max-total-layers·max-registered-sources·max-hives-per-source·max-concurrent-rsi-requests·max-scope-guids-per-token·max-private-layers-per-token·max-subtree-watch-depth·max-transaction-watch-event-burst - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The kernel's internal identifier of a runtime limit, from the LCS_LIM_*
codes with the prefix removed and folded to lower case. A different
vocabulary from config.name: that is the name an administrator wrote in
the registry, and can be anything; this is the limit the kernel validated,
and a record can carry one without the other.
Carried by:
No event carries this field yet.
B.5.11 config.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The name of the configuration value being reported on, within
config.key.path.
Carried by:
eventd.config.changedkacs.config.value.rejectedkmes.config.value.rejectedlcs.config.value.rejectednetd.hostname.changedpeipkg.repository.reconfigured
B.5.12 config.received.kind #
- Type:
str.enum - Values:
missing·wrong-type·out-of-range - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How the stored value failed validation. Three different administrative mistakes with three different fixes: a value that was absent, one of the wrong type, and one that was well-typed but outside the permitted range.
KMES and LCS reached this vocabulary independently and disagreed —
u32_out_of_range and u64_out_of_range against dword_out_of_range for
the same condition. The width was never information the reader needed,
since config.expected.type already carries it, so the three collapse to
one.
Carried by:
B.5.13 config.received.text #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
A value supplied as text and rejected because it could not be parsed, as
the caller wrote it. Carried where there is no well-typed value for
config.received.value to hold.
Carried by:
No event carries this field yet.
B.5.14 config.received.type #
- Type:
uint.enum - Values:
0 REG_NONE·1 REG_SZ·2 REG_EXPAND_SZ·3 REG_BINARY·4 REG_DWORD·5 REG_DWORD_BIG_ENDIAN·6 REG_LINK·7 REG_MULTI_SZ·8 REG_RESOURCE_LIST·9 REG_FULL_RESOURCE_DESCRIPTOR·10 REG_RESOURCE_REQUIREMENTS_LIST·11 REG_QWORD - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The registry type the value actually had. Present only when the failure was the type itself.
Carried by:
B.5.15 config.received.value #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The value that was stored and rejected. Present only when the value was well-typed and out of range — there is nothing to report for a value that was missing or of the wrong type entirely.
Carried by:
B.5.16 config.root #
- Type:
str.enum - Values:
registry·kmes·layers·port-reservations·network - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Which kernel-read configuration root the record concerns:
Machine\System\Registry for the registry's own limits,
Machine\System\KMES, the layer metadata under
Machine\System\Registry\Layers, the port reservations under
Machine\System\Network\TcpIp\PortReservations, and the network policy
under Machine\System\Network. Which subsystem's key, not which setting
within it.
Carried by:
No event carries this field yet.
B.5.17 config.roots-present #
- Type:
str.enum[] - Values:
registry·kmes·layers·port-reservations·network - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The configuration roots found present when the registry was bootstrapped,
from the same values as config.root. A root missing from the list is
running on compiled-in defaults, and the kernel watches the whole machine
hive for it to appear rather than watching the root itself.
Carried by:
No event carries this field yet.
B.5.18 config.seed-file #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The shipped registry seed file that was applied, from /usr/share/regim/.
Seed files are inert until an image names them, so this records which
defaults a machine was actually given.
Carried by:
No event carries this field yet.
B.5.19 config.self-watch-mode #
- Type:
str.enum - Values:
disarmed·targeted·machine-root-fallback·mixed - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How precisely the kernel is watching its own configuration in the
registry. targeted means each kernel-read configuration key has its own
watch. machine-root-fallback means none of those keys could be watched,
so the kernel watches the whole Machine hive and re-reads everything on
any change beneath it. mixed means some keys are watched precisely and
the fallback covers the rest. disarmed means no watch is armed, so a
configuration change will not be noticed until the next bootstrap.
Anything other than targeted is a degraded mode, cheaper to tolerate
than to diagnose: configuration still loads, but from coarse rescans.
Carried by:
No event carries this field yet.
B.5.20 config.text #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
A setting's value in force, where the value is not an integer: a string,
or the textual rendering of a value of another registry type. The registry
type is config.type.
Carried by:
B.5.21 config.text-previous #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The text value a change replaced, beside config.text.
Carried by:
B.5.22 config.type #
- Type:
uint.enum - Values:
0 REG_NONE·1 REG_SZ·2 REG_EXPAND_SZ·3 REG_BINARY·4 REG_DWORD·5 REG_DWORD_BIG_ENDIAN·6 REG_LINK·7 REG_MULTI_SZ·8 REG_RESOURCE_LIST·9 REG_FULL_RESOURCE_DESCRIPTOR·10 REG_RESOURCE_REQUIREMENTS_LIST·11 REG_QWORD - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The registry type of the value now in force, as the number of its REG_*
constant. Absent when no value is in force. The same registry types, in the
same numbering, as config.expected.type.
Carried by:
B.5.23 config.type-previous #
- Type:
uint.enum - Values:
0 REG_NONE·1 REG_SZ·2 REG_EXPAND_SZ·3 REG_BINARY·4 REG_DWORD·5 REG_DWORD_BIG_ENDIAN·6 REG_LINK·7 REG_MULTI_SZ·8 REG_RESOURCE_LIST·9 REG_FULL_RESOURCE_DESCRIPTOR·10 REG_RESOURCE_REQUIREMENTS_LIST·11 REG_QWORD - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The registry type of the value a change replaced. Absent when there was no value before.
Carried by:
B.5.24 config.value #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The value in force after the event. On a rejection that is the last known-good value, not a corrected version of what was stored: configuration validation is reject-or-keep and never clamps.
The registry and this field can legitimately disagree. The registry shows what was written; this shows what the subsystem is actually using.
Carried by:
eventd.config.changedkmes.config.value.rejectedlcs.config.value.rejectedpeipkg.repository.reconfigured
B.5.25 config.value-previous #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The value that was in force before the change. With config.value, the
before and after of a setting that took effect.
Carried by:
Generated from kernel.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.6 counter.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under counter, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.6.1 counter.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The name of a counter stream, as the policy author wrote it in COUNT and
Counter.<name>. At most 63 bytes.
Carried by:
No event carries this field yet.
B.6.2 counter.partition #
- Type:
uint.flags - Values:
0x1 source-address·0x2 destination-address·0x4 interface - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
Which packet facts a counter view is partitioned by: one cell per distinct value of the facts set here. Zero is a single cell for the whole machine. A packet that lacks a partitioning fact, such as non-IP traffic in a view partitioned by address, counts nowhere.
Carried by:
No event carries this field yet.
B.6.3 counter.threshold #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The threshold a rule's counter condition compared counter.value against.
The two together are the whole content of a crossing.
Carried by:
No event carries this field yet.
B.6.4 counter.value #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The value held in a counter cell for the window a rule read. Its unit is
whatever the stream counts: emissions of COUNT, or bytes where the rule
counts Length, which is the length the stack saw and not the wire's.
Carried by:
No event carries this field yet.
B.6.5 counter.windows #
- Type:
uint.duration[] - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The sliding-window lengths a counter table answers, at most eight. The kernel holds them to the whole second and approximates each window with eight buckets of an eighth of its length, so a window's value is accurate only to that bucket.
Carried by:
No event carries this field yet.
Generated from ntfe.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.7 destination.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under destination, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.7.1 destination.address #
- Type:
str.ip - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The address the traffic was going to, rendered in the textual form for its family.
Carried by:
B.7.2 destination.attribution #
- Type:
str.enum - Values:
socket-stamp·no-kacs-state·unstamped·no-namespace·loopback-sender-lost·time-wait·stack-handled·no-sending-socket·multicast-or-broadcast·no-receiver·principal-unreadable - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
Why the destination endpoint was given the attribution it has. Not
emitted today: NTFE decides it on every flow and encodes it nowhere, so
destination.endpoint and destination.unattributed are all a record
carries. The values are those of source.attribution;
multicast-or-broadcast and no-receiver are the inbound paths that give
shared and none.
Carried by:
No event carries this field yet.
B.7.3 destination.endpoint #
- Type:
str.enum - Values:
program·kernel·shared·none - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
What stands at the destination end of the traffic, when that end is on this
machine. program is a process's socket, and the destination.socket.*
fields then say whose. kernel is the network stack itself. shared is
inbound multicast or broadcast, delivered to every socket bound to the
port and so attributable to none of them. none means nothing on the
machine will receive the traffic: a port nobody is listening on, which is
the signature of a port scan. Present only on flow-layer records, and absent
when the destination is a remote machine.
Carried by:
No event carries this field yet.
B.7.4 destination.file.parent.inode #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The inode number of the directory a rename moves an object into. Subject
to the same caveats as object.file.inode.
Carried by:
No event carries this field yet.
B.7.5 destination.file.path #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The path an object went to, in an operation on two objects such as a
rename or a link. Where it came from is source.file.path.
Carried by:
No event carries this field yet.
B.7.6 destination.mac #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The link-layer address the traffic was going to, written as six
colon-separated lowercase hexadecimal octets. Known only from a frame's
Ethernet header, so it is absent for outbound traffic judged at the IP
seats, where the frame has not been built yet, even when source.mac is
present.
Carried by:
No event carries this field yet.
B.7.7 destination.port #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The transport port the traffic was going to. Present only for protocols that have ports.
Carried by:
B.7.8 destination.socket.process.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The durable GUID of the process recorded on the destination endpoint's socket when it was stamped. Unlike the process ID it is never reused, so it still names the right process after that process has exited.
Carried by:
No event carries this field yet.
B.7.9 destination.socket.process.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The short name of the process recorded on the destination endpoint's socket when it was stamped. It is the kernel's command name, at most 15 characters and lossily sanitised, so two different programs can share it; it is a hint for a person, not an identity.
Carried by:
No event carries this field yet.
B.7.10 destination.socket.process.pid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The process ID recorded on the destination endpoint's socket when it was
stamped. A socket can outlive the process that created it, so this process
may have exited and its ID been reused; destination.socket.process.guid
is the identity that survives that.
Carried by:
No event carries this field yet.
B.7.11 destination.socket.token.service-sid #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The per-service SID of the token stamped on the destination endpoint's socket, when that token belongs to a service. It is found among the token's enabled groups and is always the 32-byte S-1-5-80 form. Absent for a socket that no service stands behind.
Carried by:
No event carries this field yet.
B.7.12 destination.socket.token.sid #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The user SID of the token KACS stamped on the destination endpoint's
socket. It is the identity the socket was stamped with, not necessarily the
identity of whoever reads from it now. Present only when
destination.endpoint is program.
Carried by:
No event carries this field yet.
B.7.13 destination.stratum.flags #
- Type:
uint.flags - Values:
0x1 create·0x2 read-only·0x4 absent-may - Asserted: no
- Carried in: the payload
- Defined in:
stratafs.evman
The configuration flags of the stratum an object was written into, as the
mount option set them. Decoded as source.stratum.flags is. A copy-up
always writes into the stratum flagged create, so on a copy-up record
this field says nothing new; it matters where an object was written in
place.
Carried by:
No event carries this field yet.
B.7.14 destination.stratum.index #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
stratafs.evman
Which stratum an object was written into, by index within the mount's stack.
Carried by:
B.7.15 destination.stratum.path #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
stratafs.evman
The filesystem path of the stratum an object was written into.
Carried by:
B.7.16 destination.token.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The durable GUID of a token the operation created, which did not exist before it: the result of a duplicate, a filter or a token creation.
Carried by:
No event carries this field yet.
B.7.17 destination.unattributed #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
Whether NTFE confessed that it could not attribute the destination endpoint
to a principal. When true, every rule condition on that endpoint's identity
was false for this traffic rather than evaluated. False does not prove
the endpoint was attributed: a program endpoint whose token could not
be read still reads false here.
Carried by:
No event carries this field yet.
Generated from kernel.evman, ntfe.evman, stratafs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.8 emission.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under emission, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.8.1 emission.batch-index #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The zero-based position, in a batch emit, of the entry that failed. A batch is emitted up to this entry and not from it: every earlier entry reached the ring, and this entry and every later one did not.
Carried by:
No event carries this field yet.
B.8.2 emission.buckets-clamped #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The number of per-process rate buckets a reconfiguration cut down.
Lowering emission.rate-limit clamps the remaining budget of every live
process to the new ceiling at once; this counts the processes whose budget
was above it.
Carried by:
No event carries this field yet.
B.8.3 emission.budget #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The number of events a process could still emit before its rate limit
stopped it: the tokens left in its bucket when the record was made. The
bucket refills at emission.rate-limit events a second and holds no more
than that, so a process's burst is one second's worth. On a throttled
emit this is below emission.requested, which is why the emit was
refused.
Carried by:
No event carries this field yet.
B.8.4 emission.emitted #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The number of events an emit operation actually wrote to the ring. Where
it falls short of emission.requested, the difference is events the
caller meant to record that are not in the stream.
Carried by:
No event carries this field yet.
B.8.5 emission.length #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The total size of one event as written to the ring: the fixed header, the event type and the payload together. It is the figure checked against the configured maximum event size, and against half the ring's capacity, the largest event a ring accepts.
Carried by:
No event carries this field yet.
B.8.6 emission.length-claimed #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
An event length read from a ring that failed KMES's sanity check: zero, larger than the ring, or larger than the bytes between the tail and the write position. It is not the size of any real event. It means the ring's framing is corrupt, and KMES recovered by discarding everything pending in that ring at once — a loss that is never added to the ring's dropped count and is bounded only by the ring's size.
Carried by:
No event carries this field yet.
B.8.7 emission.payload-length #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The byte length of an event's MessagePack payload, without the header or the event type.
Carried by:
B.8.8 emission.rate-limit #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The per-process emission ceiling KMES is running on, in events a second:
the MaxEmitRatePerProcess registry value, 10 000 by default and accepted
from 100 to 1 000 000. It applies to emission from userspace; kernel
emitters are not limited, and a process holding SeTcbPrivilege is exempt.
Carried by:
B.8.9 emission.rate-limit-previous #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The per-process emission ceiling before a reconfiguration, in events a
second. Lowering the ceiling is a way to thin out the event stream, so a
record where this exceeds emission.rate-limit deserves a look at who
changed it.
Carried by:
No event carries this field yet.
B.8.10 emission.requested #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The number of events an emit operation asked to write: 1 for a single emit, or the entry count of a batch, which is at most 256.
Carried by:
No event carries this field yet.
B.8.11 emission.throttled #
- Type:
uint.duration - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
How long a producer was held back by its rate limit, from the first emit refused for want of budget to the first accepted again. A duration, not a flag. Every emit refused in that span was an event the producer meant to record that is not in the stream.
Carried by:
No event carries this field yet.
B.8.12 emission.type #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The event type an emitter was trying to emit, as it supplied it. It names
what was attempted, not what was recorded: a record carrying it is about
an emission that failed or was cut short, and no event of this type need
exist. Absent where the type itself was the fault — empty, or not valid
UTF-8 — in which case emission.type-length still gives its length.
Carried by:
B.8.13 emission.type-length #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The byte length of the event type an emitter supplied, at most 65 535.
Carried by:
No event carries this field yet.
Generated from kmes.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.9 emitter.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under emitter, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.9.1 emitter.class #
- Type:
uint.enum - Values:
0 userspace·1 kmes·2 kacs·3 lcs·4 ntfe - Set: open
- Asserted: no
- Carried in: the header
- Defined in:
kernel.evman
The emission path that wrote the record: a kernel subsystem, or userspace
through the emit system call. Set by the kernel, never by the emitter, so
a record claiming a kernel event type with class userspace was written
by a program, whatever its type says.
The class is the path, not the event type's root. StrataFS records are
written through KACS and carry class kacs.
Carried by:
Every record, in the header.
B.9.2 emitter.process.executable #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman - In groups:
subject
The path the emitting process was executed from, resolved at exec with symbolic links already followed. Left out when the process has no executable, as a kernel thread has none.
Carried by:
kacs.audit.access.checked(viasubject)kacs.audit.descriptor.changed(viasubject)kacs.audit.handle.used(viasubject)kacs.audit.privilege.used(viasubject)kacs.caap.policy.changed(viasubject)kacs.caap.sacl.skipped(viasubject)kacs.caap.staging.diverged(viasubject)kacs.descriptor.rejected(viasubject)kacs.impersonation.reverted(viasubject)kacs.impersonation.started(viasubject)kacs.mount.policy.changed(viasubject)
B.9.3 emitter.process.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the header
- Defined in:
kernel.evman
The process the record was written from, as a durable GUID. A random v4
minted when the process's KACS state is allocated, so unlike a PID it is
never reused and outlives the process. Zero on records written outside
task context, where current names whatever task was interrupted rather
than a real actor.
Carried by:
Every record, in the header.
B.9.4 emitter.process.init-ns-admin #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the acting credential held CAP_SYS_ADMIN in the initial user
namespace at the time of the check. A Linux capability, so context for a
decision and never the decision itself: it says whether the caller could
have done the same thing from outside any user namespace, which is how an
operator tells a container-local actor from a host-wide one.
Carried by:
No event carries this field yet.
B.9.5 emitter.process.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman - In groups:
subject
The kernel's name for the emitting process, typically the executable's
basename. Not argv[0] — a process that rewrote its argv is unaffected
here. Lossy: two processes can share a name.
Carried by:
kacs.audit.access.checked(viasubject)kacs.audit.descriptor.changed(viasubject)kacs.audit.handle.used(viasubject)kacs.audit.privilege.used(viasubject)kacs.caap.policy.changed(viasubject)kacs.caap.sacl.skipped(viasubject)kacs.caap.staging.diverged(viasubject)kacs.descriptor.rejected(viasubject)kacs.impersonation.reverted(viasubject)kacs.impersonation.started(viasubject)kacs.mount.policy.changed(viasubject)
B.9.6 emitter.process.pid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman - In groups:
subject
The process ID the record was written from. Reliable only in the short
term: PIDs are reused, so a PID in a week-old record may name something
unrelated. Correlate durably on emitter.process.guid.
Carried by:
kacs.audit.access.checked(viasubject)kacs.audit.descriptor.changed(viasubject)kacs.audit.handle.used(viasubject)kacs.audit.privilege.used(viasubject)kacs.caap.policy.changed(viasubject)kacs.caap.sacl.skipped(viasubject)kacs.caap.staging.diverged(viasubject)kacs.descriptor.rejected(viasubject)kacs.impersonation.reverted(viasubject)kacs.impersonation.started(viasubject)kacs.mount.policy.changed(viasubject)
B.9.7 emitter.process.userns-depth #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How deeply nested the acting credential's user namespace is. 0 is the
initial namespace; each level of unshare(CLONE_NEWUSER) adds one. Read
beside emitter.process.init-ns-admin: a capability held at depth 3 is
held only over that namespace's own objects.
Carried by:
No event carries this field yet.
B.9.8 emitter.stamp-failure #
- Type:
str.enum - Values:
no-task-context·effective-token-guid-read-failed·true-token-guid-read-failed·process-no-process-state - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Which header identity stamp could not be filled, and why. A header GUID of zero is otherwise ambiguous: a kernel-originated record that correctly has no actor and one whose actor was lost read the same, and this field is what tells them apart. Absent when every stamp was filled.
no-task-context covers all three stamps at once: the record was written
outside task context, where current is whatever task was interrupted, so
the kernel deliberately writes no identity rather than a wrong one. The
other values name one stamp each — the effective or true token's GUID
could not be read, or the process had no KACS process state.
Carried by:
No event carries this field yet.
B.9.9 emitter.thread.tid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The thread that wrote the record, where the operation is scoped to a thread
rather than to its process. Impersonation is per thread, so on an
impersonation record this, not emitter.process.pid, names what took on
the client's identity. Reused like a PID, so correlate durably on the
header GUIDs.
Carried by:
B.9.10 emitter.token.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the header
- Defined in:
kernel.evman
The effective token the emitting thread ran under, as a durable GUID.
Under impersonation this is the client's token; emitter.true-token.guid
is the thread's underlying one.
Carried by:
Every record, in the header.
B.9.11 emitter.true-token.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the header
- Defined in:
kernel.evman
The emitting thread's own token, before any impersonation. Equal to
emitter.token.guid when the thread is not impersonating, so a query for
records where the two differ is a query for impersonated work.
Carried by:
Every record, in the header.
Generated from kernel.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.10 engine.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under engine, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.10.1 engine.abi-version #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The version of the NTFE device interface the running kernel implements. The interface is experimental and carries no stability promise, so a tool reading the device checks this before trusting anything else.
Carried by:
No event carries this field yet.
B.10.2 engine.enforcing #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
Whether NTFE enforces anything at all: true when at least one rules layer has a published forest. False from boot until the first generation ingests, and whenever no layer has policy, in which case every traversal is waved through.
Carried by:
No event carries this field yet.
Generated from ntfe.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.11 evaluation.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under evaluation, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.11.1 evaluation.flags #
- Type:
uint.flags - Values:
0x1 backstop·0x2 fail-closed·0x4 reject-degraded·0x8 rejudged·0x10 identity-unresolved - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
How an evaluation reached its result. backstop means no rule yielded and
the compiled-in DROP answered. fail-closed means evaluation could not
finish, usually for want of atomic memory, and the packet was dropped
regardless of policy. reject-degraded means a REJECT went out as a DROP
because no answer could be built or sent. rejudged means a flow's cached
verdict was stale, by generation or by time, and was judged again.
identity-unresolved means an endpoint could not be attributed.
Carried by:
No event carries this field yet.
Generated from ntfe.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.12 event.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under event, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.12.1 event.boot.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the header
- Defined in:
kernel.evman
The boot the record belongs to. Not in the record itself: the consumer
supplies the boot it read the record in, which is what makes
event.sequence unique across reboots.
Carried by:
Every record, in the header.
B.12.2 event.cpu #
- Type:
uint - Asserted: no
- Carried in: the header
- Defined in:
kernel.evman
The CPU whose ring carried the record. Sequence numbers are per CPU, so
event.cpu and event.sequence together identify a record within a boot.
Carried by:
Every record, in the header.
B.12.3 event.sequence #
- Type:
uint - Asserted: no
- Carried in: the header
- Defined in:
kernel.evman
The record's sequence number within its CPU's ring for this boot. Sequence numbers on one ring are contiguous, so a gap between two records read from it is a count of records lost.
Carried by:
Every record, in the header.
B.12.4 event.time #
- Type:
uint.time - Asserted: no
- Carried in: the header
- Defined in:
kernel.evman
When the record was made, in nanoseconds since the Unix epoch. Stamped by the kernel at emission, so a payload never repeats it: an event carrying its own timestamp is carrying a second, forgeable one.
Carried by:
Every record, in the header.
B.12.5 event.type #
- Type:
str - Asserted: no
- Carried in: the header
- Defined in:
kernel.evman
The event type, such as kacs.audit.access.checked. Stamped from the
emitter's argument, so it names what the emitter claims happened; the
emitter.* header fields are the evidence of who wrote it.
Carried by:
Every record, in the header.
Generated from kernel.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.13 fields.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under fields, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.13.1 fields.attestation.userspace #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Set on a kernel-originated record that carries a value userspace supplied, and absent otherwise. Such a value is the supplying component's claim: it is what the component says it enforced, which the kernel copied but did not observe. The fields that can hold one are marked asserted in their definitions.
A userspace-originated record never carries this field, because its
emitter.class already says the whole payload is the emitter's word.
Carried by:
kacs.audit.access.checkedkacs.audit.privilege.usedkacs.caap.sacl.skippedkacs.caap.staging.diverged
Generated from kernel.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.14 firmware.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under firmware, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.14.1 firmware.policy-mode #
- Type:
str.enum - Values:
enforce·log - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The firmware signature policy in force. enforce refuses a firmware blob
that does not verify at the PeiosTcb tier; log loads it anyway and
records the verdict.
The shipping default is log. On a stock system a failing
firmware.result therefore does not mean the blob was refused; read this
field before concluding that a device went without its firmware. The mode
is fixed for the boot and never changes at runtime.
Carried by:
No event carries this field yet.
B.14.2 firmware.policy-origin #
- Type:
str.enum - Values:
compiled-default·command-line - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
Where firmware.policy-mode came from for this boot. compiled-default
is the kernel build's own setting; command-line is a kacs_fwsig=enforce
or kacs_fwsig=log override. An override lasts one boot and needs only
access to the bootloader, which is why it is recorded apart from the mode
itself. An unrecognised kacs_fwsig= value is ignored with a kernel
warning and leaves the origin compiled-default.
Carried by:
No event carries this field yet.
B.14.3 firmware.result #
- Type:
str.enum - Values:
allowed·unsigned·no-key-match·below-tcb·unverifiable·probe-failed - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The verdict on a firmware load, named for its KACS_FW_* code. allowed
is the only passing verdict: the blob verified against a built-in key at
the PeiosTcb tier. unsigned means the file carried no signature,
no-key-match that it was signed but no built-in key verified it,
below-tcb that a key verified it at a lower tier, unverifiable that
verification could not run, and probe-failed that the signature
material could not be read.
A failing verdict is not a refusal under log policy. Whether the
blob was loaded is firmware.policy-mode's to say; this field is what the
kernel concluded about the blob either way. unverifiable and
probe-failed fail rather than pass on purpose: a verification that
cannot run must not become the way around it.
Carried by:
No event carries this field yet.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.15 flow.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under flow, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.15.1 flow.endpoint #
- Type:
str.enum - Values:
source·destination - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
Which end of the flow the record concerns: the local endpoint the flow layer judged. Outbound traffic is judged for its source and inbound traffic for its destination; a loopback flow has two local ends and is judged once for each, and this field is what tells those two records apart.
Carried by:
No event carries this field yet.
B.15.2 flow.id #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
Conntrack's identifier for the flow. Not emitted today: the flows dump carries it but the verdict record does not, so a verdict cannot be joined to the flow it was passed on. Unique only among flows alive at the same time.
Carried by:
No event carries this field yet.
B.15.3 flow.rule.hash #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The hash of the path of the rule behind a flow's cached verdict: the
FNV-1a-64 hash of the whole registry path, kept in the flow's sentence. The
sentence keeps no path, so after a re-judgment this is the only thing that
survives about the rule that previously decided the flow. Resolve it
against the generation that wrote the sentence, because a later generation
may name its rules differently. It is the same value a report carries as
rule.hash.
Carried by:
No event carries this field yet.
B.15.4 flow.state #
- Type:
str.enum - Values:
new·established·related·invalid·untracked - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
How connection tracking classified this traffic. untracked means
connection tracking gave the traffic no flow state NTFE recognises:
tracking was not applied, or conntrack judged the packet incoherent. Absent
at the ingress seat, which stands before connection tracking.
invalid is not produced today. It is reserved for traffic that
tracking applied to and found fitting no known flow, but telling that apart
needs conntrack's own verdict, which NTFE does not read yet, so incoherent
traffic reports untracked. The value stays listed because the enumeration
is closed and its values are part of the ABI.
Carried by:
B.15.5 flow.verdict-expiry #
- Type:
uint.time - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
When a flow's cached verdict stops being valid, because a time condition the judgment consulted will next change its answer. The flow is re-judged on its first packet after this time. Absent when the verdict has no time edge, though it is still re-judged when a new generation lands. The kernel holds this to the whole second.
Carried by:
No event carries this field yet.
Generated from ntfe.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.16 graph.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under graph, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.16.1 graph.context #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The graph execution context an event belongs to: one boot's start of its service set, or one on-demand start of a service and everything it needs. Records with the same context belong to one attempt to bring a set of services up.
Numbered from 0 in the order peinit creates contexts. Not durable: the numbering starts again when peinit does.
Carried by:
B.16.2 graph.counts.added #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
How many service definitions a configuration reload added: names that had no definition before.
Written only when the reload succeeded. A failed reload changed nothing, and carries no counts.
Carried by:
B.16.3 graph.counts.discarded #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
How many definitions a configuration reload withdrew and dropped at
once, because their services were not running. Written only when the
reload succeeded, as graph.counts.added is.
Carried by:
B.16.4 graph.counts.marked-removed #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
How many definitions a configuration reload withdrew from services that
were still running. Such a service is left running, cannot be started
again, and is discarded once it stops. Written only when the reload
succeeded, as graph.counts.added is.
Carried by:
B.16.5 graph.counts.restored #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
How many definitions a configuration reload brought back after an
earlier reload had removed them while their services were still
running. Written only when the reload succeeded, as
graph.counts.added is.
Carried by:
B.16.6 graph.counts.undecodable #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
How many definitions a configuration reload found in the registry but
could not decode. Each such service is failed, or, if it was running,
left running with its definition marked removed. Not counted under
graph.counts.marked-removed or graph.counts.discarded, because the
key is still there. Written only when the reload succeeded, as
graph.counts.added is.
Carried by:
B.16.7 graph.counts.updated #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
How many existing service definitions a configuration reload changed.
Written only when the reload succeeded, as graph.counts.added is.
Carried by:
B.16.8 graph.phase #
- Type:
str.enum - Values:
boot·reload-config·phase2-boot - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
When the service graph was validated. The phase changes what a finding
did: at boot, the services it names are marked and the boot continues;
at reload-config, the whole reload is rejected and nothing is applied;
at phase2-boot, the boot plan itself could not be built.
Carried by:
B.16.9 graph.role #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The role no service fills. A role is a virtual name a service declares it
provides, such as authn, and other services depend on the role rather
than on a service name. The services that need it are in
graph.services.
Carried by:
B.16.10 graph.services #
- Type:
str[] - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The services a graph finding involves. For a dependency cycle, the services in the cycle, in the order the cycle runs; for an unfilled role, the services that need the role and cannot start; for a reload during the boot window, the services whose definitions were deferred.
Carried by:
peinit.boot.downgradedpeinit.config.reload.appliedpeinit.config.reload.deferredpeinit.graph.validation.failedpeinit.graph.validation.warned
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.17 identity.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under identity, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.17.1 identity.sids #
- Type:
bin.sid[] - Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The user SIDs of the well-known identities the kernel mints at boot: today SYSTEM (S-1-5-18) and Anonymous Logon (S-1-5-7), the roots of every identity on the system. A boot record without one of them means that token could not be built — a failure the code otherwise returns silently.
Carried by:
No event carries this field yet.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.18 kernel.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under kernel, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.18.1 kernel.build.disabled #
- Type:
str[] - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The security-relevant kernel build options this kernel was built without,
by CONFIG_ name. They matter as much as those enabled: an option that
should be off — a second packet-filtering framework, a mandatory access
control module Peios does not use — would be a second, unratified policy
surface, and a kernel built with it on is otherwise indistinguishable
from one built correctly.
Carried by:
No event carries this field yet.
B.18.2 kernel.build.enabled #
- Type:
str[] - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The security-relevant kernel build options this kernel was built with,
by CONFIG_ name, such as CONFIG_MODULE_SIG_FORCE and
CONFIG_STRICT_DEVMEM. Only the options that bear on security are
listed, not the whole configuration.
Carried by:
No event carries this field yet.
B.18.3 kernel.divergence.checks #
- Type:
str[] - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The checks stock Linux makes that this kernel does not, one name per
check. Peios removes or replaces several upstream checks — the
capability-based ptrace gate, the credential tests in signal delivery,
the switch to real credentials in faccessat — because KACS makes those
decisions instead. An investigator reasoning from Linux semantics will be
wrong about each one, and this is where to find out which. Nothing emits
the field yet, so the names it will carry are not yet fixed.
Carried by:
No event carries this field yet.
B.18.4 kernel.divergence.reports #
- Type:
str[] - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The values this kernel reports differently from stock Linux, one name per
divergence. Where kernel.divergence.checks lists checks not made, this
lists answers that differ: getuid and getgid reading the task's real
credentials, SO_PEERCRED and the filesystem identity carrying the IDs
KACS projects, the capability sets in /proc and from capget
rewritten. Correlating Linux uids across sources fails silently without
it, because a peer credential and a /proc entry can disagree in ways
stock Linux never produces. Nothing emits the field yet, so the names it
will carry are not yet fixed.
Carried by:
No event carries this field yet.
B.18.5 kernel.lockdown #
- Type:
str.enum - Values:
none·integrity·confidentiality - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The kernel lockdown level in force. A Peios kernel is built to force
integrity, which refuses the paths that write to the running kernel —
/dev/mem, kexec, hibernation, raw MSR writes, unsigned BPF — to every
principal however privileged; KACS has no say in it and no privilege lifts
it. Any other value means the machine is not running a kernel built as
Peios builds it.
Carried by:
No event carries this field yet.
B.18.6 kernel.lsms #
- Type:
str[] - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The Linux security modules active, in the order the kernel calls them; a
Peios kernel runs landlock, lockdown, integrity and pkm. The list
is fixed and checked when the kernel is built but stated nowhere at
runtime, so without this field a machine booted on a kernel built some
other way cannot be told apart from one that was not.
Carried by:
No event carries this field yet.
B.18.7 kernel.securityfs-node #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The securityfs node an initialisation concerned, as its path beneath the
securityfs root (/sys/kernel/security), such as kacs/sessions. A node
that fails to be created makes every tool that reads it fail with
ENOENT, and this is where the cause is recorded. KACS creates its nodes
all or nothing, so one failure leaves the whole kacs directory absent.
Carried by:
No event carries this field yet.
B.18.8 kernel.version #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The Linux kernel release actually running, as uname -r reports it.
Peios pins the upstream version it builds on and checks it at build time,
but nothing else states it at runtime.
Carried by:
No event carries this field yet.
Generated from kmes.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.19 linux.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under linux, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.19.1 linux.cap #
- Type:
str.enum - Values:
chown·dac-override·dac-read-search·fowner·fsetid·kill·setgid·setuid·setpcap·linux-immutable·net-bind-service·net-broadcast·net-admin·net-raw·ipc-lock·ipc-owner·sys-module·sys-rawio·sys-chroot·sys-ptrace·sys-pacct·sys-admin·sys-boot·sys-nice·sys-resource·sys-time·sys-tty-config·mknod·lease·audit-write·audit-control·setfcap·mac-override·mac-admin·syslog·wake-alarm·block-suspend·audit-read·perfmon·bpf·checkpoint-restore - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The Linux capability whose check prompted the record, named as its
CAP_* constant with the prefix removed, in kebab case: CAP_NET_ADMIN
is net-admin. Context, never the decision. On Peios every capability
check is answered by KACS, either by mapping the capability to a
privilege — net-admin to SeTcbPrivilege, for example — or with a fixed
answer, so this says which Linux gate asked and privilege.name and the
outcome say what was decided.
Two sets have fixed answers and never involve a privilege. Twelve are
granted to every process unconditionally, because KACS makes the real
decision elsewhere: chown, dac-override, dac-read-search, fowner,
fsetid, kill, setgid, setuid, net-bind-service,
net-broadcast, ipc-owner and lease. setpcap, setfcap and
mac-override are refused whatever the token holds.
The set is open because new Linux releases add capabilities.
Carried by:
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.20 loss.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under loss, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.20.1 loss.bytes #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The bytes of events a loss discarded. Reported in bytes where KMES cannot count events: when a ring's framing is found corrupt, everything pending is dropped at once without being parsed, and when a capacity swap skips an event too large for the smaller ring, only its size is known.
Carried by:
No event carries this field yet.
B.20.2 loss.class #
- Type:
uint.enum - Values:
0 userspace·1 kmes·2 kacs·3 lcs·4 ntfe - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The emission path of the events that were lost, numbered as emitter.class
numbers it: the kernel subsystem, or userspace, that was writing them. A
loss record is written by KMES, so its own emitter.class is always
kmes; this is the field that says whose events went missing.
Carried by:
No event carries this field yet.
B.20.3 loss.count #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The number of events lost in one occurrence. For a gap found in the
sequence numbers, the size of that gap: loss.sequence-last minus
loss.sequence, plus one.
Carried by:
B.20.4 loss.preceding-time #
- Type:
uint.time - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The time of the last record received before a gap, in nanoseconds since the Unix epoch. With the time of the record that revealed the gap, it bounds when the lost events were written. Absent when no record preceded the gap on that ring, as at the start of a boot.
Carried by:
B.20.5 loss.sequence #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The first sequence number a loss covers. Sequence numbers are contiguous
per CPU ring, so with buffer.cpu this locates the gap exactly, and it is
the field that reconciles a loss KMES reports with a gap a consumer infers
from event.sequence.
Not every loss has one. An event dropped because its CPU had no usable ring, an event emitted before KMES started, and an emit refused at the system call consume no sequence number and leave no gap to point at.
Carried by:
B.20.6 loss.sequence-last #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The last sequence number of a gap, where loss.sequence is the first.
Both ends are included in the loss.
Carried by:
B.20.7 loss.total #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kmes.evman
The cumulative count of events a ring has dropped. Not emitted today. KMES keeps the counter, but it is not in the ring's metadata page and nothing outside the kernel's own tests reads it, so no emitter has it to report. It also under-reports: a corrupt-ring discard, an event skipped by a capacity swap and an event lost for want of a usable ring are never added to it, so even once it is emitted it is a lower bound rather than a total.
Carried by:
No event carries this field yet.
Generated from kmes.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.21 machinery.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under machinery, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.21.1 machinery.collision.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The other name in a hash collision: a second tag or stream name whose hash equals that of the name the record carries. NTFE refuses a generation in which this happens, so that a hash stays a single identity while the policy runs.
Carried by:
No event carries this field yet.
B.21.2 machinery.hash #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The FNV-1a-64 hash by which the tag and counter stores know a tag name or a counter stream name. Tags and streams are hashed by the same function into separate namespaces, so one hash can name both a tag and a stream: read it with the record's event type. A hash maps to a name only within the generation that published it.
Carried by:
No event carries this field yet.
Generated from ntfe.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.22 memory.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under memory, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.22.1 memory.check-phase #
- Type:
str.enum - Values:
mmap·mprotect·enable-time-scan - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
When a memory-protection check fired. mmap and mprotect are the calls
a process makes. enable-time-scan is the sweep KACS makes over a
process's existing mappings when a mitigation is turned on, and what it
refuses is the mitigation, not a mapping: a refusal in that phase leaves
the process unprotected rather than short of memory.
Carried by:
No event carries this field yet.
B.22.2 memory.protection #
- Type:
uint.flags - Values:
0x1 PROT_READ·0x2 PROT_WRITE·0x4 PROT_EXEC·0x8 PROT_SEM·0x1000000 PROT_GROWSDOWN·0x2000000 PROT_GROWSUP - Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The protection bits a mapping call asked for: the prot argument to
mmap or mprotect. Write-XOR-execute refuses any request that carries
both PROT_WRITE and PROT_EXEC.
Carried by:
No event carries this field yet.
B.22.3 memory.protection-previous #
- Type:
uint.flags - Values:
0x1 PROT_READ·0x2 PROT_WRITE·0x4 PROT_EXEC - Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The protection an existing mapping carried before an mprotect, in the
same bit encoding as memory.protection. Read from the mapping's own
flags, so only the read, write and execute bits are ever set. Absent on
mmap, which has no earlier mapping.
It matters because write-XOR-execute judges the transition, not just the
request: an mprotect to PROT_EXEC alone is refused when the mapping is
currently writable, and one to PROT_WRITE alone when it is currently
executable.
Carried by:
No event carries this field yet.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.23 mitigation.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under mitigation, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.23.1 mitigation.action #
- Type:
str.enum - Values:
enable·disable·lock·unlock - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The direction of a change to a process mitigation's state. enable and
lock harden a process; disable and unlock loosen it, and are the
ones to read closely. A Peios mitigation is meant to persist once set, so
a permitted disable or unlock is unusual, and a refused one is a
process trying to weaken its own protection.
Carried by:
No event carries this field yet.
B.23.2 mitigation.name #
- Type:
str.enum - Values:
wxp·tlp·lsv·ui-access·no-child·cfif·cfib·pie·sml - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The single process mitigation a record is about, named for its
KACS_MIT_* bit: wxp write-XOR-execute, tlp trusted library paths,
lsv library signature verification, no-child the ban on creating
processes, cfif and cfib forward- and backward-edge control-flow
integrity, pie the refusal of non-PIE executables, sml the
speculation mitigation lock, and ui-access, which is reserved.
There is no cfi. The legacy KACS_MIT_CFI bit is an alias that sets
both cfif and cfib and is not itself kept, so a record names whichever
of the two it concerns.
A request for cfif fails today on every live process, because the
kernel offers no way to turn on IBT for a running task. A service that
asks for it is not protected by it, whatever it believes.
Carried by:
No event carries this field yet.
B.23.3 mitigation.trusted-prefixes #
- Type:
str.path[] - Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The path prefixes the trusted library paths mitigation (tlp) accepts. A
process under it may map a file executable only if the file's path begins
with one of these. Each entry begins and ends with / and is compared
byte for byte with the start of the path, without normalisation. At most
64, held in one table for the whole system.
These are prefixes, not files: a search for a file's path does not match
the prefix that admitted it. An empty list trusts nothing rather than
checking nothing, so a process under tlp is then refused every
file-backed executable mapping.
Carried by:
No event carries this field yet.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.24 mutation.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under mutation, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.24.1 mutation.sequence #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The sequence number LCS stamped on a registry mutation. Allocated from the kernel's global monotonic counter, so sequences order writes across every hive and source, and the higher sequence wins when two writes land in layers of equal precedence.
The counter resumes at boot from one past the highest sequence any registered source reports. A record's sequence is therefore comparable with any other from the same registry, not only those from the same boot.
Carried by:
B.24.2 mutation.sequence-expected #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The sequence number a conditional write asserted as the value's current
one, the precondition of a compare-and-swap. The write proceeds only if
the value still carries this sequence, and fails with cas-failed from the
source otherwise. Absent on an unconditional write.
Carried by:
B.24.3 mutation.sequence-watermark #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The next sequence number the kernel would allocate, at the moment of the
record. Every sequence legitimately stored in the registry is below it, so
a source reporting a sequence at or above the watermark is claiming a
write the kernel never allocated, which is the future-sequence check that
lcs.source.response.rejected records.
Carried by:
No event carries this field yet.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.25 network.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under network, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.25.1 network.bridge.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The name of the bridge device an interface was enslaved to or released from. Enslaving an interface to a bridge moves its traffic off the IP seats and onto its device seats alone, which changes which rules can apply to it.
Carried by:
No event carries this field yet.
B.25.2 network.direction #
- Type:
str.enum - Values:
in·out - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
Which way the traffic was moving relative to this machine.
Carried by:
B.25.3 network.ether-type #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The link-layer protocol number of the frame.
Carried by:
B.25.4 network.family #
- Type:
uint.enum - Values:
0 AF_UNSPEC·2 AF_INET·10 AF_INET6 - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The address family of the addresses in this record. AF_UNSPEC when the
traffic was evaluated below the layer where addresses exist, in which case
no address, port or protocol field is present.
Carried by:
B.25.5 network.hop-limit #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The packet's remaining hop count: the IPv4 TTL or the IPv6 hop limit, as the packet carried it at the seat that judged it. Absent below the layer where an IP header exists.
Carried by:
No event carries this field yet.
B.25.6 network.interface.disposition #
- Type:
str.enum - Values:
policed·bridge-port·seats-failed·other-namespace - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
How NTFE stands towards a network device. policed has its device seats in
place, and its IP traffic also reaches the IP seats, where the flow layer
and conntrack facts exist. bridge-port has its device seats, but its
frames are switched at the link layer and never reach the IP seats, so only
the device seats judge them and no flow-state or identity rule applies.
seats-failed is a device that registered but whose seats could not be
attached: its raw-packet traffic is never judged. other-namespace is
outside the initial network namespace and is not policed at all.
Carried by:
No event carries this field yet.
B.25.7 network.interface.disposition-previous #
- Type:
str.enum - Values:
policed·bridge-port·seats-failed·other-namespace - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The disposition a network device had before the change a record reports,
in the terms of network.interface.disposition. A change from policed to
bridge-port is the one that quietly withdraws every flow-layer rule from
the device.
Carried by:
No event carries this field yet.
B.25.8 network.interface.index #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The kernel's index for that interface. Zero when no interface was
determined. Indexes are reused across interface teardown, so
network.interface.name is the durable one.
Carried by:
B.25.9 network.interface.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The network interface the traffic was seen on.
Carried by:
B.25.10 network.length #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The length of the traffic unit evaluated.
Carried by:
B.25.11 network.namespace #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The network namespace a device or flow belongs to, by the kernel's namespace number. NTFE polices only the initial namespace, so today this field exists to say that something was in a different one and was not judged at all.
Carried by:
No event carries this field yet.
B.25.12 network.protocol #
- Type:
uint.enum - Values:
1 ICMP·2 IGMP·6 TCP·17 UDP·47 GRE·50 ESP·51 AH·58 IPv6-ICMP·132 SCTP·136 UDPLite - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The transport protocol number, from the IANA protocol numbers registry. A number not listed is still a protocol, rendered as its number.
Carried by:
B.25.13 network.tcp-flags #
- Type:
uint.flags - Values:
0x1 FIN·0x2 SYN·0x4 RST·0x8 PSH·0x10 ACK·0x20 URG·0x40 ECE·0x80 CWR - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The flag bits of the TCP header, taken whole from the flags byte at offset 13. Absent on any protocol other than TCP, and also absent on a TCP packet whose header NTFE could not read, which a reader cannot otherwise tell from a protocol without flags.
Carried by:
No event carries this field yet.
B.25.14 network.vlan #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The VLAN id the traffic belongs to. At the device seats it is the tag on the frame; at the IP seats the tag has already been stripped and it is the id of the VLAN device the packet arrived on or leaves by. Absent when the traffic is not on a VLAN.
Carried by:
No event carries this field yet.
Generated from ntfe.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.26 notify.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under notify, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.26.1 notify.errno #
- Type:
int.errno - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The error number a service sent as ERRNO=, as a negative errno. Services
send it positive, by convention; this field holds it negated.
The field is the service's claim about itself, informational only; peinit neither acts on nor retains it. The protocol does not require the text to be a number: text that is not a positive number has no errno to carry, and the record is written without this field.
Carried by:
B.26.2 notify.exit-status #
- Type:
int - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The exit status a service sent as EXIT_STATUS=, informationally. Not
the exit code of any process: a service may send it while still running,
and its real exit appears in object.process.exit-code. Text that is not
an integer is not carried, and the record is written without this field.
Carried by:
B.26.3 notify.progress.bounded #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
Whether the progress a service or submitted job reported declares an end:
true for PROGRESS=N/ and PROGRESS=N/M, false for a bare PROGRESS=N,
which counts with no end (PSPU §4.19). It is what tells a viewer to draw a
bar or to expect one rather than show a rising count.
True with notify.progress.total absent means the end is declared but not
yet known. Absent when no progress figure has been reported, as when a
service has sent only PROGRESS_UNIT=.
Not implied by notify.progress.total: PROGRESS=N and PROGRESS=N/ both
leave the total absent, and PSPU §4.19 has a viewer draw them differently.
Carried by:
B.26.4 notify.progress.current #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
How far a service or submitted job has got, as it reported with
PROGRESS=: the count before any /. Its unit is
notify.progress.unit. It may fall from one record to the next; do not
read motion from the difference between two.
Carried by:
B.26.5 notify.progress.total #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The end notify.progress.current is counting towards, as reported after
the / of PROGRESS=. At least 1, and never less than the current
value. The total may change between records, because a job that
rescans has learned something.
Absent when the service gave no total, and absence alone does not say
whether one is coming. PROGRESS=N counts with no end, while
PROGRESS=N/ counts towards an end not yet known, and both leave this
field absent; notify.progress.bounded is what tells them apart.
Carried by:
B.26.6 notify.progress.unit #
- Type:
str.enum - Values:
bytes·items·percent - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
What notify.progress.current and notify.progress.total count, as
reported with PROGRESS_UNIT=. Absent when the service gave no unit.
Carried by:
B.26.7 notify.status #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The text a service sent as STATUS=: free text describing what it is
doing. The service's own words, unvalidated; never read it as a
statement by peinit.
Carried by:
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.27 object.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under object, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.27.1 object.account.sid #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
lpsd.evman
The SID of the account a record is about: a principal a principal source holds, whose credential was checked or changed, or which was created, deleted or modified. The SID, never the name: an account can be renamed, and a record that named it would then describe someone else.
Not object.token.sid, which is the user of a token. A token speaks for
an account, but an account changed while nobody is signed in has no token
to name it by.
Carried by:
lpsd.account.createdlpsd.account.deletedlpsd.account.modifiedlpsd.credential.changedlpsd.credential.verifiedlpsd.group.member.addedlpsd.group.member.removed
B.27.2 object.certificate.digest #
- Type:
bin - Asserted: no
- Carried in: the payload
- Defined in:
trustd.evman
The SHA-256 digest of the certificate's DER encoding: its fingerprint, the
name a distrust gives it and the one trust list prints, carried as 32
bytes rather than as hexadecimal. The name to search by, because it is the
only one that identifies a certificate: two certificates can share a
subject name, and a subject name can be forged.
Carried by:
B.27.3 object.certificate.digest-previous #
- Type:
bin - Asserted: no
- Carried in: the payload
- Defined in:
gxwid.evman
The SHA-256 digest of the DER encoding of the certificate this one
replaced: its fingerprint, as 32 bytes, in the form of
object.certificate.digest. With it, the record answers "what was this
machine's certificate before?" without going back to an older record.
Carried by:
B.27.4 object.certificate.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
trustd.evman
The certificate's subject distinguished name, in the form trust show
prints it, such as CN=DigiCert TLS ECC P384 Root G5,O=DigiCert\, Inc.,C=US.
For a person to read; it does not identify the certificate on its own.
Search by object.certificate.digest.
Carried by:
B.27.5 object.certificate.purposes #
- Type:
str.enum[] - Values:
server-auth·code-signing - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
trustd.evman
What the certificate is trusted for, one value per purpose, each once. The
registry spells a purpose in the form ServerAuth; the record carries it in
kebab-case, a hyphen at each change from lower to upper case and in place
of anything that is not a letter or a digit, so ServerAuth is
server-auth and CodeSigning is code-signing. A root from the shipped
bundle is trusted for server-auth. An administrator may record any other
purpose, and it appears here in the same form.
Carried by:
B.27.6 object.cgroup.generation #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The cgroup generation of the service the cgroup belongs to. peinit
advances it when a service's tree is leaked, so that the next run gets a
fresh tree; it is the number in a path's %gen suffix. Not the same
counter as object.service.generation, which advances on every start.
Carried by:
B.27.7 object.cgroup.path #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The cgroup the event is about, as its full path under
/sys/fs/cgroup/peinit. A service's cgroup is named for the service,
with any byte outside letters, digits, ., _ and - written as %
and two hexadecimal digits, so the path can differ from the service name.
Once a service's tree has leaked, later trees carry a %gen suffix and
the cgroup generation, so that a new run never shares a tree with an
unkillable process. A submitted job's cgroup is under jobs/, named by
the job's GUID.
Carried by:
B.27.8 object.cgroup.type #
- Type:
str.enum - Values:
service-tree·health·hooks·helper - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
Which part of a service's process containment the cgroup is.
service-tree is the whole of it and the most serious to lose; health
and hooks hold its health checks and hooks; helper is a pre-start
check helper's cgroup.
Carried by:
B.27.9 object.claim.holder #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peipkg.evman
The package that holds object.claim.role after the event, by name.
Absent when the event left the role unheld, as revoking a claim does.
Carried by:
B.27.10 object.claim.role #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peipkg.evman
The role a claim event changed the holder of. A role is a virtual name that several installed packages may contend to own on the filesystem, with at most one holding it at a time (PSPU §5.23).
Carried by:
B.27.11 object.device.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The device a firmware blob was being loaded for.
Not emitted today. The firmware check runs in the hook on the file read, which sees the file and not the device that requested it, so the kernel has no device to report.
Carried by:
No event carries this field yet.
B.27.12 object.event-namespace.pattern #
- Type:
str - Asserted: yes
- Carried in: the payload
- Defined in:
eventd.evman
The event-namespace pattern whose security descriptor eventd checked a
caller's access against. It is the pattern the descriptor was written for,
not the event type the caller asked about: eventd looks for a descriptor
under the event type itself, then under each shorter dotted prefix of it,
then under *, and uses the first it finds. A read of
kacs.audit.access.checked therefore records kacs or * when that is
where the descriptor lives, so a search for one event type by its full name
misses checks made under a broader pattern.
The value is eventd's claim, copied by the kernel from the check's audit
context, so a record carrying it also carries fields.attestation.userspace.
Carried by:
No event carries this field yet.
B.27.13 object.fd-store.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The name a service gave a file descriptor it asked peinit to keep, with
FDNAME=. A descriptor sent without a name is stored as stored.
Names are not unique: a service may store several descriptors under one
name, and two services may use the same one, so a name identifies
nothing on its own without object.service.name.
Carried by:
B.27.14 object.file.code-kind #
- Type:
str.enum - Values:
exec-image·mapped-library·firmware-blob - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
What kind of code load the record concerns: an image being executed, a
library being mapped executable, or a firmware blob being loaded for a
device. A search on object.file.path returns all three; this tells them
apart.
Carried by:
No event carries this field yet.
B.27.15 object.file.delete-on-close-count #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many delete-on-close requests are armed on the file's inode. The file is removed when the last handle carrying one is closed, so a count above zero on an open says the file is already scheduled to disappear.
Carried by:
No event carries this field yet.
B.27.16 object.file.device #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The device number of the filesystem the object lives on, as stat reports
it in st_dev. With object.file.inode it names a file where no path can
be resolved. A device number is assigned when a filesystem is mounted, so
the pair names a file only while that filesystem stays mounted, and not at
all across a reboot.
Carried by:
B.27.17 object.file.digest #
- Type:
bin - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The SHA-256 digest of a file whose signature was checked. The strongest identifier of "this exact binary" a record can carry: unlike a path or an inode it survives a rename and changes when the content does.
Carried by:
No event carries this field yet.
B.27.18 object.file.elf-type #
- Type:
str.enum - Values:
none·rel·exec·dyn·core - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The ELF type of an executable, from e_type, with the ET_ prefix removed
and the name folded to lower case. exec is a fixed-address executable,
dyn a position-independent one or a shared object. The PIE mitigation
refuses exec. Open because the ELF standard reserves ranges for
operating-system and processor-specific types.
Carried by:
No event carries this field yet.
B.27.19 object.file.fs-type #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The type of filesystem the file lives on, by name, such as ext4. Mount
policy is derived from the filesystem type, so this explains why a file on
proc was never checked at all.
Carried by:
No event carries this field yet.
B.27.20 object.file.gid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The Linux GID that owns the file, as stat reports it. Like
object.file.uid, a projection rather than anything KACS decides on.
Carried by:
No event carries this field yet.
B.27.21 object.file.inode #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The inode number the object presents. The best object identifier a filesystem record has, and neither stable nor unique: StrataFS builds a private inode for every open that shares the number of the outer one, a mount root's number follows whichever stratum provides it, and numbers are re-derived at mount time. Expect both false joins and false splits when correlating on it.
Carried by:
B.27.22 object.file.inode-lower #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The inode number of the real object in the underlying filesystem, beneath
a stacking filesystem's own. Stable, but unique only within that
filesystem. Carried beside object.file.inode; the two differing is how a
record shows that the object a caller holds has been rebound to a different
backing object.
Carried by:
No event carries this field yet.
B.27.23 object.file.inode-replacement #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The inode number of the object that took this one's place: the new object
in a supersede, or the object found where the expected one should have
been. A record carrying both this and object.file.inode names the
object that was meant and the object that was there.
Carried by:
No event carries this field yet.
B.27.24 object.file.integrity #
- Type:
uint.integrity - Values:
0 Untrusted·4096 Low·8192 Medium·12288 High·16384 System - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The mandatory integrity label on the file, as an integrity RID. Distinct
from any principal's integrity: a record about lowering a file's label
carries both, and subject.token.integrity is the caller's.
Carried by:
No event carries this field yet.
B.27.25 object.file.integrity-capped #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the label an exec would have taken from the file's signature was
capped at the process's current label instead of applied. The kernel does
this when a traced process, or one running with no_new_privs, execs a
binary whose signature would raise its PIP label: the exec goes ahead, but
a tracer that no longer dominated the process must not keep control of it.
True means the binary ran with less trust than its signing key grants.
The label capped is the PIP label, object.process.pip.type and
object.process.pip.trust, despite the field's name.
Carried by:
No event carries this field yet.
B.27.26 object.file.mode #
- Type:
uint.flags - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The object's POSIX permission bits, as stat reports them in the low
twelve bits of st_mode: the read, write and execute triples, set-user-ID,
set-group-ID and sticky. A projection for Linux compatibility; access is
decided by the security descriptor, never by these bits. The object's type
is object.file.type, not the high bits of this field.
Carried by:
No event carries this field yet.
B.27.27 object.file.mount-owner.cookie #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The mount that owns an on-disk staging object, where that is not the mount
inspecting it. Two mounts can share a create stratum, so a mount recovering
orphans can meet another mount's staging; this, against
object.file.mount.cookie, is the ownership check that decides whether the
object is left alone.
Carried by:
No event carries this field yet.
B.27.28 object.file.mount.cookie #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The mount instance the file lives on, as the 64-bit random cookie StrataFS assigns each mount. Two mounts sharing a stratum produce otherwise indistinguishable records, which only this separates.
Not emitted today. Only the StrataFS ftrace tracepoints carry the cookie; no event does yet.
Carried by:
No event carries this field yet.
B.27.29 object.file.parent.inode #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The inode number of the directory that holds the file. Subject to the same
caveats as object.file.inode: neither stable nor unique.
Carried by:
No event carries this field yet.
B.27.30 object.file.parent.path #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The path of the directory that holds the file the operation acted on. Carried where the parent is what was decided on — creating or removing an entry is authorised against the directory, not the entry.
Carried by:
No event carries this field yet.
B.27.31 object.file.path #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The absolute path of the file the operation acted on, resolved at the enforcement point.
Carried by:
kacs.audit.access.checkedkacs.audit.descriptor.changedkacs.audit.handle.usedkacs.audit.privilege.usedkacs.caap.sacl.skippedkacs.caap.staging.divergedkacs.descriptor.rejectedpeipkg.action.authorised
B.27.32 object.file.path-relative #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The path of the object within its mount, /-prefixed. Not an absolute
system path — deliberately a separate field from object.file.path,
because a search for an absolute path would otherwise match KACS records
and silently miss these.
StrataFS events fire from inside the filesystem, below the layer where a
mount is known, so there is no absolute path for them to resolve against.
The KACS events covering the same operation do carry the caller's absolute
path, joined by the header's emitter.process.guid and time.
An absolute path to the backing object is derivable from this record
alone: join this onto destination.stratum.path for a copy-up, or onto
source.stratum.path for a refusal. That join is unavailable on a refusal
raised before any provider was determined, where the stratum path is
absent.
Carried by:
B.27.33 object.file.size #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The size of the object in bytes or, on a copy, how many bytes the copy moved. Distinguishes a copy-up of a large file from one of a symlink, which otherwise produce the same record.
Carried by:
No event carries this field yet.
B.27.34 object.file.size-previous #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The size first observed, where the record compares it against a later one.
On a signature check this is the size when probing began and
object.file.size the size when it ended: a difference means the file was
written to while its signature was being read.
Carried by:
No event carries this field yet.
B.27.35 object.file.tcb-signed #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the executed binary was signed by a key trusted at the TCB tier, the floor for code that runs with full system trust. A usermodehelper exec below the floor is refused; an ordinary exec below it is allowed, at the lower trust its signature earns.
Carried by:
No event carries this field yet.
B.27.36 object.file.type #
- Type:
str.enum - Values:
directory·regular·symlink·device·fifo·socket - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
What kind of filesystem object the file is. Some operations are refused by type alone — StrataFS will not copy a socket, FIFO or device up into a writable stratum — and a copy-up of a directory moves no contents, so the type is needed to read the rest of the record.
device covers both character and block devices.
Carried by:
No event carries this field yet.
B.27.37 object.file.type-previous #
- Type:
str.enum - Values:
directory·regular·symlink·device·fifo·socket - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The type the object had before it changed, beside object.file.type. The
pair records an object replaced by one of a different kind under the same
name, such as a stratum root that was a directory and is now a file.
Carried by:
No event carries this field yet.
B.27.38 object.file.uid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The Linux UID that owns the file, as stat reports it. A copy-up preserves
the provider's owner, so this is the UID a disk quota is charged to,
whoever caused the copy. Ownership for access control is the descriptor's
owner SID, object.sd.owner.
Carried by:
No event carries this field yet.
B.27.39 object.group.sid #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
lpsd.evman
The SID of the group a record is about: one a principal source created or
deleted, or whose membership it changed. It may be a group of the source's
own domain or a well-known one such as BUILTIN\Administrators, which a
local source records memberships of.
Carried by:
lpsd.account.modifiedlpsd.group.createdlpsd.group.deletedlpsd.group.member.addedlpsd.group.member.removed
B.27.40 object.handle.enforced #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the handle, or the object behind it, is under KACS enforcement. False on a handle to an object on an unmanaged mount, where no descriptor is checked and every later operation through the handle is allowed.
Carried by:
No event carries this field yet.
B.27.41 object.handle.mode #
- Type:
uint.flags - Values:
0x1 FMODE_READ·0x2 FMODE_WRITE·0x4 FMODE_LSEEK·0x8 FMODE_PREAD·0x10 FMODE_PWRITE·0x20 FMODE_EXEC - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The kernel's open-mode bits on the file handle (f_mode). Only the bits
listed are stable meanings; the kernel sets others for its own
bookkeeping, and a consumer should ignore them. Distinct from
access.granted, which is what KACS allowed the handle; this is what the
handle was opened for.
Carried by:
No event carries this field yet.
B.27.42 object.handle.origin-readable #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether reading the StrataFS layer-origin attribute through the handle was permitted when its view of the directory was settled. Decided once, at open, and enforced at a much later call, so a refusal names a decision made earlier than the record that reports it.
Carried by:
No event carries this field yet.
B.27.43 object.hive.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The name of the registry hive an operation acted on, such as Machine or
Users. A hive is the unit a registry source registers to serve, so this
names which source's namespace the operation reached.
Carried by:
No event carries this field yet.
B.27.44 object.ipc.id #
- Type:
int - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The System V IPC identifier of the object, as semget, shmget or
msgget returned it. Small, reused once the object is removed, and scoped
to an IPC namespace, so it identifies an object only briefly and only
within one namespace.
Carried by:
B.27.45 object.ipc.type #
- Type:
str.enum - Values:
sem·shm·msg - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Which kind of System V IPC object the operation acted on: a semaphore set, a shared-memory segment or a message queue.
Carried by:
B.27.46 object.job.activation-generation #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The activation generation of the service the job belongs to, at the time
the job was created. The same counter as object.service.generation, so
it tells which run of a service a job served. Absent for a submitted
job, which belongs to no service.
Carried by:
B.27.47 object.job.arguments #
- Type:
str[] - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The job's argument vector, including argv[0]. Kept whole up to 32 KiB
of encoded arguments; past that, the arguments are cut at the last whole
argument that fits, never inside one, and object.job.arguments-truncated
is true. A cut argument vector is a prefix of the real one, so do not
read its last element as the last argument the job was given.
Carried by:
B.27.48 object.job.arguments-count #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
How many arguments the job was given, before any were cut. Equal to the
length of object.job.arguments unless that was cut.
Carried by:
B.27.49 object.job.arguments-truncated #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
Whether object.job.arguments was cut to fit the record. When true,
object.job.arguments-count says how many arguments there really were.
Carried by:
B.27.50 object.job.created-time #
- Type:
uint.time - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
When peinit created the job, in nanoseconds since the Unix epoch. Creation is when peinit decided to run it, before its process exists.
peinit times a job on the monotonic clock, and writes the instant as the wall-clock time it was when it builds the record. If the wall clock was stepped between the job's creation and its end, the time is reported on the clock as it stood at the end.
Carried by:
B.27.51 object.job.duration #
- Type:
uint.duration - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
How long the job lasted, in nanoseconds, from its creation to its end.
Measured from creation, not from start, so it includes any time spent
waiting to start; subtract object.job.created-time from
object.job.started-time to find that part.
Carried by:
B.27.52 object.job.executable #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The path of the executable the job runs.
Carried by:
B.27.53 object.job.guid #
- Type:
bin.guid - Asserted: yes
- Carried in: the payload
- Defined in:
peinit.evman
The job the event is about. A job is one process peinit runs: a service's main process, one of its hooks or health checks, or a job a client submitted. The GUID is a UUIDv7, so it also orders jobs by when they were created.
On a kacs.audit.access.checked record the value is peinit's claim, copied
by the kernel from the check's audit context {kind: "job", job: {guid}},
so that record also carries fields.attestation.userspace.
Carried by:
peinit.cgroup.leakedpeinit.event.droppedpeinit.internal-error.containedpeinit.job.createdpeinit.job.endedpeinit.job.output.droppedpeinit.job.startedpeinit.job.status.reported
B.27.54 object.job.started-time #
- Type:
uint.time - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
When the job's process started, in nanoseconds since the Unix epoch.
Absent if the job never started, which is how a job that failed before
its process existed shows itself. Projected from the monotonic clock as
object.job.created-time is.
Carried by:
B.27.55 object.job.state #
- Type:
str.enum - Values:
created·running·completed·failed·abandoned - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The job's state after the event. completed, failed and abandoned
are terminal. abandoned means its process survived SIGKILL: the job is
over but the process is still there.
Carried by:
B.27.56 object.job.submitter.sid #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The user SID of the client that submitted the job. Present only on a
submitted job.
Carried by:
B.27.57 object.job.token.groups #
- Type:
bin.sid[] - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The group SIDs of the token the job's process runs as, in the token's order.
Carried by:
B.27.58 object.job.token.privileges #
- Type:
uint.flags - Values:
0x4 KACS_SE_CREATE_TOKEN_PRIVILEGE·0x8 KACS_SE_ASSIGN_PRIMARY_TOKEN_PRIVILEGE·0x10 KACS_SE_LOCK_MEMORY_PRIVILEGE·0x20 KACS_SE_INCREASE_QUOTA_PRIVILEGE·0x80 KACS_SE_TCB_PRIVILEGE·0x100 KACS_SE_SECURITY_PRIVILEGE·0x200 KACS_SE_TAKE_OWNERSHIP_PRIVILEGE·0x400 KACS_SE_LOAD_DRIVER_PRIVILEGE·0x800 KACS_SE_SYSTEM_PROFILE_PRIVILEGE·0x1000 KACS_SE_SYSTEMTIME_PRIVILEGE·0x2000 KACS_SE_PROFILE_SINGLE_PROCESS_PRIVILEGE·0x4000 KACS_SE_INCREASE_BASE_PRIORITY_PRIVILEGE·0x20000 KACS_SE_BACKUP_PRIVILEGE·0x40000 KACS_SE_RESTORE_PRIVILEGE·0x80000 KACS_SE_SHUTDOWN_PRIVILEGE·0x100000 KACS_SE_DEBUG_PRIVILEGE·0x200000 KACS_SE_AUDIT_PRIVILEGE·0x800000 KACS_SE_CHANGE_NOTIFY_PRIVILEGE·0x1000000 KACS_SE_REMOTE_SHUTDOWN_PRIVILEGE·0x10000000 KACS_SE_MANAGE_VOLUME_PRIVILEGE·0x20000000 KACS_SE_IMPERSONATE_PRIVILEGE·0x100000000 KACS_SE_RELABEL_PRIVILEGE·0x800000000 KACS_SE_CREATE_SYMBOLIC_LINK_PRIVILEGE - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The privileges present on the token the job's process runs as, as the
flags of the 64-bit privilege word of uapi/pkm/token.h, one
KACS_SE_*_PRIVILEGE bit each. Present is not enabled: read
object.job.token.privileges-enabled for which ones the process can use
without enabling them first.
Flags, as the kernel's subject.token.privileges are, not an access mask:
a uint.mask is decoded against object.kind (PGSS §6.5), which means
nothing on a job event.
Carried by:
B.27.59 object.job.token.privileges-enabled #
- Type:
uint.flags - Values:
0x4 KACS_SE_CREATE_TOKEN_PRIVILEGE·0x8 KACS_SE_ASSIGN_PRIMARY_TOKEN_PRIVILEGE·0x10 KACS_SE_LOCK_MEMORY_PRIVILEGE·0x20 KACS_SE_INCREASE_QUOTA_PRIVILEGE·0x80 KACS_SE_TCB_PRIVILEGE·0x100 KACS_SE_SECURITY_PRIVILEGE·0x200 KACS_SE_TAKE_OWNERSHIP_PRIVILEGE·0x400 KACS_SE_LOAD_DRIVER_PRIVILEGE·0x800 KACS_SE_SYSTEM_PROFILE_PRIVILEGE·0x1000 KACS_SE_SYSTEMTIME_PRIVILEGE·0x2000 KACS_SE_PROFILE_SINGLE_PROCESS_PRIVILEGE·0x4000 KACS_SE_INCREASE_BASE_PRIORITY_PRIVILEGE·0x20000 KACS_SE_BACKUP_PRIVILEGE·0x40000 KACS_SE_RESTORE_PRIVILEGE·0x80000 KACS_SE_SHUTDOWN_PRIVILEGE·0x100000 KACS_SE_DEBUG_PRIVILEGE·0x200000 KACS_SE_AUDIT_PRIVILEGE·0x800000 KACS_SE_CHANGE_NOTIFY_PRIVILEGE·0x1000000 KACS_SE_REMOTE_SHUTDOWN_PRIVILEGE·0x10000000 KACS_SE_MANAGE_VOLUME_PRIVILEGE·0x20000000 KACS_SE_IMPERSONATE_PRIVILEGE·0x100000000 KACS_SE_RELABEL_PRIVILEGE·0x800000000 KACS_SE_CREATE_SYMBOLIC_LINK_PRIVILEGE - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
Which of the privileges in object.job.token.privileges are enabled, as
flags in the same 64-bit word. Always a subset of it.
Carried by:
B.27.60 object.job.token.sid #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The user SID of the token the job's process runs as.
Carried by:
B.27.61 object.job.type #
- Type:
str.enum - Values:
service-main·pre-exec-hook·post-exec-hook·reload-hook·health-check·submitted - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
What the job is for. Every type except submitted belongs to a service,
named in object.service.name; a submitted job belongs to the client
that submitted it, named in object.job.submitter.sid, and has no
service.
Carried by:
B.27.62 object.key.created #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether a create-key request made a new key. False means the key already existed and the request opened it instead, so the security descriptor and volatility the caller asked for were not applied: the key keeps the ones it already had.
Carried by:
B.27.63 object.key.cross-source #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether a registry lookup crossed from one source's hive into another's by following a symbolic link. When true, the key reached is served by a different backend from the one the path began in.
Carried by:
No event carries this field yet.
B.27.64 object.key.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The registry key an operation acted on. Keys are identified by GUID rather than by path, so a record names the key durably but not legibly — nothing in the event stream currently binds a key GUID to the path an administrator would recognise.
key means a registry key and nothing else. A cryptographic key is a
signing-key.
Carried by:
lcs.audit.backup.endedlcs.audit.backup.startedlcs.audit.key.createdlcs.audit.key.deletedlcs.audit.key.descriptor.changedlcs.audit.key.hiddenlcs.audit.key.openedlcs.audit.key.tombstonedlcs.audit.restore.endedlcs.audit.restore.startedlcs.audit.value.deletedlcs.audit.value.setlcs.source.response.rejected
B.27.65 object.key.layer.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The name of the registry layer a key operation resolved in: the layer
whose entry the operation read or wrote. Distinct from object.layer.name,
which names a layer the operation acted on as a whole.
Carried by:
lcs.audit.key.createdlcs.audit.key.deletedlcs.audit.key.hiddenlcs.audit.key.tombstonedlcs.audit.value.deletedlcs.audit.value.set
B.27.66 object.key.parent.volatile #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the parent of a key being created is volatile. A volatile key may not have a non-volatile child, so a create that asks for a non-volatile key beneath a volatile parent is refused.
Carried by:
No event carries this field yet.
B.27.67 object.key.path #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The resolved absolute path of the registry key, in canonical form, such as
Machine\System\KMES. Carried beside object.key.guid so a record is
legible as well as durable. Resolved: a %CURRENT_USER% substitution has
already been made, and the path as the caller wrote it is
object.key.path-requested.
Carried by:
lcs.audit.key.createdlcs.audit.key.deletedlcs.audit.key.descriptor.changedlcs.audit.key.hiddenlcs.audit.key.tombstonedlcs.audit.value.deletedlcs.audit.value.set
B.27.68 object.key.path-requested #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The registry path as the caller wrote it, before LCS rewrote it. A path
whose first component is the alias CurrentUser is rewritten to the
caller's own key under Users, named by the caller's SID; this field keeps
the original, and object.key.path holds the rewritten path.
The same requested path therefore names different keys for different
callers. Search on object.key.path to find which key was actually
reached.
Carried by:
No event carries this field yet.
B.27.69 object.key.reference-count #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The number of references held on a key's entry in the kernel's key cache, at the moment of the record. Each open handle holds one, so a count that reaches zero is the point at which LCS tells the source it may drop the key.
Carried by:
No event carries this field yet.
B.27.70 object.key.symlink #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether a registry key is a symbolic link to another key. A lookup that
passes through a link key is redirected to its target, named by
object.key.target.guid.
Carried by:
B.27.71 object.key.symlink-depth #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many registry symbolic links a lookup followed before stopping. The walk is bounded by the configured symlink depth limit, so a value at that limit means the lookup was refused for following too many links.
Carried by:
No event carries this field yet.
B.27.72 object.key.target.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The key a registry symbolic link redirects to, by GUID. Necessarily
separate from object.key.guid: a record of a link being followed names
the link and its target at once.
Carried by:
No event carries this field yet.
B.27.73 object.key.target.rsi.slot #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The registry source slot that owns the target of a symbolic link, present
when it differs from the source that owns the link itself. A link that
leads into another source's hive is the registry's equivalent of a
filesystem symlink crossing a trust boundary; see object.key.cross-source.
Carried by:
No event carries this field yet.
B.27.74 object.key.value.digest #
- Type:
bin - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The SHA-256 digest of a registry value's data, so that two records can be
compared to see whether a value changed without either carrying the data.
Recorded in place of the raw bytes, which an event never carries by
default. Equal digests mean equal data; the type is a separate field, so
compare object.key.value.type too.
A digest is not a secret-keeping device. A value drawn from a small set, such as a flag or a short PIN, can be recovered by hashing each candidate and comparing.
Carried by:
B.27.75 object.key.value.digest-previous #
- Type:
bin - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The SHA-256 digest of the data of the value a write replaced or a delete
removed. Equal to object.key.value.digest on a write that stored what
was already there.
Carried by:
B.27.76 object.key.value.length #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The byte length of a registry value's data. Recorded in place of the data itself, which an event never carries by default because values can be large and can hold secrets.
Carried by:
B.27.77 object.key.value.length-previous #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The byte length of the value a write replaced or a delete removed. Read
with object.key.value.length.
Carried by:
B.27.78 object.key.value.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The name of the registry value an operation acted on. The empty string is a legal name, identifying the key's default value, so an empty field and an absent one mean different things: absent means the record concerns no value at all.
Carried by:
B.27.79 object.key.value.type #
- Type:
uint.enum - Values:
0 REG_NONE·1 REG_SZ·2 REG_EXPAND_SZ·3 REG_BINARY·4 REG_DWORD·5 REG_DWORD_BIG_ENDIAN·6 REG_LINK·7 REG_MULTI_SZ·8 REG_RESOURCE_LIST·9 REG_FULL_RESOURCE_DESCRIPTOR·10 REG_RESOURCE_REQUIREMENTS_LIST·11 REG_QWORD·65535 REG_TOMBSTONE - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The type of a registry value, as the number of its REG_* constant.
REG_TOMBSTONE is LCS's own marker for a value deleted within one layer:
it hides the value from lower layers rather than holding data, and appears
only on a write that deletes.
Carried by:
B.27.80 object.key.value.type-previous #
- Type:
uint.enum - Values:
0 REG_NONE·1 REG_SZ·2 REG_EXPAND_SZ·3 REG_BINARY·4 REG_DWORD·5 REG_DWORD_BIG_ENDIAN·6 REG_LINK·7 REG_MULTI_SZ·8 REG_RESOURCE_LIST·9 REG_FULL_RESOURCE_DESCRIPTOR·10 REG_RESOURCE_REQUIREMENTS_LIST·11 REG_QWORD·65535 REG_TOMBSTONE - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The type of the value a write replaced or a delete removed, in the terms
of object.key.value.type. It is the value readers resolved before the
operation, across every layer, so it can come from a different layer from
the one written.
Carried by:
B.27.81 object.key.volatile #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether a registry key is volatile, meaning it lives only in memory and does not survive a reboot.
Carried by:
B.27.82 object.key.volatile-requested #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The volatility a create-key caller asked for. It can differ from
object.key.volatile when the request opened an existing key, whose
volatility a create request does not change.
Carried by:
B.27.83 object.kind #
- Type:
str.enum - Values:
file·process·token·key·socket·ipc·mount-namespace·event-namespace·metric-namespace·log-namespace·eventd-admin·service·job·peinit-system·authd-session-end - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
What sort of thing the operation acted on. Load-bearing rather than
descriptive: it names the table an access mask in the same record decodes
against, and without it access.granted has no defined meaning.
The mask spaces genuinely collide. 0x0001 is FILE_READ_DATA on a file
and PROCESS_TERMINATE on a process; the socket mapping reuses
FILE_WRITE_DATA as a socket's write right.
event-namespace, metric-namespace, log-namespace and eventd-admin
come from eventd's audit context, on access checks eventd asks KACS to make
against its own descriptors. The first three are checks against a
namespace pattern; eventd-admin is eventd's check of whether a caller may
administer it, the administer() check in eventd's query/security.rs.
service, job and peinit-system come from peinit's audit context, on
the access checks peinit makes against its own descriptors: a service's, a
job's, and the descriptor guarding peinit's system-wide operations.
authd-session-end comes from authd's audit context, on its check against
the SessionEndSecurity descriptor of whether a caller may end another
principal's logon session.
mount-namespace is a mount table, checked when a process changes it. A
namespace has no name or identifier a record could carry, so a record names
it by its kind alone.
socket has no identity fields yet, and neither has ipc on an access
check: such a record names the object only by its kind, until those
enforcement points are plumbed to supply one. A change to an IPC object's
descriptor does name it, by object.ipc.type and object.ipc.id.
Carried by:
kacs.audit.access.checkedkacs.audit.descriptor.changedkacs.audit.handle.usedkacs.audit.privilege.usedkacs.caap.sacl.skippedkacs.caap.staging.divergedkacs.descriptor.rejectedkacs.impersonation.revertedkacs.impersonation.startedlcs.audit.key.createdlcs.audit.key.deletedlcs.audit.key.descriptor.changedlcs.audit.key.hiddenlcs.audit.key.openedlcs.audit.key.tombstonedlcs.audit.value.deletedlcs.audit.value.set
B.27.84 object.layer.base #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
Whether an operation targeted the base layer, the bottom of every key's resolution order. The base layer always exists at precedence 0 and cannot be disabled.
Carried by:
No event carries this field yet.
B.27.85 object.layer.enabled #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
Whether a registry layer is enabled, after the change the record describes. A disabled layer takes no part in resolution: its values exist but no reader sees them.
Carried by:
No event carries this field yet.
B.27.86 object.layer.enabled-previous #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
Whether a registry layer was enabled before the change the record
describes. Read with object.layer.enabled: a record where the two differ
is a layer being switched on or off.
Carried by:
No event carries this field yet.
B.27.87 object.layer.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The name of the registry layer an operation acted on, as written when the
layer was created. Layer names are compared without regard to case, so
two records whose names differ only in case name the same layer. The base
layer is named base.
Carried by:
B.27.88 object.layer.precedence #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
A registry layer's precedence, after the change the record describes. Where two layers define the same value, the one with the higher precedence wins, and the higher sequence breaks a tie. This number therefore decides which value the whole system sees.
Carried by:
No event carries this field yet.
B.27.89 object.layer.precedence-previous #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
A registry layer's precedence before the change the record describes.
Read with object.layer.precedence: raising a layer above another
silently changes which of their values every reader resolves.
Carried by:
No event carries this field yet.
B.27.90 object.layer.resolution-changed #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
Whether a change to a layer altered what any reader of the registry actually resolves, as opposed to changing a layer whose values were already outranked. LCS computes this and discards it today, so it is not yet emitted on any record.
Carried by:
No event carries this field yet.
B.27.91 object.log-namespace.pattern #
- Type:
str - Asserted: yes
- Carried in: the payload
- Defined in:
eventd.evman
The log-store namespace pattern whose security descriptor eventd checked a
caller's access against. As with object.event-namespace.pattern, it is the
pattern the descriptor was written for, not the log origin the caller asked
about: eventd tries the origin, then each shorter dotted prefix, then *,
and uses the first that has a descriptor.
An origin may name a producer within a service after a /, such as
jobs/<guid>, and that part is never a pattern: the walk starts at the
service. A service's hooks, reloads and health checks therefore record the
service's own pattern, and a search for the full origin finds none of them.
The value is eventd's claim, copied by the kernel from the check's audit
context, so a record carrying it also carries fields.attestation.userspace.
Carried by:
No event carries this field yet.
B.27.92 object.metric-namespace.pattern #
- Type:
str - Asserted: yes
- Carried in: the payload
- Defined in:
eventd.evman
The metric-namespace pattern whose security descriptor eventd checked a
caller's access against. As with object.event-namespace.pattern, it is the
pattern the descriptor was written for, not the metric name: eventd tries the
name, then each shorter dotted prefix, then *, and uses the first that has
a descriptor.
eventd checks metric namespaces for two different things, reading stored
metrics and publishing a sample, and this field is the same for both. Which
one was checked is in access.requested, not here.
The value is eventd's claim, copied by the kernel from the check's audit
context, so a record carrying it also carries fields.attestation.userspace.
Carried by:
No event carries this field yet.
B.27.93 object.mount.cookie #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The mount instance the operation acted on, as the 64-bit random cookie StrataFS assigns each mount.
Not emitted today. Only the StrataFS ftrace tracepoints carry the cookie, and no superblock identifier exists for other filesystems: their tracepoints carry the superblock's magic number, which names a filesystem type rather than an instance.
Carried by:
No event carries this field yet.
B.27.94 object.mount.cookie-epoch #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The boot a mount cookie belongs to. A cookie read back from an on-disk marker can name a live mount only if it was written in this boot, and this is what decides that.
Carried by:
No event carries this field yet.
B.27.95 object.mount.depth #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
stratafs.evman
The filesystem stacking depth of a StrataFS mount: one more than the
deepest stacking filesystem among its strata. Linux permits a depth of at
most 2, so a mount whose strata are themselves stacked filesystems can be
refused with ELOOP.
Carried by:
No event carries this field yet.
B.27.96 object.mount.fs-type #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The type of filesystem mounted, by name, such as ext4 or tmpfs. Mount
policy is derived from it: the kernel decides from the type alone that a
proc or sysfs mount is unmanaged.
Carried by:
B.27.97 object.mount.options #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
stratafs.evman
The stack option string of a StrataFS mount exactly as the caller supplied
it, whether or not it could be parsed. That is what makes it useful on a
rejected mount: it shows what was asked for, where object.mount.strata
could only show what was understood.
Carried by:
No event carries this field yet.
B.27.98 object.mount.options-previous #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
stratafs.evman
The stack option string a StrataFS mount was created with, recorded on a
remount beside the string the remount supplied in object.mount.options.
A remount may not change the stack, and the test is a byte-for-byte
comparison of the two strings, so a stack that is equivalent but written
differently is refused.
Carried by:
No event carries this field yet.
B.27.99 object.mount.path #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The directory the mount was attached at, as an absolute path.
Carried by:
No event carries this field yet.
B.27.100 object.mount.policy #
- Type:
str.enum - Values:
unmanaged·deny-missing·synthesize-ephemeral·synthesize-persistent - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The KACS mount policy in force, which decides what happens to an object
with no stored descriptor. unmanaged means no access control at all:
every check on the mount is skipped. deny-missing refuses access to an
object without a descriptor. The two synthesize policies build one from
the parent and the mount's template, and differ in whether it is written
back to disk.
Changing a mount from deny-missing to a synthesize policy with a chosen
template makes the template's author the author of every unlabelled
object's access control on that volume.
Carried by:
B.27.101 object.mount.policy-generation #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The mount-policy generation in force. Every policy change increments it, and every descriptor cached under an older generation is then stale, so a change invalidates the cached descriptors of a whole volume at once.
Carried by:
B.27.102 object.mount.policy-previous #
- Type:
str.enum - Values:
unmanaged·deny-missing·synthesize-ephemeral·synthesize-persistent - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The mount policy a change replaced, beside object.mount.policy, the one
it put in force. A move away from deny-missing, or to unmanaged, is the
one to read closely: the first lets objects without a descriptor be
reached, and the second turns access control off for the whole filesystem.
Carried by:
B.27.103 object.mount.policy-requested #
- Type:
str.enum - Values:
unmanaged·deny-missing·synthesize-ephemeral·synthesize-persistent - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The mount policy a caller asked for, beside object.mount.policy. On a
refused change, the policy that stayed in force and the one that was
wanted.
Carried by:
No event carries this field yet.
B.27.104 object.mount.root #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The filesystem root that was captured when the mount was made, and against which every later layer path is resolved. Fixed for the life of the mount, so a mounter in a chroot binds the mount to that chroot's view of the filesystem for good.
Carried by:
No event carries this field yet.
B.27.105 object.mount.strata #
- Type:
str.path[] - Asserted: no
- Carried in: the payload
- Defined in:
stratafs.evman
The configured stack of strata of a StrataFS mount, as an ordered list of
directory paths. Index 0 is the highest-precedence stratum: where several
strata hold the same name, the one with the lowest index provides it. At
most 16 entries. Each entry's position is the index that
source.stratum.index and its relatives refer to.
Carried by:
No event carries this field yet.
B.27.106 object.operation.duration #
- Type:
uint.duration - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
How long the operation lasted, in nanoseconds, from when it was requested to when it ended. Measured from the request, not from when it started running, so it includes any time it spent pending behind other work.
Carried by:
B.27.107 object.operation.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The peinit operation the event is about: a start, stop, restart, reload or reset of one service, from the moment it is requested to the moment it ends. The GUID is a UUIDv7.
Not the generic operation domain. operation.name and its
siblings describe what any event attempted; object.operation is a
thing peinit manages, and a search for one does not find the other.
Carried by:
peinit.graph.operation.endedpeinit.job.createdpeinit.job.endedpeinit.job.startedpeinit.operation.endedpeinit.operation.mergedpeinit.operation.requestedpeinit.operation.started
B.27.108 object.operation.merged-into.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The operation that absorbed this one. A merged operation's work is done, or not, by the operation named here, so follow it to learn the outcome.
Carried by:
B.27.109 object.operation.source #
- Type:
str.enum - Values:
admin·boot·shutdown·dependency-propagation·restart-policy·timer·binds-to-recovery·binds-to-propagation·conflict-resolution·on-failure·tty-release - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
Who or what asked for the operation. admin is the only source a
client's command produces; every other source is peinit acting for its
own reasons. An admin operation is the one to tie to a person, through
the access check that admitted the command.
Carried by:
B.27.110 object.operation.state #
- Type:
str.enum - Values:
pending·running·completed·failed·merged·cancelled·aborted - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The operation's state after the event. completed, failed, merged,
cancelled and aborted are terminal. cancelled means it ended while
still pending and never ran; aborted means it ended while running;
merged means another operation absorbed it, named in
object.operation.merged-into.guid.
Carried by:
B.27.111 object.operation.type #
- Type:
str.enum - Values:
start·stop·restart·reload·reset - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
What the operation does to its service.
Carried by:
B.27.112 object.package.architecture #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peipkg.evman
The architecture of the package an event is about: an identifier of PSPU
§5.8, such as x86_64, aarch64 or noarch. noarch is a real
architecture, not the absence of one. Absent when the plan did not carry
one, as for a package being uninstalled.
Carried by:
B.27.113 object.package.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peipkg.evman
The name of the package the event is about. A package's name is its
identity across every repository that serves it (PSPU §5.3), so the same
package from two repositories has the same value here;
source.repository.name says which one an install drew from.
A transaction that touches several packages writes one record for each, so every record names at most one package.
Carried by:
peipkg.action.authorisedpeipkg.package.installedpeipkg.package.uninstalledpeipkg.package.upgraded
B.27.114 object.package.version #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peipkg.evman
The version of the package an event is about: the version installed, the version an upgrade or downgrade moved to, or the version an uninstall removed. A version of PSPU §5.5, compared by PSPU §5.6.
Carried by:
B.27.115 object.package.version-previous #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peipkg.evman
The version a package moved from, beside object.package.version, on an
upgrade, downgrade or undo. Absent on a fresh install and on an uninstall,
which replace no version.
A -previous variant, which PGSS §6.4 permits without a definition. It is
defined anyway so that the Events book generator, which does not apply that
allowance, can resolve it.
Carried by:
B.27.116 object.process.core-dump #
- Type:
str.enum - Values:
disable·user·root - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the process may dump core, as Linux's dumpable setting: disable
forbids a dump, user dumps as the process's user, and root dumps
readable only by root. The kernel forces disable on a PIP-protected
process at exec, because a core file is a copy of the memory the protection
exists to keep private.
Carried by:
No event carries this field yet.
B.27.117 object.process.core-dump-previous #
- Type:
str.enum - Values:
disable·user·root - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The core-dump setting before the operation changed it. The pair is the whole content of a record that hardens a process at exec.
Carried by:
No event carries this field yet.
B.27.118 object.process.executable #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The executable of the process the operation acted on, resolved at exec with symbolic links followed.
Carried by:
No event carries this field yet.
B.27.119 object.process.exit-code #
- Type:
int - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The exit code of the job's process, as it passed it to exit. Present
only when the process exited of its own accord; a process ended by a
signal has object.process.exit-signal instead, and never both.
A non-zero code is not necessarily a failure, because a definition may
name other codes as success. Whether the job failed is
object.job.state.
Carried by:
B.27.120 object.process.exit-signal #
- Type:
int - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The number of the signal that ended the job's process. Present only when
a signal ended it; a process that exited has object.process.exit-code
instead, and never both. A signal peinit sent to stop the job, such as
SIGTERM or SIGKILL, appears here like any other.
Carried by:
B.27.121 object.process.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The durable GUID of the process the operation acted on. Distinct from
emitter.process.guid — one record carries both when a process opens
another process.
Carried by:
kacs.audit.access.checkedkacs.audit.descriptor.changedkacs.audit.privilege.usedkacs.caap.sacl.skippedkacs.caap.staging.diverged
B.27.122 object.process.mitigations #
- Type:
uint.flags - Values:
0x1 WXP·0x2 TLP·0x4 LSV·0x8 CFI·0x10 UI_ACCESS·0x20 NO_CHILD·0x40 CFIF·0x80 CFIB·0x100 PIE·0x200 SML - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The process mitigations in force after the operation, from
uapi/pkm/psb.h. Applying mitigations only ever adds bits to the set.
CFI is a legacy alias for CFIF and CFIB together and is never set in
this field, and UI_ACCESS is reserved.
Carried by:
No event carries this field yet.
B.27.123 object.process.mitigations-added #
- Type:
uint.flags - Values:
0x1 WXP·0x2 TLP·0x4 LSV·0x8 CFI·0x10 UI_ACCESS·0x20 NO_CHILD·0x40 CFIF·0x80 CFIB·0x100 PIE·0x200 SML - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The bits this operation newly turned on. Requesting a mitigation already in force is not an error, so this can be empty on a successful request.
Carried by:
No event carries this field yet.
B.27.124 object.process.mitigations-normalised #
- Type:
uint.flags - Values:
0x1 WXP·0x2 TLP·0x4 LSV·0x8 CFI·0x10 UI_ACCESS·0x20 NO_CHILD·0x40 CFIF·0x80 CFIB·0x100 PIE·0x200 SML - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The request after the kernel normalised it: the CFI alias replaced by
CFIF and CFIB. A request naming an unknown bit, or a CFI half the CPU
does not support, fails normalisation and is refused outright.
Carried by:
No event carries this field yet.
B.27.125 object.process.mitigations-requested #
- Type:
uint.flags - Values:
0x1 WXP·0x2 TLP·0x4 LSV·0x8 CFI·0x10 UI_ACCESS·0x20 NO_CHILD·0x40 CFIF·0x80 CFIB·0x100 PIE·0x200 SML - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The mitigations the caller asked for, as written. Compare against
object.process.mitigations-normalised to see how the kernel read the
request.
Carried by:
No event carries this field yet.
B.27.126 object.process.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The kernel's name (comm) for the process the operation acted on. Lossy
and truncated to 15 bytes; two processes can share a name.
Carried by:
No event carries this field yet.
B.27.127 object.process.parent.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The durable GUID of the parent of a newly created process. Correlate on
this rather than object.process.parent.pid.
Carried by:
No event carries this field yet.
B.27.128 object.process.parent.pid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The process ID of the parent of a newly created process. A fork record is
written by the process that called fork or clone, so this usually
equals emitter.process.pid; it differs under CLONE_PARENT, which makes
the child a sibling of its creator.
Carried by:
No event carries this field yet.
B.27.129 object.process.pid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The process ID of the process the operation acted on. As with
emitter.process.pid, correlate durably on the GUID instead.
Carried by:
kacs.audit.access.checkedkacs.audit.privilege.usedkacs.caap.sacl.skippedkacs.caap.staging.divergedpeinit.job.endedpeinit.job.started
B.27.130 object.process.pip.trust #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The PIP trust level of the process the operation acted on. Compared against
the caller's subject.pip.trust for dominance: a caller whose trust does
not dominate this is denied before the DACL is reached.
Carried by:
No event carries this field yet.
B.27.131 object.process.pip.type #
- Type:
uint.enum - Values:
0 None·512 Protected·1024 Isolated - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The Protected Isolated Process type of the process the operation acted on.
Compared against the caller's subject.pip.type for dominance, so a record
carrying both shows which side of the boundary each stood on.
Carried by:
No event carries this field yet.
B.27.132 object.process.shares-security #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether a newly created task shares its creator's security state rather
than receiving its own copy. True for a new thread (CLONE_THREAD), which
shares one process state and therefore one process GUID; false for a fork,
which gets a new process state and a new GUID.
Carried by:
No event carries this field yet.
B.27.133 object.process.threads #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many threads the process had when the operation ran. Read with
object.process.threads-switched.
Carried by:
No event carries this field yet.
B.27.134 object.process.threads-switched #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many of the process's threads took on a new identity in the operation.
A primary token install applies to every thread; a value below
object.process.threads means some threads still run under the old token,
a process split between two identities.
Carried by:
No event carries this field yet.
B.27.135 object.process.token-previous.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The durable GUID of the primary token a token install displaced. With
object.process.token.guid, the before and after of the process's identity.
Carried by:
No event carries this field yet.
B.27.136 object.process.token-previous.sid #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The user SID of the token a token install displaced. Comparing it against the new token's SID is how a record shows a process changing which principal it runs as, rather than only replacing its token.
Carried by:
No event carries this field yet.
B.27.137 object.process.token.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The durable GUID of the primary token the process holds after the
operation. Carried on a token install, where it names the token the process
now runs as; the token it replaced is object.process.token-previous.guid.
Carried by:
No event carries this field yet.
B.27.138 object.repository.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peipkg.evman
The repository the event is about, by the name it is configured under.
The name is local configuration, not an identity: a repository is
identified by its base URL, in object.repository.url, and two systems
may call one repository by different names.
Carried by:
peipkg.action.authorisedpeipkg.repository.addedpeipkg.repository.reconfiguredpeipkg.repository.removed
B.27.139 object.repository.url #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peipkg.evman
The base URL of the repository the event is about, which is what
identifies it (PSPU §5.36). An HTTPS URL unless the repository was
given an insecure-transport allowance, so an http: value here marks a
repository whose indexes are fetched without transport security.
Carried by:
B.27.140 object.sd.cache-state #
- Type:
str.enum - Values:
valid·missing·corrupt - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The state of the descriptor KACS holds cached for the inode. Anything but
valid means the normal check could not run against the object's own
descriptor; what happened instead depends on the mount policy, and a
caller with SeRestorePrivilege can replace a missing or corrupt one
outright.
Carried by:
No event carries this field yet.
B.27.141 object.sd.components #
- Type:
uint.flags - Values:
0x1 OWNER·0x2 GROUP·0x4 DACL·0x8 SACL·0x10 LABEL - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Which parts of the descriptor the operation addressed, as the
KACS_SECINFO_* bits of uapi/pkm/sd.h. Each part is guarded by a
different right — WRITE_OWNER for the owner, WRITE_DAC for the DACL,
ACCESS_SYSTEM_SECURITY for the SACL — so a record about a descriptor
change is read with this.
Carried by:
B.27.142 object.sd.digest #
- Type:
bin - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The SHA-256 digest of the object's security descriptor in its binary form, so that two records can be compared for "same descriptor" without either carrying the descriptor itself.
Carried by:
B.27.143 object.sd.digest-previous #
- Type:
bin - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The digest of the descriptor a change replaced. Equal to object.sd.digest
on a change that rewrote the descriptor without altering it.
Carried by:
B.27.144 object.sd.length #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The byte length of the object's security descriptor. Where a record names two descriptors, this is the object's own.
Carried by:
kacs.audit.descriptor.changedkacs.descriptor.rejectedlcs.audit.key.createdlcs.audit.key.descriptor.changed
B.27.145 object.sd.length-previous #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The byte length of the descriptor a change replaced.
Carried by:
B.27.146 object.sd.mount-generation #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The mount-policy generation stamped on the cached descriptor when it was
cached. Compared with object.mount.policy-generation: a descriptor from an
older generation is stale and is discarded on next use.
Carried by:
No event carries this field yet.
B.27.147 object.sd.owner #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The owner SID on the object's security descriptor. Not the token's default
owner, object.token.default-owner, which is the owner a token gives the
objects it creates.
Carried by:
B.27.148 object.sd.owner-previous #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The owner SID a change replaced. Taking ownership is how a principal gains the right to rewrite an object's DACL, so the pair is the record of who lost control of the object to whom.
Carried by:
B.27.149 object.sd.provenance #
- Type:
str.enum - Values:
xattr·missing·synthetic·synthetic-pending·corrupt·stratafs-root - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Where the cached descriptor came from. xattr was read from the object's
stored descriptor; synthetic was built from the parent and the mount's
template; synthetic-pending was synthesised on a persistent mount and has
not yet been written back; stratafs-root is a StrataFS root's descriptor,
read through the outer inode and re-read on every check. A different
vocabulary from object.sd.cache-state, held beside it on the same entry.
Carried by:
No event carries this field yet.
B.27.150 object.sd.synthesis-depth #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many ancestor directories a descriptor synthesis walked. An object with no stored descriptor inherits from its parent, which may itself need synthesising, so the walk climbs until it finds a labelled ancestor.
Carried by:
No event carries this field yet.
B.27.151 object.sd.synthesis-depth-limit #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The ceiling on object.sd.synthesis-depth, 32 levels in this kernel. A
synthesis that reaches it fails closed: no descriptor is built and the
access is refused, however the object's ancestors are labelled.
Carried by:
No event carries this field yet.
B.27.152 object.service.conflict.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The name of the service this one conflicts with, on a finding that two
services both triggered at boot declare a conflict and so cannot both
start. Which of the pair is object.service.name and which is this field
carries no meaning: the conflict is mutual.
Carried by:
B.27.153 object.service.dependency.kind #
- Type:
str.enum - Values:
requires·wants·binds-to - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
How the service depends on object.service.dependency.name. requires
and binds-to are hard: the service cannot start without the dependency.
binds-to is also tied to it afterwards, and stops and recovers with it.
Carried by:
B.27.154 object.service.dependency.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The name of the dependency a graph finding is about: the service this
one requires and cannot have, or the one whose block has blocked it.
Read object.service.dependency.kind for how this service depends on it.
The name is as the definition wrote it. A missing-dependency finding names a service that does not exist, so do not expect to find any other record about it.
Carried by:
B.27.155 object.service.dependents #
- Type:
str[] - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The services that hard-depend on this one, by name. Hard dependencies are
requires and binds-to; a wants dependent is not listed, because it
starts whether or not this service does.
Carried by:
B.27.156 object.service.failed #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
Whether an internal error in peinit failed the service it occurred on. False means peinit contained the error without changing the service's state. Either way the fault was peinit's, not the service's.
Carried by:
B.27.157 object.service.generation #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The service's activation generation: a counter peinit advances each time
the service enters starting, so that every incarnation of a service has
its own number. Records with the same object.service.name and the same
generation describe one run of the service.
The same counter as object.job.activation-generation, seen from the
service rather than from one of its jobs. It is held in memory and starts
again when peinit does, so it orders runs within one boot only.
Carried by:
B.27.158 object.service.health-check.interval #
- Type:
uint.duration - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The configured interval between health checks, in nanoseconds. Configured in whole seconds, so it is always a whole number of seconds.
Carried by:
B.27.159 object.service.health-check.restart-window #
- Type:
uint.duration - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The configured restart window of a health check, in nanoseconds.
Configured in whole seconds, so it is always a whole number of seconds.
A validation finding carries it beside object.service.health-check.interval
and object.service.health-check.retries, because it is their
combination that a definition gets wrong.
Carried by:
B.27.160 object.service.health-check.retries #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The configured number of consecutive health-check failures at which peinit stops reporting the service unhealthy and acts on it. Failures short of this count change nothing but the service's health.
Carried by:
B.27.161 object.service.name #
- Type:
str - Asserted: yes
- Carried in: the payload
- Defined in:
peinit.evman
The name of the service the event is about, as its definition names it. The same value peinit passes as the object name in the KACS audit context when it checks a caller's access to a service, so a peinit record and the access-check record for the same command join on it.
On a kacs.audit.access.checked record the value is peinit's claim, copied
by the kernel from the check's audit context {kind: "service", service: {name}}, so that record also carries fields.attestation.userspace.
A name, not a durable identity: a service removed and later redefined
under the same name is a different service with the same
object.service.name. Read object.service.generation beside it to tell
one activation from the next.
Carried by:
authd.service.attestedpeinit.boot.downgradedpeinit.cgroup.leakedpeinit.critical-service.failedpeinit.event.droppedpeinit.fd-store.rejectedpeinit.graph.operation.endedpeinit.graph.validation.failedpeinit.graph.validation.warnedpeinit.internal-error.containedpeinit.job.createdpeinit.job.endedpeinit.job.startedpeinit.on-failure.suppressedpeinit.operation.endedpeinit.operation.mergedpeinit.operation.requestedpeinit.operation.startedpeinit.service.abandonedpeinit.service.reload.timed-out
B.27.162 object.service.on-failure-chain #
- Type:
str[] - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The OnFailure handlers already started in the chain that peinit cut short, in the order they were started. A chain is cut when the next handler is already in it, which is a loop, or when it has reached sixteen handlers.
Carried by:
B.27.163 object.service.on-failure.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The OnFailure handler peinit declined to start, because starting it would have looped or gone too deep. The failure the handler was meant to handle went unhandled, so look for it here when a handler did not run.
Carried by:
B.27.164 object.service.state #
- Type:
str.enum - Values:
inactive·starting·active·reloading·stopping·completed·backoff·failed·abandoned·skipped - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The service's state after the event, in the vocabulary of the control
channel (PSPU §4.B). Exactly four states satisfy dependents: active,
reloading, completed and skipped. Use that set and no other when
asking whether something depending on this service could be running.
abandoned means a process survived SIGKILL and is still there,
unkillably. It is not a stopped service.
Carried by:
B.27.165 object.service.state-previous #
- Type:
str.enum - Values:
inactive·starting·active·reloading·stopping·completed·backoff·failed·abandoned·skipped - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The service's state before the event, with the same values as
object.service.state. The two together are the transition; a record
carrying only one of them says where the service is, not how it got
there.
Carried by:
B.27.166 object.service.timer.schedule #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The service's timer schedule, exactly as its definition wrote it. On a finding that the schedule does not parse, this is the text that failed, so it may not be a valid schedule at all.
Carried by:
B.27.167 object.service.transition-cause #
- Type:
str.enum - Values:
explicit-start·dependency-start·restart-policy·binds-to-recovery·timer·explicit-stop·explicit-reload·explicit-reset·conflict-eviction·binds-to-propagation·shutdown-wave·process-crash·clean-exit·clean-exit-restart·readiness-timeout·watchdog-timeout·health-check-failure·pre-hook-failure·parent-setup-failure·pre-exec-failure·dependency-failure·restart-budget-exhausted·cycle-detected·validation-error·assertion-error·condition-skipped·tty-unavailable·process-unkillable·internal-error - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
Why the service's state changed. The causes beginning explicit- follow
an operator's command; the rest are peinit's own reasons, and an operator
reading one has learned what the manager did, not what anyone asked for.
internal-error is a fault in peinit, not in the service. The
service was failed because peinit could not carry out a step on its
behalf; the service itself may have been healthy.
Carried by:
B.27.168 object.service.type #
- Type:
str.enum - Values:
simple·oneshot - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
How the service's main process is supervised. A simple service is up
while its process runs; a oneshot service runs to completion and is
then completed. Health checks are scheduled for simple services only,
which is why a validation finding names the type.
Carried by:
B.27.169 object.session.auth-package #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The name of the authentication package that established the session, as the creator supplied it. Not interpreted by the kernel.
Carried by:
B.27.170 object.session.id #
- Type:
uint.luid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The LUID of the logon session the operation acted on. The same value as
subject.token.auth-id and object.token.auth-id on records about tokens
in that session, which is how a session's records are joined to everything
done under it.
Carried by:
B.27.171 object.session.logon-time #
- Type:
uint.time - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
When the session was established.
Carried by:
B.27.172 object.session.logon-type #
- Type:
str.enum - Values:
interactive·network·batch·service·network-cleartext·new-credentials·remote-interactive - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
What kind of logon the session represents, from the KACS_LOGON_TYPE_*
values in uapi/pkm/token.h. The kernel refuses to create a session of any
other type.
Carried by:
B.27.173 object.session.reference-count #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many live tokens or other references the session still holds. A session is destroyed when this reaches zero; on a refused destroy, a non-zero count is why.
Carried by:
No event carries this field yet.
B.27.174 object.session.sid #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The logon SID the kernel synthesised for the session. Derived from
object.session.id, but it appears in descriptors as a principal in its own
right, so a search by SID has to find it here.
Carried by:
No event carries this field yet.
B.27.175 object.session.user.sid #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The user SID of the principal the logon session belongs to: whose session
it is. Not object.session.sid, which is the logon SID synthesised for the
session itself; a search for what one user's sessions did matches this
field, and a search for one session matches that one.
Carried by:
B.27.176 object.socket.address #
- Type:
str.ip - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The address a listening socket is bound to. A wildcard address (0.0.0.0
or ::) is a legitimate value: it means every address on the machine.
Carried by:
No event carries this field yet.
B.27.177 object.socket.id #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The identity of the socket the operation acted on.
Not emitted today. Peios publishes no stable socket identifier; the kernel's inode number and pointer are both unsuitable, so records name a socket only by its kind and, for a listener, its address and port.
Carried by:
No event carries this field yet.
B.27.178 object.socket.identity-dropped #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether a conveyed identity was dropped because the receiver's control buffer was too small to hold it. The message is delivered either way, so true means the receiver got data with no identity attached and could not tell.
Carried by:
No event carries this field yet.
B.27.179 object.socket.impersonation-limit #
- Type:
uint.enum - Values:
0 Anonymous·1 Identification·2 Impersonation·3 Delegation - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The highest impersonation level the socket will convey to a peer.
Raising it to Impersonation or Delegation lets a receiver act as the
sender, not merely identify it.
Carried by:
No event carries this field yet.
B.27.180 object.socket.options #
- Type:
uint.flags - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The options of a listening socket: whether it is one of an SO_REUSEPORT
group, whether an IPv6 socket refuses IPv4-mapped traffic, and whether a
bound UDP socket is also connected to one peer. The kernel reports these
today as three separate booleans, and no bit assignment for them is
defined yet.
Carried by:
No event carries this field yet.
B.27.181 object.socket.port #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The port a listening socket is bound to. A listener has one port, which is
neither a source nor a destination, so it is not source.port or
destination.port; a search for a port number should cover all three.
Carried by:
No event carries this field yet.
B.27.182 object.socket.principal-kind #
- Type:
str.enum - Values:
unstamped·program·kernel - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
What kind of principal governs the socket for network policy. program is
a process, whose token and process facts were stamped on the socket;
kernel is a kernel socket, with no token. unstamped is a socket that
was never stamped, so network policy has nobody to attribute its traffic
to.
Carried by:
No event carries this field yet.
B.27.183 object.socket.reservation #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The port-reservation selector a bind matched, naming the reservation whose descriptor decided whether the caller could bind the port. Peios replaces the privileged-port floor with these reservations entirely, so this is the rule that let a process listen on its port.
Carried by:
No event carries this field yet.
B.27.184 object.socket.state #
- Type:
str.enum - Values:
free·unconnected·connecting·connected·disconnecting - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The socket's connection state, from the Linux SS_* value with its prefix
removed and folded to lower case. A listening socket reads unconnected
here: listening is a transport state, not a connection state.
Carried by:
No event carries this field yet.
B.27.185 object.socket.token-delivered #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the conveyed token actually reached the receiver. False means the message arrived and the identity did not.
Carried by:
No event carries this field yet.
B.27.186 object.socket.token-derived #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the token conveyed on a send was derived from the sender's token rather than passed as is, so the receiver did not get the sender's token itself.
Carried by:
No event carries this field yet.
B.27.187 object.socket.token-passing #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether automatic per-send token passing is on for the socket: whether every send carries the sender's identity to the receiver without being asked.
Carried by:
No event carries this field yet.
B.27.188 object.socket.type #
- Type:
str.enum - Values:
stream·dgram·raw·rdm·seqpacket - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The socket's type, from the Linux SOCK_* value with its prefix removed
and folded to lower case.
Carried by:
No event carries this field yet.
B.27.189 object.thread.tid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
A thread the operation acted on, where it is not the emitting thread: the target of a per-thread impersonation or of a thread-token open.
Carried by:
No event carries this field yet.
B.27.190 object.token.auth-id #
- Type:
uint.luid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The LUID of the logon session the token the operation acted on belongs to.
The same value as object.session.id on that session's own records.
Carried by:
B.27.191 object.token.capabilities #
- Type:
bin.sid[] - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The confinement capability SIDs (S-1-15-3-…) of the token the operation
acted on. Unrelated to Linux capabilities.
Carried by:
No event carries this field yet.
B.27.192 object.token.default-owner #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The owner SID the token stamps on objects it creates. Changing it changes who owns everything the token makes from then on, which is why a default adjustment is worth a record.
Carried by:
No event carries this field yet.
B.27.193 object.token.elevation #
- Type:
str.enum - Values:
default·full·limited - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The token's role in a split elevation pair. full is the elevated half
and limited the filtered one; default is a token that belongs to no
pair. The other half of a pair is object.token.linked.guid.
Carried by:
No event carries this field yet.
B.27.194 object.token.gid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The Linux GID the token the operation acted on projects onto.
Carried by:
No event carries this field yet.
B.27.195 object.token.group-attributes #
- Type:
uint.flags[] - Values:
SE_GROUP_MANDATORY·SE_GROUP_ENABLED_BY_DEFAULT·SE_GROUP_ENABLED·SE_GROUP_OWNER·SE_GROUP_USE_FOR_DENY_ONLY·SE_GROUP_INTEGRITY·SE_GROUP_INTEGRITY_ENABLED·SE_GROUP_RESOURCE·SE_GROUP_LOGON_ID - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Per-group attribute bitmasks of the token the operation acted on,
positionally parallel to object.token.groups: groups[i] is described by
group-attributes[i]. A group adjustment changes these and not the SIDs,
so a record of one is read here.
Carried by:
No event carries this field yet.
B.27.196 object.token.groups #
- Type:
bin.sid[] - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The group SIDs of the token the operation acted on. Read with
object.token.group-attributes, which says which of them are enabled.
Carried by:
No event carries this field yet.
B.27.197 object.token.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The durable GUID of the token the operation acted on. Distinct from
emitter.token.guid — one record carries both when a token is used to
open or adjust another token.
Carried by:
kacs.audit.access.checkedkacs.audit.descriptor.changedkacs.audit.privilege.usedkacs.caap.sacl.skippedkacs.caap.staging.divergedkacs.impersonation.revertedkacs.impersonation.started
B.27.198 object.token.id #
- Type:
uint.luid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The LUID of the token the operation acted on, for token-open and token-adjust checks.
Carried by:
authd.logon.attemptedauthd.service.attestedkacs.audit.access.checkedkacs.audit.descriptor.changedkacs.audit.privilege.usedkacs.caap.sacl.skippedkacs.caap.staging.divergedkacs.impersonation.started
B.27.199 object.token.impersonation #
- Type:
uint.enum - Values:
0 Anonymous·1 Identification·2 Impersonation·3 Delegation - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The impersonation level of the token the operation acted on. A primary
token reports 0, so read object.token.type before reading 0 as
Anonymous.
Carried by:
B.27.200 object.token.impersonation-permitted #
- Type:
uint.enum - Values:
0 Anonymous·1 Identification·2 Impersonation·3 Delegation - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The impersonation level a gate computed as permitted. Where it is below
object.token.impersonation, the level was silently reduced: the caller
lacked the privilege to impersonate at the full level, or the client's
integrity was higher than the caller's. The operation still succeeds, at
the lower level.
Carried by:
B.27.201 object.token.integrity #
- Type:
uint.integrity - Values:
0 Untrusted·4096 Low·8192 Medium·12288 High·16384 System - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The integrity level of the token the operation acted on, as an integrity RID.
Carried by:
B.27.202 object.token.interactive-session #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The interactive-session scope the token is bound to, which separates the interactive sessions of one machine from each other. 0 means the token is bound to no interactive session.
Carried by:
No event carries this field yet.
B.27.203 object.token.linked.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The durable GUID of the other token in a linked elevation pair. Carried where two tokens are linked, so the record names both halves.
Carried by:
No event carries this field yet.
B.27.204 object.token.primary-group #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The primary-group SID the token stamps on objects it creates, beside
object.token.default-owner.
Carried by:
No event carries this field yet.
B.27.205 object.token.privileges #
- Type:
uint.flags - Values:
0x4 KACS_SE_CREATE_TOKEN_PRIVILEGE·0x8 KACS_SE_ASSIGN_PRIMARY_TOKEN_PRIVILEGE·0x10 KACS_SE_LOCK_MEMORY_PRIVILEGE·0x20 KACS_SE_INCREASE_QUOTA_PRIVILEGE·0x80 KACS_SE_TCB_PRIVILEGE·0x100 KACS_SE_SECURITY_PRIVILEGE·0x200 KACS_SE_TAKE_OWNERSHIP_PRIVILEGE·0x400 KACS_SE_LOAD_DRIVER_PRIVILEGE·0x800 KACS_SE_SYSTEM_PROFILE_PRIVILEGE·0x1000 KACS_SE_SYSTEMTIME_PRIVILEGE·0x2000 KACS_SE_PROFILE_SINGLE_PROCESS_PRIVILEGE·0x4000 KACS_SE_INCREASE_BASE_PRIORITY_PRIVILEGE·0x20000 KACS_SE_BACKUP_PRIVILEGE·0x40000 KACS_SE_RESTORE_PRIVILEGE·0x80000 KACS_SE_SHUTDOWN_PRIVILEGE·0x100000 KACS_SE_DEBUG_PRIVILEGE·0x200000 KACS_SE_AUDIT_PRIVILEGE·0x800000 KACS_SE_CHANGE_NOTIFY_PRIVILEGE·0x1000000 KACS_SE_REMOTE_SHUTDOWN_PRIVILEGE·0x10000000 KACS_SE_MANAGE_VOLUME_PRIVILEGE·0x20000000 KACS_SE_IMPERSONATE_PRIVILEGE·0x100000000 KACS_SE_RELABEL_PRIVILEGE·0x800000000 KACS_SE_CREATE_SYMBOLIC_LINK_PRIVILEGE - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The privileges the token the operation acted on holds, as the flags of its 64-bit privilege word: bit n is the privilege whose LUID is n.
Carried by:
B.27.206 object.token.privileges-enabled #
- Type:
uint.flags - Values:
0x4 KACS_SE_CREATE_TOKEN_PRIVILEGE·0x8 KACS_SE_ASSIGN_PRIMARY_TOKEN_PRIVILEGE·0x10 KACS_SE_LOCK_MEMORY_PRIVILEGE·0x20 KACS_SE_INCREASE_QUOTA_PRIVILEGE·0x80 KACS_SE_TCB_PRIVILEGE·0x100 KACS_SE_SECURITY_PRIVILEGE·0x200 KACS_SE_TAKE_OWNERSHIP_PRIVILEGE·0x400 KACS_SE_LOAD_DRIVER_PRIVILEGE·0x800 KACS_SE_SYSTEM_PROFILE_PRIVILEGE·0x1000 KACS_SE_SYSTEMTIME_PRIVILEGE·0x2000 KACS_SE_PROFILE_SINGLE_PROCESS_PRIVILEGE·0x4000 KACS_SE_INCREASE_BASE_PRIORITY_PRIVILEGE·0x20000 KACS_SE_BACKUP_PRIVILEGE·0x40000 KACS_SE_RESTORE_PRIVILEGE·0x80000 KACS_SE_SHUTDOWN_PRIVILEGE·0x100000 KACS_SE_DEBUG_PRIVILEGE·0x200000 KACS_SE_AUDIT_PRIVILEGE·0x800000 KACS_SE_CHANGE_NOTIFY_PRIVILEGE·0x1000000 KACS_SE_REMOTE_SHUTDOWN_PRIVILEGE·0x10000000 KACS_SE_MANAGE_VOLUME_PRIVILEGE·0x20000000 KACS_SE_IMPERSONATE_PRIVILEGE·0x100000000 KACS_SE_RELABEL_PRIVILEGE·0x800000000 KACS_SE_CREATE_SYMBOLIC_LINK_PRIVILEGE - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Which of the privileges in object.token.privileges are enabled, as flags
in the same bit layout. A privilege adjustment changes this, not the held set.
Carried by:
No event carries this field yet.
B.27.207 object.token.restricted #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the token is restricted: whether it carries restricting SIDs that every access check must also satisfy.
Carried by:
B.27.208 object.token.restricting-sids #
- Type:
bin.sid[] - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The restricting SIDs on a filtered token: the second list an access check must also satisfy before a restricted token is granted anything. Carried where a token is filtered, or where a filtered token is acted on.
Carried by:
No event carries this field yet.
B.27.209 object.token.sid #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The user SID of the token the operation acted on. On a record where one
principal handles another's token — duplicating, filtering or installing
it — this is the principal the token speaks for, and subject.token.sid
the one who handled it.
Carried by:
B.27.210 object.token.supplementary-gid-count #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many supplementary GIDs the token's Linux projection carries. A token
naming more groups than Linux can hold fails projection with -E2BIG, and
this is the count that was too many.
Carried by:
No event carries this field yet.
B.27.211 object.token.type #
- Type:
str.enum - Values:
primary·impersonation - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the token the operation acted on is a primary token or an
impersonation token. As with subject.token.type, needed to read
object.token.impersonation: a primary token also reports 0 there.
Carried by:
B.27.212 object.token.uid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The Linux UID the token the operation acted on projects onto. A projection for interoperability, never the identity KACS decides on.
Carried by:
No event carries this field yet.
B.27.213 object.token.user-deny-only #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the token's user SID is deny-only: it matches deny ACEs but never allow ACEs, so the token gets nothing through its own user identity.
Carried by:
No event carries this field yet.
B.27.214 object.token.write-restricted #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the token is write-restricted: its restricting SIDs apply only to write access, and reads are decided as for an unrestricted token.
Carried by:
No event carries this field yet.
B.27.215 object.watch.depth-limit #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The maximum depth beneath its key at which a subtree watch reports
changes. 0 means unbounded, and 0 is the default. The limit is the
machine-wide MaxSubtreeWatchDepth setting rather than anything the
watcher chose, so changes deeper than it are suppressed for every subtree
watch at once.
Carried by:
No event carries this field yet.
B.27.216 object.watch.effects #
- Type:
uint.flags - Values:
0x1 layer-delete - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The internal effects that a registry commit triggered through the
kernel's own configuration watches. layer-delete means the watch
dispatch for the commit already handled a layer deletion, so the commit
skips its own separate layer-delete step. Absent when the commit
triggered none.
Carried by:
No event carries this field yet.
B.27.217 object.watch.event #
- Type:
str.enum - Values:
value-set·value-deleted·subkey-created·subkey-deleted·sd-changed·key-deleted·overflow - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The type of a registry watch event, named from its REG_WATCH_* constant
with the prefix removed. overflow replaces individual events when they
could not all be delivered, telling the watcher only that it missed
something and must re-read the key.
An overflow does not always mean a full queue. It is also queued when a
single event could not be encoded, so a watcher seeing one cannot tell
which happened.
Carried by:
No event carries this field yet.
B.27.218 object.watch.filter #
- Type:
uint.flags - Values:
0x1 REG_NOTIFY_VALUE·0x2 REG_NOTIFY_SUBKEY·0x4 REG_NOTIFY_SD - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The categories of change a process asked a registry watch to report: value changes, subkey creation and deletion, and security descriptor changes. A watcher is told only of changes in the categories it set.
Carried by:
No event carries this field yet.
B.27.219 object.watch.subtree #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
Whether a registry watch covers the whole subtree beneath its key rather
than the key alone. How deep a subtree watch reaches is
object.watch.depth-limit, and that limit is unbounded by default.
Carried by:
No event carries this field yet.
B.27.220 object.xattr.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The name of an extended attribute the operation acted on, in full with its
namespace prefix, such as security.capability. The names Peios reserves
for its own descriptors and markers are refused or hidden whatever the
caller's access.
Carried by:
No event carries this field yet.
Generated from eventd.evman, gxwid.evman, kernel.evman, lcs.evman, lpsd.evman, peinit.evman, peipkg.evman, stratafs.evman, trustd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.28 operation.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under operation, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.28.1 operation.adopted #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether an operation took over an object that already existed in the target stratum instead of creating a new one. An adopted object keeps whatever it already had, so an adoption where a creation was expected is worth reading closely.
Carried by:
No event carries this field yet.
B.28.2 operation.anonymous #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether an object was created through the anonymous path, an unnamed
temporary file (O_TMPFILE) that has no name in any stratum until it is
linked in.
Carried by:
No event carries this field yet.
B.28.3 operation.arguments #
- Type:
uint[] - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The numeric arguments of the command in operation.code, in order, such as
the prctl arguments after the option.
Carried by:
No event carries this field yet.
B.28.4 operation.attempts #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many attempts the operation made before it gave up.
Carried by:
No event carries this field yet.
B.28.5 operation.attributes-copied #
- Type:
uint.flags - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The metadata attributes a copy-up carried across from the original object to the copy. Copy-up carries the modification time, the mode (except on a symbolic link, whose mode the VFS fixes), the owning UID and GID, and the extended attributes other than StrataFS's own. The bit assignments are not yet defined, so this field declares no values until they are.
Carried by:
No event carries this field yet.
B.28.6 operation.available #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many items existed for the operation to return. Read with
operation.returned.
Carried by:
No event carries this field yet.
B.28.7 operation.bytes #
- Type:
uint.bytes - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many bytes the operation moved before it stopped. On a Unix stream socket, a read is cut short where the sender's conveyed identity changes, and this is the only explanation the reader has for a short read.
Carried by:
No event carries this field yet.
B.28.8 operation.clone-flags #
- Type:
uint.flags - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The Linux CLONE_* flags a new task was created with. CLONE_THREAD
decides whether the new task shares its creator's security state, which
object.process.shares-security records.
Carried by:
No event carries this field yet.
B.28.9 operation.code #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The command or opcode within the operation, such as an ioctl command or a
prctl option. Its vocabulary depends on operation.name, so the two are
read together.
Carried by:
No event carries this field yet.
B.28.10 operation.compat #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the call came in through the 32-bit compatibility entry point.
Carried by:
No event carries this field yet.
B.28.11 operation.contents-copied #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether a copy-up copied an object's contents or only created the node. Always false for a directory, whose entries are not copied. Read with the object's type before assuming a copy-up moved any data.
Carried by:
No event carries this field yet.
B.28.12 operation.copy-up-generation #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The generation of the current copy-up phase. Incremented each time a phase
is armed, so a handle carrying an older generation is stale. Unrelated to
object.mount.policy-generation.
Carried by:
No event carries this field yet.
B.28.13 operation.copy-up-phase #
- Type:
str.enum - Values:
none·source-read·create·populate·publish-link·publish-rename·cleanup·orphan-marker - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The phase of an in-flight StrataFS copy-up. KACS admits the copy-up's own operations without a caller check only while they match the armed phase, so a refusal here is a copy-up that tried something its phase does not allow.
Carried by:
No event carries this field yet.
B.28.14 operation.create-disposition #
- Type:
str.enum - Values:
supersede·open·create·open-if·overwrite·overwrite-if - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
What a native open was told to do about an existing or missing file, from
the KACS_DISPOSITION_* values. supersede and overwrite destroy an
existing file's content; open fails if the file is missing and create
fails if it exists.
Carried by:
No event carries this field yet.
B.28.15 operation.create-options #
- Type:
uint.flags - Values:
0x1 DIRECTORY·0x2 DELETE_ON_CLOSE - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The create options of a native open, from the KACS_CREATE_OPT_* bits.
DELETE_ON_CLOSE schedules the file's removal when the last handle that
requested it is closed.
Carried by:
No event carries this field yet.
B.28.16 operation.decision #
- Type:
str.enum - Values:
primary·parent-fallback·source·dest·delete-existing - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Which sub-decision of a namespace operation the record concerns, from the
KACS_NS_* codes. A single-decision operation reports primary; a delete
refused on the object can still be allowed by DELETE_CHILD on the
parent, parent-fallback; a rename produces up to three records, for the
source, the destination parent, and an existing destination it replaces.
Carried by:
No event carries this field yet.
B.28.17 operation.duration #
- Type:
uint.duration - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How long the operation took.
Carried by:
No event carries this field yet.
B.28.18 operation.entry-point #
- Type:
str.enum - Values:
mount·directory-open·rmdir·staging-lookup·copy-up - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The operation that set off recovery of orphaned staging entries, the
partial copies an interrupted copy-up leaves in the create stratum.
mount sweeps the create stratum's root when the mount is made;
directory-open and rmdir sweep the directory concerned; copy-up
sweeps the destination's parent first; staging-lookup fires when a path
lookup names an entry that looks like a staging name.
staging-lookup is the one an unprivileged process can trigger at will,
by looking up such a name.
Carried by:
No event carries this field yet.
B.28.19 operation.exec-unsafe #
- Type:
uint.flags - Values:
0x1 LSM_UNSAFE_SHARE·0x2 LSM_UNSAFE_PTRACE·0x4 LSM_UNSAFE_NO_NEW_PRIVS - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The kernel's unsafe-exec flags (bprm->unsafe) at an exec. Under
PTRACE or NO_NEW_PRIVS, an exec may not raise the process's trust, and
a signature-derived label is capped instead.
Carried by:
No event carries this field yet.
B.28.20 operation.failed-count #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many items in a batch operation failed. With
operation.succeeded-count, the whole batch; a non-zero count here on a
record whose outcome.success is true is a partial success.
Carried by:
B.28.21 operation.fd #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The file descriptor a bulk operation transferred through — the sink a backup was written to, or the source a restore was read from. Direction is given by the event, not by this field.
Carried by:
B.28.22 operation.gid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The GID a setgid-family call asked for.
Carried by:
No event carries this field yet.
B.28.23 operation.linux-projection #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the Linux identity projection — UID, GID and supplementary groups — was recomputed as part of a token install. False means the process's Linux credentials still reflect the previous token.
Carried by:
No event carries this field yet.
B.28.24 operation.lsm-flags #
- Type:
uint.flags - Values:
0x1 LSM_SETID_ID·0x2 LSM_SETID_RE·0x4 LSM_SETID_RES·0x8 LSM_SETID_FS - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Which variant of a legacy set-identity call was made, as the kernel's
LSM_SETID_* flags: setuid, setreuid, setresuid or setfsuid, and
the gid equivalents. 0 for setgroups.
Carried by:
No event carries this field yet.
B.28.25 operation.name #
- Type:
str.enum - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Which operation was attempted. Each event declares the names its
enforcement point uses, and the sets are disjoint where two points could
be confused: FACS names operations on an already-open handle with a
file. prefix, and StrataFS names its own without one.
Carried by:
kacs.audit.handle.usedkacs.audit.privilege.usedkacs.caap.policy.changedkacs.impersonation.revertedlcs.audit.key.tombstonedlpsd.account.modifiedlpsd.credential.changedlpsd.credential.verifiedpeipkg.action.authorisedstratafs.mutation.refusedtimed.clock.stepped
B.28.26 operation.name-truncated #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether a name in the record could not be carried in full and was cut short. When true, do not search for the name exactly: the full name is longer than what the record holds.
Carried by:
No event carries this field yet.
B.28.27 operation.non-blocking #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether a path walk was in RCU mode, the kernel's non-blocking lookup. StrataFS refuses every RCU-mode step, so the kernel retries the walk in blocking mode; a record with this set is the refusal, not a failure the caller sees.
Carried by:
No event carries this field yet.
B.28.28 operation.open-flags #
- Type:
uint.flags - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The open flags the caller asked for, as the Linux O_* bits of the
flags argument. The rights these imply are what KACS checks;
access.requested-minimum records the least of them.
Carried by:
No event carries this field yet.
B.28.29 operation.open-flags-effective #
- Type:
uint.flags - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The open flags StrataFS used against the stratum below, after rewriting
the flags the caller asked for, which operation.open-flags holds and
whose O_* values decode this one too. Where the object is not written in
place, a write open of a regular file is reopened read-only without
O_APPEND, and O_TRUNC is dropped, until a write copies the object up.
The difference between the two fields is the finding: a caller that asked to truncate a file may hold a descriptor on which nothing has been truncated yet.
Carried by:
No event carries this field yet.
B.28.30 operation.profiling-scope #
- Type:
str.enum - Values:
single-process·system-wide - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The scope of a profiling authorisation. system-wide samples every
process, PIP-protected ones included, and needs SeSystemProfilePrivilege;
single-process profiles one other process and needs
SeProfileSingleProcessPrivilege.
Carried by:
No event carries this field yet.
B.28.31 operation.ptrace-initiator #
- Type:
str.enum - Values:
tracer·tracee - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Which side began a ptrace relationship. tracer is a debugger attaching
to another process; tracee is a process inviting its parent to control
it, with PTRACE_TRACEME.
Carried by:
No event carries this field yet.
B.28.32 operation.reentry-depth #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The nesting depth of the kernel's own re-entry into descriptor storage: a
read or write of a descriptor attribute that passes back through the
security hooks, as happens through a stacking filesystem. Not the depth of
a directory walk, which is object.sd.synthesis-depth.
Carried by:
No event carries this field yet.
B.28.33 operation.rename-flags #
- Type:
uint.flags - Values:
0x1 RENAME_NOREPLACE·0x2 RENAME_EXCHANGE·0x4 RENAME_WHITEOUT - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The flags a caller passed to renameat2. StrataFS always refuses
RENAME_WHITEOUT with EINVAL, so a record carrying that bit is a
refusal however the rest of the rename looked.
Carried by:
No event carries this field yet.
B.28.34 operation.replaced #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the operation silently replaced something that was already there, such as a link between tokens that displaced an existing pair.
Carried by:
No event carries this field yet.
B.28.35 operation.returned #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many items the operation returned. Fewer than operation.available
means the result was truncated.
Carried by:
No event carries this field yet.
B.28.36 operation.route #
- Type:
str.enum - Values:
in-place·copy-up·read-only - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How StrataFS routed a mutation of an existing object. in-place wrote the
object in the stratum that provides it. copy-up first copied it into the
create stratum and wrote the copy. read-only refused it with EROFS,
because the object's stratum does not accept writes and no copy-up was
possible.
read-only has several causes, among them a read-only mount, no create
stratum, and an object that cannot be copied, and this field does not say
which.
Carried by:
No event carries this field yet.
B.28.37 operation.setattr-mask #
- Type:
uint.flags - Values:
0x1 ATTR_MODE·0x2 ATTR_UID·0x4 ATTR_GID·0x8 ATTR_SIZE·0x10 ATTR_ATIME·0x20 ATTR_MTIME·0x40 ATTR_CTIME·0x80 ATTR_ATIME_SET·0x100 ATTR_MTIME_SET·0x200 ATTR_FORCE·0x400 ATTR_CTIME_SET·0x800 ATTR_KILL_SUID·0x1000 ATTR_KILL_SGID·0x2000 ATTR_FILE·0x4000 ATTR_KILL_PRIV·0x8000 ATTR_OPEN·0x10000 ATTR_TIMES_SET·0x20000 ATTR_TOUCH·0x40000 ATTR_DELEG - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Which attributes a setattr asked to change, as the kernel's ATTR_* bits.
These bits are internal to the Linux kernel rather than part of its ABI,
so they are decoded against the kernel version that wrote the record.
ATTR_SIZE with ATTR_OPEN is a truncate from open with O_TRUNC, not
an explicit truncate call.
Carried by:
No event carries this field yet.
B.28.38 operation.settled-view #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether a record concerns a directory view settled when its descriptor was opened, rather than a live resolution of the merged path. A settled view keeps the listing and strata it was opened with, and goes on answering from them after a rename and when the descriptor is passed to another process, so it can disagree with what the path resolves to now.
Carried by:
No event carries this field yet.
B.28.39 operation.signal #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The signal number of a signal delivery decision. The signal decides which process right is required, so a refusal is read with it.
Carried by:
No event carries this field yet.
B.28.40 operation.site #
- Type:
str.enum - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Which system call or kernel gate the record came from. Peios adds about thirty access checks to Linux call sites — many of them re-checks on an already-open file descriptor, added because an open descriptor used to be a way round the check — and a refusal is only useful if it says which one fired. Each event declares the sites it can come from.
Carried by:
No event carries this field yet.
B.28.41 operation.stacked-create #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the object's real inode was created by a stacking filesystem below StrataFS rather than directly by the stratum's own filesystem. When true, a different filesystem's hooks ran on the creation, which changes which hook stamped the object's security descriptor.
Carried by:
No event carries this field yet.
B.28.42 operation.stage #
- Type:
str.enum - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Which internal stage of a multi-step operation the record concerns. Several unrelated operations have stages, with nothing in common between their vocabularies, so each event declares its own.
Carried by:
B.28.43 operation.stage-requested #
- Type:
str.enum - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The stage that was asked for while another, operation.stage, was still
active. Drawn from the same vocabulary as operation.stage on the same
event, which declares it.
Carried by:
No event carries this field yet.
B.28.44 operation.strata #
- Type:
uint.flags - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The set of strata taking part in a merged operation, as flags with one bit per stratum: bit 0 is the stratum at index 0, the highest-precedence one. A merged directory's listing, for example, draws on every stratum whose bit is set.
Carried by:
No event carries this field yet.
B.28.45 operation.succeeded-count #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
How many items in a batch operation succeeded.
Carried by:
B.28.46 operation.timeout #
- Type:
uint.duration - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The timeout that applied to the operation. On an operation that timed
out, operation.duration will have reached it.
Carried by:
No event carries this field yet.
B.28.47 operation.uid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The UID a setuid-family call asked for. Peios does not let such a call
change identity; the record shows what was attempted.
Carried by:
No event carries this field yet.
Generated from kernel.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.29 outcome.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under outcome, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.29.1 outcome.address #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The user-space address that faulted when the kernel tried to copy from or to it. Meaningful only within the faulting process's address space, and only until that address space changes.
Carried by:
No event carries this field yet.
B.29.2 outcome.cleanup #
- Type:
str.enum - Values:
succeeded·failed·declined - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The outcome of the compensating action StrataFS took to undo part of a
failed operation, such as removing an object it had just created.
declined means StrataFS deliberately did not undo it, because the name
no longer referred to the object it had created and removing it could
have removed someone else's.
Both failed and declined leave an object behind in the stratum that
nothing in the merged view accounts for.
Carried by:
No event carries this field yet.
B.29.3 outcome.data-lost #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the outcome a record describes destroyed data, rather than only failing to do what was asked. Set this aside from ordinary failures: a data-losing outcome is not recovered by retrying.
Carried by:
No event carries this field yet.
B.29.4 outcome.deferred #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether a refusal was raised after the caller had already been told the operation succeeded. A deferred refusal has nobody left to receive an error, which is why it is recorded unconditionally rather than only for the arrangement errors.
Carried by:
B.29.5 outcome.detail #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Free text about the outcome, for a person to read. Optional, and never
parsed: anything a consumer would filter on belongs in outcome.reason or
a field of its own. Never the output of a language's debug formatter.
Carried by:
eventd.store.quarantinedpeinit.config.reload.appliedpeinit.critical-service.failedpeinit.graph.validation.failedpeinit.job.endedpeinit.operation.endedpeinit.recovery.enteredpeipkg.action.authorisedpeipkg.claim.changedpeipkg.package.installedpeipkg.package.uninstalledpeipkg.package.upgradedpeipkg.repository.addedpeipkg.repository.removedpeipkg.transaction.recovered
B.29.6 outcome.errno #
- Type:
int.errno - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The error the operation failed with, as a negative errno. Absent on success. Present where an event records an outcome that has a specific failure rather than only a boolean.
Carried by:
kacs.audit.descriptor.changedkacs.caap.policy.changedkacs.config.value.rejectedkacs.impersonation.revertedkacs.impersonation.startedkacs.signature.crypto.failedkmes.buffer.swap.failedkmes.config.refresh.failedlcs.audit.backup.endedlcs.audit.key.deletedlcs.audit.key.descriptor.changedlcs.audit.key.hiddenlcs.audit.key.tombstonedlcs.audit.restore.endedlcs.audit.transaction.committedlcs.audit.value.deletedlcs.audit.value.setntfe.policy.rejectedpeinit.event.droppedstratafs.file.copied-upstratafs.mutation.refused
B.29.7 outcome.errno-cleanup #
- Type:
int.errno - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The error a compensating action failed with, when an operation failed and
its undo failed too. Read with outcome.errno, the error of the operation
itself: the pair is how orphaned objects come to exist. Absent when the
undo succeeded or was not attempted.
Carried by:
No event carries this field yet.
B.29.8 outcome.errno-masked #
- Type:
int.errno - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The error an underlying step actually failed with, where StrataFS
returned a different one to the caller. outcome.errno holds the error
the caller saw; this field holds the one it replaced, which is usually the
one that explains the failure.
Carried by:
No event carries this field yet.
B.29.9 outcome.fallback #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the operation took a fallback path because the mechanism it prefers was unavailable. The operation still completed, by a slower or coarser route.
Carried by:
No event carries this field yet.
B.29.10 outcome.field #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The name of the structure field an error was attributed to, such as the member of an argument structure that was out of range.
Carried by:
No event carries this field yet.
B.29.11 outcome.invariant-violated #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether an internal invariant was found broken and execution continued anyway. A record with this set describes a state the code assumes cannot happen, so treat whatever follows it on the same mount as suspect.
Carried by:
No event carries this field yet.
B.29.12 outcome.offset #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The byte offset within the input at which parsing failed. Read with
outcome.field where the input is a structure.
Carried by:
No event carries this field yet.
B.29.13 outcome.reason #
- Type:
str.enum - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Why the operation resolved the way it did. Each event declares its own
values: a list, or a curated subset of a uapi/pkm/trace.h family, which
carries codes that are never event reasons. A trace code is rendered with
the family prefix stripped and case folded, so KACS_FSR_GRANT_DENY
presents as grant-deny, and an investigation escalating from the event
stream to a live trace sees the same decision described in the same terms.
Carried by:
authd.logon.attemptedauthd.service.attestedauthd.session.endedkacs.audit.handle.usedkacs.caap.sacl.skippedkacs.config.value.rejectedkacs.descriptor.rejectedkacs.impersonation.startedlcs.audit.transaction.committedlcs.source.response.rejectedlpsd.account.createdlpsd.account.deletedlpsd.account.modifiedlpsd.credential.changedlpsd.credential.verifiedlpsd.group.createdlpsd.group.deletedlpsd.group.member.addedlpsd.group.member.removedntfe.policy.rejectedntfe.verdict.reportedpeinit.boot.downgradedpeinit.critical-service.failedpeinit.fd-store.rejectedpeinit.graph.validation.failedpeinit.graph.validation.warnedpeinit.notify.rejectedpeinit.on-failure.suppressedpeinit.recovery.enteredpeipkg.claim.changedpeipkg.package.installedpeipkg.package.uninstalledpeipkg.package.upgradedpeipkg.repository.added
B.29.14 outcome.result-raw #
- Type:
int - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
A raw result from an internal step that was neither zero nor a valid
negative errno, recorded instead of being passed on. Deliberately not
outcome.errno: a value outside the errno range returned from an open
could reach the caller as a pointer to a file, so StrataFS converts it to
EIO, and this field holds what it caught.
Carried by:
No event carries this field yet.
B.29.15 outcome.reverted #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether a credential change the kernel had prepared was silently undone, so that the call returned success having changed nothing. The caller is told it worked; this is the only record that it did not.
Carried by:
No event carries this field yet.
B.29.16 outcome.success #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the operation succeeded. What counts as success narrows per event; read the event's own note before computing a rate over it.
Carried by:
authd.logon.attemptedauthd.service.attestedauthd.session.endedkacs.audit.access.checkedkacs.audit.descriptor.changedkacs.audit.handle.usedkacs.audit.privilege.usedkacs.caap.policy.changedkacs.impersonation.revertedkacs.impersonation.startedkacs.mount.policy.changedlcs.audit.backup.endedlcs.audit.key.createdlcs.audit.key.deletedlcs.audit.key.descriptor.changedlcs.audit.key.hiddenlcs.audit.key.openedlcs.audit.key.tombstonedlcs.audit.restore.endedlcs.audit.transaction.committedlcs.audit.value.deletedlcs.audit.value.setlpsd.account.createdlpsd.account.deletedlpsd.account.modifiedlpsd.credential.changedlpsd.credential.verifiedlpsd.group.createdlpsd.group.deletedlpsd.group.member.addedlpsd.group.member.removedpeinit.config.reload.appliedpeinit.graph.operation.endedpeinit.job.endedpeinit.operation.endedpeipkg.action.authorisedpeipkg.claim.changedpeipkg.package.installedpeipkg.package.uninstalledpeipkg.package.upgradedpeipkg.repository.addedpeipkg.repository.refreshedpeipkg.repository.removedpeipkg.transaction.recoveredstratafs.file.copied-up
B.29.17 outcome.verdict #
- Type:
str.enum - Values:
pass·reject·drop - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The decision a policy engine reached, where the decision has more than two
states and outcome.success cannot express it. reject and drop both
refuse, and differ in whether the refusal is signalled back to the sender
or the traffic is discarded silently.
Carried by:
B.29.18 outcome.verdict-previous #
- Type:
str.enum - Values:
pass·reject·drop - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The verdict a flow's cached judgment held before it was judged again. Set
beside outcome.verdict, the pair is what tells a reload that changed a
live connection's fate, such as pass to drop, from the far commoner
re-judgment that changed nothing.
Carried by:
No event carries this field yet.
Generated from kernel.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.30 policy.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under policy, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.30.1 policy.fallback #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the compiled-in fallback policy was answering rather than a loaded rule set. Makes a silent fall-back to built-in security policy visible: the system keeps working, on rules no administrator wrote.
Carried by:
B.30.2 policy.generation #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The generation counter of the rule set in force when the decision was made. Increments on every policy load, so a record can be tied to the exact rule set that produced it rather than to whatever is loaded when the record is read.
Carried by:
B.30.3 policy.generation-previous #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The generation that was in force before a policy load replaced it. On a
failed load, compare with policy.previous-retained.
Carried by:
B.30.4 policy.layers #
- Type:
str.enum[] - Values:
raw-packet·packet·flow - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The rules layers the active generation loaded a forest for. A layer missing
from the list judges nothing and waves every traversal past it. A missing
flow layer also turns off endpoint attribution for the whole machine,
so no flow carries an owner and every identity field is absent.
Carried by:
B.30.5 policy.previous-retained #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether a failed policy load left the previous rule set in force. True is
the safe outcome: the old policy still applies. False means the previous
rule set is not in force either; policy.fallback says what is answering
instead.
Carried by:
B.30.6 policy.report-threshold #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The machine's reporting threshold, CurrentReportingLevel: a rule's report
fires only when its level is at least this. It runs from 1 to 6 and is 1
when the registry has no value. Rules may only declare levels 1 to 5, so a
threshold of 6 silences every report on the machine, and a silent stream
then means nothing about the traffic.
Carried by:
B.30.7 policy.report-threshold-previous #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The machine's reporting threshold before the change a record reports, in
the terms of policy.report-threshold. A move to 6 is a change to the
audit volume itself and turns every network report off.
Carried by:
Generated from kernel.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.31 privilege.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under privilege, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.31.1 privilege.authority #
- Type:
str.enum - Values:
privilege·administrators - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
Which rung of a privileged gate let the operation through. privilege
means the caller's token held the privilege the gate asks for, enabled,
and the use was marked against it. administrators means it did not, and
the gate fell back to the caller's enabled membership of Administrators.
The second is an authorisation by group membership rather than by privilege. From outside the kernel the two look identical, so this field is the only way to tell a gate passed by SeTcbPrivilege from one passed by an administrator who never held it.
Carried by:
No event carries this field yet.
B.31.2 privilege.contributed #
- Type:
uint.mask - Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The access bits this privilege supplied to the check, intersected with what
the caller actually asked for. Distinct from access.granted, which is the
whole check's result rather than this privilege's part in it.
Carried by:
B.31.3 privilege.held #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
Whether the caller's effective token held privilege.name, enabled, at
the moment the gate asked. Every privilege gate tests for an enabled
privilege, so a privilege present on the token but disabled counts as not
held: false here, with the privilege visible in the token, is a caller
that did not enable it, not one that lacked it.
Carried by:
B.31.4 privilege.name #
- Type:
str.enum - Values:
SeSecurityPrivilege·SeTakeOwnershipPrivilege·SeBackupPrivilege·SeRestorePrivilege·SeRelabelPrivilege·SeCreateTokenPrivilege·SeAssignPrimaryTokenPrivilege·SeLockMemoryPrivilege·SeIncreaseQuotaPrivilege·SeTcbPrivilege·SeLoadDriverPrivilege·SeSystemProfilePrivilege·SeSystemtimePrivilege·SeProfileSingleProcessPrivilege·SeIncreaseBasePriorityPrivilege·SeShutdownPrivilege·SeDebugPrivilege·SeAuditPrivilege·SeChangeNotifyPrivilege·SeRemoteShutdownPrivilege·SeManageVolumePrivilege·SeImpersonatePrivilege·SeCreateSymbolicLinkPrivilege - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The privilege a record is about, by its Windows name. On an access check
it is the privilege that contributed access, and only the first five can
influence an access check. At a Linux capability gate it is the privilege
the capability maps to — SeTcbPrivilege for most, SeDebugPrivilege for
sys-ptrace, SeShutdownPrivilege for sys-boot and so on — and
SeManageVolumePrivilege or SeTcbPrivilege at the volume gate.
SeImpersonatePrivilege appears on an impersonation that needed it. The set
is open because new privileges may be defined.
Carried by:
B.31.5 privilege.required-held #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
Whether the operation the caller asked for was the privileged variant, so that a privilege had to be spent to perform it. Several KACS operations have an ordinary form and a privileged one — installing a primary token for a different identity rather than the caller's own, for example — and this says which the caller reached for.
A boolean, not a privilege name: the privilege in question is
privilege.name.
Carried by:
No event carries this field yet.
B.31.6 privilege.surviving #
- Type:
uint.mask - Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The subset of privilege.contributed that reached the final granted mask.
Empty means the privilege fired and a later layer stripped it — a
confinement, a CAAP rule, or PIP non-dominance — which is usually the more
interesting record of the two, because it shows a boundary doing its job.
Carried by:
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.32 prompt.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under prompt, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.32.1 prompt.handler #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The name of the handler a PROMPT action addressed. NTFE has no handler
transport yet, so every prompt falls back at once; the handler's name is
the operator's only clue to which integration is missing.
Carried by:
No event carries this field yet.
B.32.2 prompt.handler-fallback #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
What a PROMPT fell back to when its handler did not answer: the action
its fallback resolved to, as the rule wrote it. Fallbacks can themselves
be prompts, up to four deep, and this is the action at the end of the
chain.
Carried by:
No event carries this field yet.
Generated from ntfe.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.33 reject.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under reject, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.33.1 reject.delivered-to #
- Type:
str.enum - Values:
wire·self - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
Where NTFE sent a refusal. wire answers the remote peer on the link, from
the ingress seat. self routes the answer back into this machine, from
every other seat, so the stack fails the local socket at once rather than
leaving it to time out.
Carried by:
No event carries this field yet.
B.33.2 reject.packet #
- Type:
str.enum - Values:
tcp-reset·icmp-port-unreachable·icmp-admin-prohibited - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The packet NTFE built to deliver a refusal: a TCP reset for refused TCP,
an ICMP or ICMPv6 port-unreachable for refused anything else, or an ICMP or
ICMPv6 administratively-prohibited for every prohibited refusal. It decides
what the refused program sees: ECONNREFUSED for a reset or a
port-unreachable, EHOSTUNREACH for an IPv4 prohibition, and EACCES for
an IPv6 one. Absent when the REJECT degraded to a DROP.
Carried by:
No event carries this field yet.
Generated from ntfe.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.34 request.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under request, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.34.1 request.deferred-effect #
- Type:
str.enum - Values:
restore-commit·key-mutation - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The kind of kernel-side work attached to a source request, to be applied
when the source's reply arrives rather than when the caller returns.
restore-commit completes a registry restore; key-mutation publishes
the effects of a key write, such as watch notifications and generation
updates. Absent when the request carries no deferred work.
Deferred work is what makes a late reply consequential. When the caller
has already given up (request.waiter-detached), the effect is still
applied, so a change the caller was told had failed can land afterwards.
Carried by:
No event carries this field yet.
B.34.2 request.id #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The identifier of the source request this record concerns, for joining a record to the exchange that produced it. A pure correlation key.
Carried by:
B.34.3 request.op-code #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The source-protocol operation code of the request this record concerns. Identifies which kind of exchange returned unusable data.
Carried by:
B.34.4 request.op-code-expected #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The source-protocol operation code the kernel expected a response to carry,
recorded where the source answered with a different one. Read it beside
request.op-code, which holds the code that actually arrived; the
disagreement between the two is the finding.
A response code is the request code with the high bit 0x8000 set, so an
expected code of 0x8021 answers a request of 0x0021 (RSI_SET_VALUE).
Carried by:
No event carries this field yet.
B.34.5 request.status #
- Type:
str.enum - Values:
ok·not-found·already-exists·storage-error·not-empty·too-large·txn-busy·invalid·cas-failed·txn-not-supported - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The RSI status code a registry source returned for a request, named from
the RSI_* status constants with the prefix removed. The kernel maps each
status to an errno before the caller sees it, and the errno loses the
source's own account: storage-error and a response the kernel could not
use at all both reach the caller as EIO, and only this field says which
it was.
A status code outside this list is never carried here. It is rejected as
malformed source data and recorded on lcs.source.response.rejected with
the reason unknown-rsi-status-code.
Carried by:
No event carries this field yet.
B.34.6 request.timed-out #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
Whether a request's wait for the source's reply ran out before the reply arrived. The deadline is the configured request timeout. A timed-out request is not cancelled at the source: its reply may still arrive, and any deferred effect attached to it may still be applied.
Carried by:
lcs.audit.key.deletedlcs.audit.key.descriptor.changedlcs.audit.key.hiddenlcs.audit.key.tombstonedlcs.audit.value.deletedlcs.audit.value.set
B.34.7 request.waited #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
Whether the kernel blocked waiting for the source's reply to a request. False for a request dispatched without a waiter, whose outcome no caller will see directly.
Carried by:
No event carries this field yet.
B.34.8 request.waiter-detached #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
Whether the caller that issued a request had stopped waiting by the time the source's reply arrived. True means the operation had already reported failure to userspace, typically a timeout, so any effect the reply carries lands after the caller was told it did not. This is the entry condition for the kernel's late-effect replay.
Carried by:
No event carries this field yet.
Generated from lcs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.35 rule.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under rule, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.35.1 rule.action-error #
- Type:
str.enum - Values:
unknown-action·bad-arity·bad-argument·unknown-reject-kind·malformed - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
Why one action expression in a rule's Actions value failed to parse.
unknown-action is a name the language does not have; bad-arity is the
wrong number of arguments; bad-argument is an argument of the wrong
shape, such as a REPORT level outside 1 to 5; unknown-reject-kind is a
REJECT story that is not minted; malformed is unbalanced parentheses or
trailing text. A rule with a bad action refuses the whole generation, not
just itself.
Carried by:
B.35.2 rule.contender.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
A second rule named on the same record as rule.name: the rule that lost a
contest to it, or a rule that collided with it, such as one reading a tag
that a higher layer writes. Like rule.name, it is the rule's registry path
relative to its layer key.
Carried by:
No event carries this field yet.
B.35.3 rule.hash #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The FNV-1a-64 hash of the rule's whole registry path. Always carried, even
when rule.name was cut short, so a reader can resolve a rule the record
could not name in full. Resolve it against the policy generation the
record names, because a later generation may name its rules differently.
A 64-bit integer rather than a digest: it identifies a path, and is not
a cryptographic summary of anything.
Carried by:
B.35.4 rule.layer #
- Type:
str.enum - Values:
raw-packet·packet·flow - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
Which evaluation layer the rule sat in. The layers see progressively more context — a raw frame, a parsed packet, a tracked connection — so the layer bounds what the rule could possibly have matched on.
Carried by:
B.35.5 rule.lint #
- Type:
str.enum - Values:
fact-never-present-at-layer - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
A non-fatal finding about a rule that ingested but is almost certainly
wrong. fact-never-present-at-layer is a condition on a fact that never
exists at the rule's layer, which the absent-fact law makes permanently
false, so the rule occupies its place in the tree and never matches. A lint
never blocks ingestion and never appears beside a failure.
Carried by:
No event carries this field yet.
B.35.6 rule.match-value #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The literal match value written in a rule, exactly as its author wrote it
in the registry. It is the text the rule compares a fact against, such as
10.0.0.0/8 or 22, before NTFE parsed it. It matters most where the value
would not parse, because then the rule key alone does not say what was
wrong.
Carried by:
No event carries this field yet.
B.35.7 rule.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The policy rule that produced this record, by name. Rules are the unit an administrator writes and edits, so this is the field that connects a record back to something a person can change.
Carried by:
B.35.8 rule.name-truncated #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
Whether rule.name was cut short because the rule's path did not fit in
the record. The cut falls at a character boundary, so the name is valid
text, but it is not the whole path: do not search for it exactly.
rule.hash still identifies the whole path, and is the value to resolve
the rule by.
Absent when the name was carried in full.
Carried by:
B.35.9 rule.priority #
- Type:
int - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The effective priority of a rule, with any priority it inherits from its ancestors in the rule tree already resolved. When several rules in one layer yield a verdict, the highest priority wins and a tie goes to the stricter verdict, so this is the number that explains why one rule beat another. It is a signed 64-bit integer, as the rules language holds it, and negative values are legal: a lower value means a rule loses to any rule with a higher one.
Carried by:
No event carries this field yet.
B.35.10 rule.report-level #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The reporting level the matching rule asked for. Records exist only because a rule contained an explicit report directive, and this is the level it named.
Carried by:
B.35.11 rule.seat #
- Type:
str.enum - Values:
ingress·egress·local-in·local-out - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
Where in the path evaluation happened. Distinguishes traffic entering or leaving the machine from traffic terminating at or originating from a local socket, which is the difference between forwarding policy and host policy.
Carried by:
Generated from ntfe.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.36 shutdown.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under shutdown, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.36.1 shutdown.finalization-state #
- Type:
str.enum - Values:
waiting-for-services·ready·failed·completed - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
How far the system shutdown had got when the event was written.
waiting-for-services means services were still being stopped; ready
means they had stopped and the final action had not yet run; failed
means finalisation itself went wrong; completed means it finished.
Carried by:
Generated from peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.37 signature.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under signature, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.37.1 signature.algorithm #
- Type:
str.enum - Values:
ml-dsa-65 - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The algorithm a signature was made with. Peios code signatures are ML-DSA-65 over a SHA-256 digest of the file, and that is the only value today; the set is open so that a second algorithm can be added.
Carried by:
No event carries this field yet.
B.37.2 signature.crypto-stage #
- Type:
str.enum - Values:
alloc-transform·set-pubkey·boot-probe - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
Where the signature machinery failed, when verification could not run.
alloc-transform means the kernel could not obtain an ML-DSA-65
transform; set-pubkey that the transform rejected a key from the
built-in table, which is a fault in the table rather than a signature
that did not match; boot-probe that the one-off check made once the
crypto subsystem is up found the algorithm unavailable.
Each makes verification impossible rather than negative. The exec path refuses a signed binary it cannot verify rather than run it unlabelled, so a crypto failure shows as refused execs across the whole system.
Carried by:
B.37.3 signature.location #
- Type:
str.enum - Values:
none·elf-section·xattr - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
Where a file's signature was found. elf-section is the .peios.sig
section of an ELF file, xattr the security.peios.sig extended
attribute, and none means the file carried neither and is unsigned. The
ELF section is looked for first; an ELF file whose headers are malformed
is not then checked for the attribute.
Carried by:
No event carries this field yet.
B.37.4 signature.parse-error #
- Type:
str.enum - Values:
elf-magic-read·elf-short-ehdr·elf-ehdr-read·elf-bad-ident·elf-bad-shtable·elf-shdrs-range·elf-shstr-read·elf-strtab-range·elf-shdr-read·elf-name-read·elf-bad-sig-section·elf-bad-blob·elf-hash-fail·xattr-bad-blob·xattr-hash-fail·size-changed - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
Why the signature material on a file could not be read, named for its
KACS_SIG_* code. The elf- values are failures walking the ELF header
and section table to the .peios.sig section, or reading and hashing the
signature inside it; the xattr- values are the same for the
security.peios.sig attribute. size-changed is not a parsing failure:
the file changed size while it was being read, and the kernel discarded
what it had found.
Every value leaves the file treated as unsigned. The binary runs without the integrity label its signature would have given it, so a record carrying this field is the only thing that tells a damaged or tampered signature from no signature at all.
Carried by:
No event carries this field yet.
B.37.5 signature.result #
- Type:
str.enum - Values:
unsigned·bad-key-table·no-key-match·verified·crypto-unavailable·crypto-mismatch - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The outcome of checking a signature against the built-in signing keys,
named for its KACS_SIG_* code. verified means a key verified it and
the file takes that key's integrity tier; no-key-match that it was
signed but no built-in key verified it; unsigned that there was nothing
to check. bad-key-table and crypto-unavailable mean verification
could not run at all, and the exec path treats both as a refusal, never as
unsigned. crypto-mismatch is the result for one key that did not verify
the signature on the way to the next; the whole check then ends in
verified or no-key-match.
no-key-match runs the binary unlabelled, exactly as unsigned does.
It is a stronger signal than it looks: something was signed with a key
this system does not trust.
Carried by:
No event carries this field yet.
B.37.6 signature.signing-key.fingerprint #
- Type:
bin - Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The fingerprint of a signing key: the SHA-256 digest of its public key,
the value the kacs/signing_keys listing shows as key_sha256. Not
emitted today. The verification path identifies a key only by its
position in the built-in table and never digests it, so nothing at the
point of decision can supply this value; signature.signing-key.index is
all a record carries. Until it is, a no-key-match verdict cannot say
which key a binary was expected to match.
Carried by:
No event carries this field yet.
B.37.7 signature.signing-key.index #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kacs.evman
The zero-based position of a signing key in the kernel's built-in key
table, which is all that identifies a key at the point of verification
today. The table is compiled into the kernel, so an index means something
only against the same kernel build: compare it with the
kacs/signing_keys listing in securityfs, whose lines are in table order,
on a machine running that build. The same index on another build may be a
different key.
Carried by:
No event carries this field yet.
Generated from kacs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.38 source.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under source, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.38.1 source.address #
- Type:
str.ip - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The address the traffic came from, rendered in the textual form for its family.
Carried by:
B.38.2 source.attribution #
- Type:
str.enum - Values:
socket-stamp·no-kacs-state·unstamped·no-namespace·loopback-sender-lost·time-wait·stack-handled·no-sending-socket·multicast-or-broadcast·no-receiver·principal-unreadable - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
Why the source endpoint was given the attribution it has. Not emitted
today: NTFE decides it on every flow and encodes it nowhere, so
source.endpoint and source.unattributed are all a record carries. The
values name the paths the attribution can take: socket-stamp is the
principal KACS stamped on the socket; no-kacs-state, unstamped and
no-namespace fell back to the kernel because no stamp could be read;
loopback-sender-lost is the sender of a loopback flow that the inbound
seat cannot see; time-wait, stack-handled and no-sending-socket are
the stack answering for itself; principal-unreadable is a program
endpoint whose token could not be read.
Carried by:
No event carries this field yet.
B.38.3 source.endpoint #
- Type:
str.enum - Values:
program·kernel·shared·none - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
What stands at the source end of the traffic, when that end is on this
machine. program is a process's socket, and the source.socket.* fields
then say whose. kernel is the network stack itself: a reset, an ICMP
error, a protocol the stack consumes, or a socket nobody could attribute.
shared and none arise only at a receiving end, as destination.endpoint
describes. Present only on flow-layer records, and absent when the source is a remote
machine. Read source.unattributed before taking kernel at its word.
Carried by:
No event carries this field yet.
B.38.4 source.file.path #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The path an object came from, in an operation on two objects such as a
rename or a link. The object is at destination.file.path afterwards.
Neither is the record's object.file.path: the operation acted on both.
Carried by:
No event carries this field yet.
B.38.5 source.mac #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The link-layer address the traffic came from, written as six colon-separated lowercase hexadecimal octets. Known only where an Ethernet header exists: from the frame at the device seats, and, for outbound traffic at the IP seats, the sending device's own address. Absent on a device with no Ethernet header.
Carried by:
No event carries this field yet.
B.38.6 source.port #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The transport port the traffic came from. Present only for protocols that have ports.
Carried by:
B.38.7 source.repository.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peipkg.evman
The repository a package was installed or upgraded from, by the name it is configured under. One transaction can draw from several repositories, which is why this is per package. It is how a bad install is traced back to the repository that served it.
Absent when the package came from a local file, which is the only way an
install or upgrade has no repository. An uninstall has no source and never
carries it. The role is source because the repository is where the
package came from; object.repository.name is the field for events about a
repository itself.
Carried by:
B.38.8 source.rsi.generation #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The restart generation of a registry source slot: how many times the slot has been resumed by a re-registering source after going down. Zero for a source that has never been restarted. A count that climbs quickly is a source crash-looping.
Carried by:
No event carries this field yet.
B.38.9 source.rsi.generation-validated #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The source restart generation that a cached key handle was last validated
against. A handle whose validated generation is behind
source.rsi.generation has outlived a source restart and must be
revalidated before use, because the key it names may no longer exist.
Carried by:
No event carries this field yet.
B.38.10 source.rsi.hive #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The hive the offending source data belonged to, where the failure could be attributed to one. Absent when the data was malformed before a hive could be determined.
Carried by:
B.38.11 source.rsi.in-flight #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The number of requests dispatched to a registry source and still awaiting a reply, at the moment of the record. Bounded by the configured concurrency ceiling per source, so a value at that ceiling means new requests are queuing behind slow ones.
Carried by:
No event carries this field yet.
B.38.12 source.rsi.queued #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
The number of requests queued for a registry source and not yet delivered
to it, at the moment of the record. Distinct from source.rsi.in-flight:
a queued request has not reached the source at all, so it is discarded
rather than failed if the source goes down.
Carried by:
No event carries this field yet.
B.38.13 source.rsi.slot #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
lcs.evman
Which registry source slot supplied the data, by index. Sources are the userspace backends that serve registry content to the kernel, so this names the backend rather than anything about the caller.
Carried by:
B.38.14 source.socket.process.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The durable GUID of the process recorded on the source endpoint's socket when it was stamped. Unlike the process ID it is never reused, so it still names the right process after that process has exited.
Carried by:
No event carries this field yet.
B.38.15 source.socket.process.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The short name of the process recorded on the source endpoint's socket when it was stamped. It is the kernel's command name, at most 15 characters and lossily sanitised, so two different programs can share it; it is a hint for a person, not an identity.
Carried by:
No event carries this field yet.
B.38.16 source.socket.process.pid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The process ID recorded on the source endpoint's socket when it was
stamped. A socket can outlive the process that created it, so this process
may have exited and its ID been reused; source.socket.process.guid is the
identity that survives that.
Carried by:
No event carries this field yet.
B.38.17 source.socket.token.service-sid #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The per-service SID of the token stamped on the source endpoint's socket, when that token belongs to a service. It is found among the token's enabled groups and is always the 32-byte S-1-5-80 form. Absent for a socket that no service stands behind.
Carried by:
No event carries this field yet.
B.38.18 source.socket.token.sid #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The user SID of the token KACS stamped on the source endpoint's socket. It
is the identity the socket was stamped with, not necessarily the identity
of whoever writes to it now. Present only when source.endpoint is
program.
Carried by:
No event carries this field yet.
B.38.19 source.stratum-previous.index #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
stratafs.evman
The position, within the mount's stack, of the stratum that provided an
object before the change the record describes. Present exactly when
source.stratum-previous.path is.
Carried by:
No event carries this field yet.
B.38.20 source.stratum-previous.path #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
stratafs.evman
The filesystem path of the stratum that provided an object before the
change the record describes. Read with source.stratum.path: a record
where the two differ is an object that now comes from a different layer,
so a reader of the merged path sees different contents without the path
having changed.
Carried by:
No event carries this field yet.
B.38.21 source.stratum.flags #
- Type:
uint.flags - Values:
0x1 create·0x2 read-only·0x4 absent-may - Asserted: no
- Carried in: the payload
- Defined in:
stratafs.evman
The configuration flags of the stratum an object was read from, as the
mount option set them: +create, +ro and +am. create marks the one
stratum new objects and copy-ups are written into, read-only forbids
writing into the stratum in place, and absent-may lets the mount succeed
while the stratum's directory does not exist.
Carried by:
No event carries this field yet.
B.38.22 source.stratum.index #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
stratafs.evman
Which stratum an object was read from, by index within the mount's stack.
Absent when no provider was determined, as source.stratum.path is.
Carried by:
B.38.23 source.stratum.path #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
stratafs.evman
The filesystem path of the stratum an object was read from. Absent when
the refusal was raised before any provider was determined — creation,
tmpfile, and the heads of link and rename — in which case
source.stratum.index is absent too, and the two agree that no provider
was involved.
Carried by:
B.38.24 source.token.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The durable GUID of the token an operation derived a new one from, by
duplicating or filtering it. The new token is destination.token.guid.
Carried by:
No event carries this field yet.
B.38.25 source.token.sid #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The user SID of the token of the process that asked for an operation on
someone else's behalf: the program that originated a logon, such as
login, sshd or a graphical greeter, as against the principal being signed
in. The emitter reads it from the connected peer's token, never from
anything the peer sent.
Not subject.token.sid, which is the principal the record is about.
Several originators run as SYSTEM, so this field says which principal
asked, not which program; the kernel-stamped header names the emitter, not
the originator, so nothing else in the record does.
Carried by:
B.38.26 source.unattributed #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
Whether NTFE confessed that it could not attribute the source endpoint to a
principal. When true, every rule condition on that endpoint's identity was
false for this traffic rather than evaluated. False does not prove the
endpoint was attributed: a program endpoint whose token could not be
read still reads false here, and its identity conditions failed just the
same.
Carried by:
No event carries this field yet.
Generated from kernel.evman, lcs.evman, ntfe.evman, peipkg.evman, stratafs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.39 store.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under store, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.39.1 store.committed.cpus #
- Type:
uint[] - Asserted: no
- Carried in: the payload
- Defined in:
eventd.evman
The CPUs whose KMES rings eventd was reading when it shut down, one entry per
logical CPU in ascending order. Parallel to store.committed.sequences.
Carried by:
B.39.2 store.committed.sequences #
- Type:
uint[] - Asserted: no
- Carried in: the payload
- Defined in:
eventd.evman
For each CPU in store.committed.cpus, the highest event sequence of this
boot committed with no gap before it, taken during a graceful shutdown after
eventd had stopped reading the rings and committed everything it had read.
Parallel to store.committed.cpus. It is diagnostic only: when
eventd next starts it works out where to resume from the store itself, never
from this record.
0 means no unbroken run from sequence 1 exists for that CPU, as in
store.resume.sequences. When eventd cannot read its record of what was
committed, it leaves both arrays out of the shutdown record rather than
write zeroes (PEI-1394), so a 0 here is always a real value.
Carried by:
B.39.3 store.kind #
- Type:
str.enum - Values:
event·log·metric - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
eventd.evman
Which of eventd's stores a record is about: the sharded event store, the log
store or the metric store. On a storage error this is the store eventd found
corrupt and quarantined, and with store.shard it is the field worth alerting
on. The set is open because the database eventd keeps its own metadata in
may be reported on separately later; today eventd rebuilds that database
when it is damaged, and writes no record of it.
Carried by:
B.39.4 store.restarted #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
eventd.evman
Whether this boot's events were already in the store when eventd started, which is to say eventd had already run during this boot and this start is a restart. It is false on eventd's first start in a boot. Counting the records where it is true answers how often eventd restarted during a boot, without inferring it from gaps.
A historical shard that cannot be opened at startup is left out of the decision, so on a damaged store a restart can be reported as false.
Carried by:
B.39.5 store.resume.cpus #
- Type:
uint[] - Asserted: no
- Carried in: the payload
- Defined in:
eventd.evman
The CPUs whose KMES rings eventd resumed reading at startup, one entry per
logical CPU in ascending order. Parallel to store.resume.sequences: entry
i of that array is where reading resumed for the CPU in entry i of this
one.
Carried by:
B.39.6 store.resume.sequences #
- Type:
uint[] - Asserted: no
- Carried in: the payload
- Defined in:
eventd.evman
For each CPU in store.resume.cpus, the highest event sequence of this boot
already committed with no gap before it, which is where eventd resumed
reading that CPU's ring. Parallel to store.resume.cpus.
0 is a value, not an absence. It means no unbroken run of committed events starting at sequence 1 exists for that CPU in this boot: nothing was committed yet, or the first events were lost. Events committed after a gap are not reflected here.
Carried by:
B.39.7 store.shard #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
eventd.evman
The index of the event-store shard a record is about, counting from 0.
Present only when store.kind is event; the log and metric stores are not
sharded, so a record about either has no shard.
Carried by:
B.39.8 store.shard-count #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
eventd.evman
How many active event-store shards eventd is running with, after resolving its configured shard count. Historical shards kept only for reading are not counted.
Carried by:
Generated from eventd.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.40 subject.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under subject, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.40.1 subject.job.activation-generation #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The activation generation of the service whose job sent a notification
message, as object.job.activation-generation describes it. A message
from an earlier run of a service carries an older generation.
Carried by:
peinit.notify.errno.reportedpeinit.notify.exit-status.reportedpeinit.notify.progress.reportedpeinit.notify.rejectedpeinit.notify.status.reportedpeinit.notify.stopping.reported
B.40.2 subject.job.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The job whose process sent a notification message: the service's main process, or a submitted job.
Carried by:
peinit.notify.errno.reportedpeinit.notify.exit-status.reportedpeinit.notify.progress.reportedpeinit.notify.rejectedpeinit.notify.status.reportedpeinit.notify.stopping.reported
B.40.3 subject.operation.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The peinit operation the sending job was serving when it sent a notification message, such as the start that a readiness message completes. Absent when the job serves none.
Carried by:
peinit.notify.errno.reportedpeinit.notify.exit-status.reportedpeinit.notify.progress.reportedpeinit.notify.rejectedpeinit.notify.status.reportedpeinit.notify.stopping.reported
B.40.4 subject.pip.trust #
- Type:
uint - Asserted: yes
- Carried in: the payload
- Defined in:
kernel.evman - In groups:
subject
The PIP trust level in force for the access check. Compared for dominance:
a caller whose trust does not dominate the target's is denied before the
DACL is reached. Caller-influenced on ioctl-originated events, as
subject.pip.type is.
Carried by:
kacs.audit.access.checked(viasubject)kacs.audit.descriptor.changed(viasubject)kacs.audit.handle.used(viasubject)kacs.audit.privilege.used(viasubject)kacs.caap.policy.changed(viasubject)kacs.caap.sacl.skipped(viasubject)kacs.caap.staging.diverged(viasubject)kacs.descriptor.rejected(viasubject)kacs.impersonation.reverted(viasubject)kacs.impersonation.started(viasubject)kacs.mount.policy.changed(viasubject)
B.40.5 subject.pip.type #
- Type:
uint.enum - Values:
0 None·512 Protected·1024 Isolated - Set: closed
- Asserted: yes
- Carried in: the payload
- Defined in:
kernel.evman - In groups:
subject
The Protected Isolated Process type in force for the access check. Held in process state rather than on the token, and a caller of the access-check ioctl may supply its own value — so on ioctl-originated events this is caller-influenced, and kernel-observed everywhere else.
Carried by:
kacs.audit.access.checked(viasubject)kacs.audit.descriptor.changed(viasubject)kacs.audit.handle.used(viasubject)kacs.audit.privilege.used(viasubject)kacs.caap.policy.changed(viasubject)kacs.caap.sacl.skipped(viasubject)kacs.caap.staging.diverged(viasubject)kacs.descriptor.rejected(viasubject)kacs.impersonation.reverted(viasubject)kacs.impersonation.started(viasubject)kacs.mount.policy.changed(viasubject)
B.40.6 subject.process.executable #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The executable of the process that acted, where that is not the emitter, resolved at exec with symbolic links followed.
Carried by:
No event carries this field yet.
B.40.7 subject.process.guid #
- Type:
bin.guid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The durable GUID of the process that acted, where that is not the emitter. Never reused, so it identifies the actor across PID reuse and after the process has exited.
Carried by:
No event carries this field yet.
B.40.8 subject.process.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The kernel's name (comm) for the process that acted, where that is not
the emitter. Lossy and truncated to 15 bytes; two processes can share a
name.
Carried by:
No event carries this field yet.
B.40.9 subject.process.pid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The process ID of the process that acted, where that is not the emitter: a
daemon reporting what a client did names the client here and itself in
emitter.process.pid. Same meaning and same caveat as
emitter.process.pid — PIDs are reused, so correlate on
subject.process.guid.
Carried by:
B.40.10 subject.service.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
peinit.evman
The service whose job sent a notification message. The sender is authenticated by its process, so this is the service peinit established, not one the message claimed.
Carried by:
peinit.notify.errno.reportedpeinit.notify.exit-status.reportedpeinit.notify.progress.reportedpeinit.notify.rejectedpeinit.notify.status.reportedpeinit.notify.stopping.reported
B.40.11 subject.token.auth-id #
- Type:
uint.luid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman - In groups:
caller,subject
The LUID of the logon session the effective token belongs to. The join key to
kacs.session.destroyed and to /sys/kernel/security/kacs/sessions, and the
way to gather every event from one sign-on.
Carried by:
kacs.audit.access.checked(viasubject)kacs.audit.descriptor.changed(viasubject)kacs.audit.handle.used(viasubject)kacs.audit.privilege.used(viasubject)kacs.caap.policy.changed(viasubject)kacs.caap.sacl.skipped(viasubject)kacs.caap.staging.diverged(viasubject)kacs.descriptor.rejected(viasubject)kacs.impersonation.reverted(viasubject)kacs.impersonation.started(viasubject)kacs.mount.policy.changed(viasubject)lcs.audit.backup.ended(viacaller)lcs.audit.backup.started(viacaller)lcs.audit.key.created(viacaller)lcs.audit.key.deleted(viacaller)lcs.audit.key.descriptor.changed(viacaller)lcs.audit.key.hidden(viacaller)lcs.audit.key.opened(viacaller)lcs.audit.key.tombstoned(viacaller)lcs.audit.restore.ended(viacaller)lcs.audit.restore.started(viacaller)lcs.audit.transaction.committed(viacaller)lcs.audit.value.deleted(viacaller)lcs.audit.value.set(viacaller)
B.40.12 subject.token.capabilities #
- Type:
bin.sid[] - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The confinement capability SIDs (S-1-15-3-…) the effective token carries.
Access to an object from a confined token turns on these: a confined token
reaches only what its capabilities, as well as its user and groups, are
granted. Unrelated to Linux capabilities, which are linux.cap. Absent on
an unconfined token.
Carried by:
No event carries this field yet.
B.40.13 subject.token.gid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The Linux GID the effective token projects onto, for correlating with
Linux-side data such as file ownership and /proc. A projection for
interoperability, never the identity KACS decides on.
Carried by:
No event carries this field yet.
B.40.14 subject.token.group-attributes #
- Type:
uint.flags[] - Values:
SE_GROUP_MANDATORY·SE_GROUP_ENABLED_BY_DEFAULT·SE_GROUP_ENABLED·SE_GROUP_OWNER·SE_GROUP_USE_FOR_DENY_ONLY·SE_GROUP_INTEGRITY·SE_GROUP_INTEGRITY_ENABLED·SE_GROUP_RESOURCE·SE_GROUP_LOGON_ID - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman - In groups:
subject
Per-group attribute bitmasks, positionally parallel to
subject.token.groups. The two arrays are always the same length, and
groups[i] is described by group-attributes[i].
SE_GROUP_INTEGRITY is present for ABI parity only — mandatory integrity
control reads subject.token.integrity, never this flag.
The attributes are the token's as held, not as requested when the token was
made: creation derives SE_GROUP_ENABLED from SE_GROUP_ENABLED_BY_DEFAULT.
Carried by:
kacs.audit.access.checked(viasubject)kacs.audit.descriptor.changed(viasubject)kacs.audit.handle.used(viasubject)kacs.audit.privilege.used(viasubject)kacs.caap.policy.changed(viasubject)kacs.caap.sacl.skipped(viasubject)kacs.caap.staging.diverged(viasubject)kacs.descriptor.rejected(viasubject)kacs.impersonation.reverted(viasubject)kacs.impersonation.started(viasubject)kacs.mount.policy.changed(viasubject)
B.40.15 subject.token.groups #
- Type:
bin.sid[] - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman - In groups:
subject
The group SIDs carried by the effective token. Access is frequently granted through one of these rather than to the user SID directly, so a search by principal that ignores them misses most of what that principal actually did.
Reconstructing membership as the access check sees it means applying the
check's own rule: SE_GROUP_ENABLED set and SE_GROUP_USE_FOR_DENY_ONLY
clear, for allow-side matching, read from subject.token.group-attributes.
Carried by:
kacs.audit.access.checked(viasubject)kacs.audit.descriptor.changed(viasubject)kacs.audit.handle.used(viasubject)kacs.audit.privilege.used(viasubject)kacs.caap.policy.changed(viasubject)kacs.caap.sacl.skipped(viasubject)kacs.caap.staging.diverged(viasubject)kacs.descriptor.rejected(viasubject)kacs.impersonation.reverted(viasubject)kacs.impersonation.started(viasubject)kacs.mount.policy.changed(viasubject)peinit.operation.endedpeinit.operation.mergedpeinit.operation.requestedpeinit.operation.started
B.40.16 subject.token.id #
- Type:
uint.luid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman - In groups:
caller,subject
The token's own LUID, identifying this specific token rather than the logon session it belongs to. Correlates every event produced under one token.
Carried by:
kacs.audit.access.checked(viasubject)kacs.audit.descriptor.changed(viasubject)kacs.audit.handle.used(viasubject)kacs.audit.privilege.used(viasubject)kacs.caap.policy.changed(viasubject)kacs.caap.sacl.skipped(viasubject)kacs.caap.staging.diverged(viasubject)kacs.descriptor.rejected(viasubject)kacs.impersonation.reverted(viasubject)kacs.impersonation.started(viasubject)kacs.mount.policy.changed(viasubject)lcs.audit.backup.ended(viacaller)lcs.audit.backup.started(viacaller)lcs.audit.key.created(viacaller)lcs.audit.key.deleted(viacaller)lcs.audit.key.descriptor.changed(viacaller)lcs.audit.key.hidden(viacaller)lcs.audit.key.opened(viacaller)lcs.audit.key.tombstoned(viacaller)lcs.audit.restore.ended(viacaller)lcs.audit.restore.started(viacaller)lcs.audit.transaction.committed(viacaller)lcs.audit.value.deleted(viacaller)lcs.audit.value.set(viacaller)
B.40.17 subject.token.impersonation #
- Type:
uint.enum - Values:
0 Anonymous·1 Identification·2 Impersonation·3 Delegation - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman - In groups:
caller,subject
The impersonation level of the effective token. A primary token also
reports 0, so read subject.token.type before reading 0 as Anonymous.
An impersonation token means subject.token and emitter.process
describe different principals.
Carried by:
kacs.audit.access.checked(viasubject)kacs.audit.descriptor.changed(viasubject)kacs.audit.handle.used(viasubject)kacs.audit.privilege.used(viasubject)kacs.caap.policy.changed(viasubject)kacs.caap.sacl.skipped(viasubject)kacs.caap.staging.diverged(viasubject)kacs.descriptor.rejected(viasubject)kacs.impersonation.reverted(viasubject)kacs.impersonation.started(viasubject)kacs.mount.policy.changed(viasubject)lcs.audit.backup.ended(viacaller)lcs.audit.backup.started(viacaller)lcs.audit.key.created(viacaller)lcs.audit.key.deleted(viacaller)lcs.audit.key.descriptor.changed(viacaller)lcs.audit.key.hidden(viacaller)lcs.audit.key.opened(viacaller)lcs.audit.key.tombstoned(viacaller)lcs.audit.restore.ended(viacaller)lcs.audit.restore.started(viacaller)lcs.audit.transaction.committed(viacaller)lcs.audit.value.deleted(viacaller)lcs.audit.value.set(viacaller)
B.40.18 subject.token.integrity #
- Type:
uint.integrity - Values:
0 Untrusted·4096 Low·8192 Medium·12288 High·16384 System - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman - In groups:
caller,subject
The integrity RID of the effective token. Mandatory integrity control compares this against the object's integrity label before the DACL is consulted at all, so a denial here never reaches an ACE.
Carried by:
kacs.audit.access.checked(viasubject)kacs.audit.descriptor.changed(viasubject)kacs.audit.handle.used(viasubject)kacs.audit.privilege.used(viasubject)kacs.caap.policy.changed(viasubject)kacs.caap.sacl.skipped(viasubject)kacs.caap.staging.diverged(viasubject)kacs.descriptor.rejected(viasubject)kacs.impersonation.reverted(viasubject)kacs.impersonation.started(viasubject)kacs.mount.policy.changed(viasubject)lcs.audit.backup.ended(viacaller)lcs.audit.backup.started(viacaller)lcs.audit.key.created(viacaller)lcs.audit.key.deleted(viacaller)lcs.audit.key.descriptor.changed(viacaller)lcs.audit.key.hidden(viacaller)lcs.audit.key.opened(viacaller)lcs.audit.key.tombstoned(viacaller)lcs.audit.restore.ended(viacaller)lcs.audit.restore.started(viacaller)lcs.audit.transaction.committed(viacaller)lcs.audit.value.deleted(viacaller)lcs.audit.value.set(viacaller)
B.40.19 subject.token.privileges #
- Type:
uint.flags - Values:
0x4 KACS_SE_CREATE_TOKEN_PRIVILEGE·0x8 KACS_SE_ASSIGN_PRIMARY_TOKEN_PRIVILEGE·0x10 KACS_SE_LOCK_MEMORY_PRIVILEGE·0x20 KACS_SE_INCREASE_QUOTA_PRIVILEGE·0x80 KACS_SE_TCB_PRIVILEGE·0x100 KACS_SE_SECURITY_PRIVILEGE·0x200 KACS_SE_TAKE_OWNERSHIP_PRIVILEGE·0x400 KACS_SE_LOAD_DRIVER_PRIVILEGE·0x800 KACS_SE_SYSTEM_PROFILE_PRIVILEGE·0x1000 KACS_SE_SYSTEMTIME_PRIVILEGE·0x2000 KACS_SE_PROFILE_SINGLE_PROCESS_PRIVILEGE·0x4000 KACS_SE_INCREASE_BASE_PRIORITY_PRIVILEGE·0x20000 KACS_SE_BACKUP_PRIVILEGE·0x40000 KACS_SE_RESTORE_PRIVILEGE·0x80000 KACS_SE_SHUTDOWN_PRIVILEGE·0x100000 KACS_SE_DEBUG_PRIVILEGE·0x200000 KACS_SE_AUDIT_PRIVILEGE·0x800000 KACS_SE_CHANGE_NOTIFY_PRIVILEGE·0x1000000 KACS_SE_REMOTE_SHUTDOWN_PRIVILEGE·0x10000000 KACS_SE_MANAGE_VOLUME_PRIVILEGE·0x20000000 KACS_SE_IMPERSONATE_PRIVILEGE·0x100000000 KACS_SE_RELABEL_PRIVILEGE·0x800000000 KACS_SE_CREATE_SYMBOLIC_LINK_PRIVILEGE - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The privileges the effective token holds, as the flags of its 64-bit
privilege word from uapi/pkm/token.h: bit n is the privilege whose LUID
is n, so SeDebugPrivilege is bit 20. Holding a privilege is not the same
as using it; which are switched on is subject.token.privileges-enabled.
Userspace emitters that report a caller's privileges by name today will carry this set instead.
Carried by:
B.40.20 subject.token.privileges-enabled #
- Type:
uint.flags - Values:
0x4 KACS_SE_CREATE_TOKEN_PRIVILEGE·0x8 KACS_SE_ASSIGN_PRIMARY_TOKEN_PRIVILEGE·0x10 KACS_SE_LOCK_MEMORY_PRIVILEGE·0x20 KACS_SE_INCREASE_QUOTA_PRIVILEGE·0x80 KACS_SE_TCB_PRIVILEGE·0x100 KACS_SE_SECURITY_PRIVILEGE·0x200 KACS_SE_TAKE_OWNERSHIP_PRIVILEGE·0x400 KACS_SE_LOAD_DRIVER_PRIVILEGE·0x800 KACS_SE_SYSTEM_PROFILE_PRIVILEGE·0x1000 KACS_SE_SYSTEMTIME_PRIVILEGE·0x2000 KACS_SE_PROFILE_SINGLE_PROCESS_PRIVILEGE·0x4000 KACS_SE_INCREASE_BASE_PRIORITY_PRIVILEGE·0x20000 KACS_SE_BACKUP_PRIVILEGE·0x40000 KACS_SE_RESTORE_PRIVILEGE·0x80000 KACS_SE_SHUTDOWN_PRIVILEGE·0x100000 KACS_SE_DEBUG_PRIVILEGE·0x200000 KACS_SE_AUDIT_PRIVILEGE·0x800000 KACS_SE_CHANGE_NOTIFY_PRIVILEGE·0x1000000 KACS_SE_REMOTE_SHUTDOWN_PRIVILEGE·0x10000000 KACS_SE_MANAGE_VOLUME_PRIVILEGE·0x20000000 KACS_SE_IMPERSONATE_PRIVILEGE·0x100000000 KACS_SE_RELABEL_PRIVILEGE·0x800000000 KACS_SE_CREATE_SYMBOLIC_LINK_PRIVILEGE - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Which of the privileges in subject.token.privileges are enabled, as flags
in the same bit layout. A privilege takes part in a decision only while enabled,
so a privilege that is held but disabled here could not have been what
allowed the operation.
Carried by:
B.40.21 subject.token.sid #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman - In groups:
caller,subject
The user SID of the effective token the operation ran under. Under impersonation this is the client's SID, not that of the process doing the work.
Carried by:
authd.logon.attemptedauthd.service.attestedauthd.session.endedkacs.audit.access.checked(viasubject)kacs.audit.descriptor.changed(viasubject)kacs.audit.handle.used(viasubject)kacs.audit.privilege.used(viasubject)kacs.caap.policy.changed(viasubject)kacs.caap.sacl.skipped(viasubject)kacs.caap.staging.diverged(viasubject)kacs.descriptor.rejected(viasubject)kacs.impersonation.reverted(viasubject)kacs.impersonation.started(viasubject)kacs.mount.policy.changed(viasubject)lcs.audit.backup.ended(viacaller)lcs.audit.backup.started(viacaller)lcs.audit.key.created(viacaller)lcs.audit.key.deleted(viacaller)lcs.audit.key.descriptor.changed(viacaller)lcs.audit.key.hidden(viacaller)lcs.audit.key.opened(viacaller)lcs.audit.key.tombstoned(viacaller)lcs.audit.restore.ended(viacaller)lcs.audit.restore.started(viacaller)lcs.audit.transaction.committed(viacaller)lcs.audit.value.deleted(viacaller)lcs.audit.value.set(viacaller)lpsd.account.createdlpsd.account.deletedlpsd.account.modifiedlpsd.credential.changedlpsd.credential.verifiedlpsd.group.createdlpsd.group.deletedlpsd.group.member.addedlpsd.group.member.removednetd.hostname.changedpeinit.operation.endedpeinit.operation.mergedpeinit.operation.requestedpeinit.operation.startedpeipkg.action.authorisedpeipkg.claim.changedpeipkg.package.installedpeipkg.package.uninstalledpeipkg.package.upgradedpeipkg.repository.addedpeipkg.repository.reconfiguredpeipkg.repository.refreshedpeipkg.repository.removedpeipkg.transaction.recoveredtimed.clock.stepped
B.40.22 subject.token.type #
- Type:
str.enum - Values:
primary·impersonation - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman - In groups:
caller,subject
Whether the effective token is a primary token or an impersonation token.
Needed beside subject.token.impersonation, because a primary token and
an Anonymous impersonation token both report level 0 there.
Carried by:
kacs.audit.access.checked(viasubject)kacs.audit.descriptor.changed(viasubject)kacs.audit.handle.used(viasubject)kacs.audit.privilege.used(viasubject)kacs.caap.policy.changed(viasubject)kacs.caap.sacl.skipped(viasubject)kacs.caap.staging.diverged(viasubject)kacs.descriptor.rejected(viasubject)kacs.impersonation.reverted(viasubject)kacs.impersonation.started(viasubject)kacs.mount.policy.changed(viasubject)lcs.audit.backup.ended(viacaller)lcs.audit.backup.started(viacaller)lcs.audit.key.created(viacaller)lcs.audit.key.deleted(viacaller)lcs.audit.key.descriptor.changed(viacaller)lcs.audit.key.hidden(viacaller)lcs.audit.key.opened(viacaller)lcs.audit.key.tombstoned(viacaller)lcs.audit.restore.ended(viacaller)lcs.audit.restore.started(viacaller)lcs.audit.transaction.committed(viacaller)lcs.audit.value.deleted(viacaller)lcs.audit.value.set(viacaller)
B.40.23 subject.token.uid #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman - In groups:
subject
The Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data. A projection for interoperability, never the identity KACS decides on.
Carried by:
kacs.audit.access.checked(viasubject)kacs.audit.descriptor.changed(viasubject)kacs.audit.handle.used(viasubject)kacs.audit.privilege.used(viasubject)kacs.caap.policy.changed(viasubject)kacs.caap.sacl.skipped(viasubject)kacs.caap.staging.diverged(viasubject)kacs.descriptor.rejected(viasubject)kacs.impersonation.reverted(viasubject)kacs.impersonation.started(viasubject)kacs.mount.policy.changed(viasubject)
B.40.24 subject.true-token.sid #
- Type:
bin.sid - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The user SID of the acting thread's own token, before any impersonation.
Carried on records about impersonation, where subject.token.sid is the
client being impersonated and this is the server doing the impersonating.
Equal to subject.token.sid when the thread is not impersonating.
Carried by:
No event carries this field yet.
Generated from kernel.evman, peinit.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.41 tag.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under tag, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.41.1 tag.amount #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The amount a tag operation applied: the value a set wrote, or the amount
an add added. Both default to 1 when the rule names none. Absent for
clear.
Carried by:
No event carries this field yet.
B.41.2 tag.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The name of a flow tag, as the policy author wrote it. Not emitted
today: the tag store is passed only the tag's hash, machinery.hash, and
the name lives in the published policy. A hash names the same tag only
within one generation.
Carried by:
No event carries this field yet.
B.41.3 tag.operation #
- Type:
str.enum - Values:
set·clear·add - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
Which operation a TAG action applied to a flow tag. clear removes the
tag so that it reads as absent; clearing an absent tag does nothing.
Carried by:
No event carries this field yet.
B.41.4 tag.value #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The value a flow tag holds. An add that would overflow leaves it at the
largest representable value, where it stays.
Carried by:
No event carries this field yet.
Generated from ntfe.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.42 test.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under test, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.42.1 test.rendezvous.action #
- Type:
str.enum - Values:
none·hold·fail - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
stratafs.evman
What a test rendezvous point is set to do to the next task that reaches it.
hold blocks every task that arrives until the point is cleared. fail
makes the next arriving task's step fail with a chosen error, once, then
reverts to none, which leaves tasks unaffected.
Carried by:
No event carries this field yet.
B.42.2 test.rendezvous.name #
- Type:
str - Asserted: no
- Carried in: the payload
- Defined in:
stratafs.evman
The test rendezvous point a record concerns, one of copy-up-begin,
copy-up-publish, rename-provider and link-install. Rendezvous points
exist only in a kernel built with test hooks and booted with
stratafs.test_hooks=1, and let a conformance test hold an operation open
mid-flight or make one internal step fail. A production system never
records one.
Carried by:
No event carries this field yet.
B.42.3 test.rendezvous.waiters #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
stratafs.evman
The number of tasks blocked at a test rendezvous point, at the moment of
the record. A test polls it to know its target has arrived before acting
on the held state. Non-zero only while the point is set to hold.
Carried by:
No event carries this field yet.
Generated from stratafs.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.43 transaction.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under transaction, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.43.1 transaction.commit-outstanding #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether a transaction's commit had been sent to its source and not yet answered. When this is true and the transaction then times out or its source goes down, nobody can say whether the changes were applied: the source may have committed them before the reply was lost.
Carried by:
B.43.2 transaction.id #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The transaction the operation belonged to, registry or package. A pure correlation identifier: every record of one transaction carries the same value, from begin to commit or abort. The registry and the package manager number their transactions independently, so an identifier is unique only within one of them; which it belongs to follows from the event type.
Carried by:
lcs.audit.key.createdlcs.audit.key.deletedlcs.audit.key.descriptor.changedlcs.audit.key.hiddenlcs.audit.key.tombstonedlcs.audit.transaction.committedlcs.audit.value.deletedlcs.audit.value.setpeipkg.claim.changedpeipkg.package.installedpeipkg.package.uninstalledpeipkg.package.upgraded
B.43.3 transaction.position #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The ordinal of a mutation within its registry transaction, counting from 1 in the order the mutations were made. Correlates one operation with its place in a commit that applies many.
Carried by:
No event carries this field yet.
B.43.4 transaction.state #
- Type:
str.enum - Values:
active-unbound·active-bound·committed·aborted·timed-out·source-down - Set: open
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The state of a registry transaction, from the REG_TXN_* states. A
transaction is active-unbound until its first mutation binds it to a
source and hive, then active-bound. The other four are terminal:
committed, aborted, timed-out when it passed its deadline, and
source-down when the source holding it went away.
timed-out and source-down do not by themselves say whether the
transaction's changes landed; see transaction.commit-outstanding.
Carried by:
Generated from kernel.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.44 trigger.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under trigger, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.44.1 trigger.ace #
- Type:
bin.ace - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The exact ACE that caused this record, so a consumer can identify which rule
fired. Absent when trigger.kind is policy, because no ACE was involved.
Carried by:
B.44.2 trigger.fail-closed #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether a failure to write this audit record may fail the operation it describes. True where the record is mandatory and the operation is refused rather than left unrecorded; false where emission is best effort.
Carried by:
No event carries this field yet.
B.44.3 trigger.file.path #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The path of the file that caused the record, where that is not the object the record is about. Two cases: an operation on one object that had a side effect elsewhere, such as the ancestor directories a copy-up materialised; and an ancestor whose type masked the subtree the object sits in.
Carried by:
No event carries this field yet.
B.44.4 trigger.kind #
- Type:
str.enum - Values:
sacl·policy - Set: closed
- Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Why an audit record exists at all. sacl means an audit ACE on the object,
or in a central access policy, matched this access. policy means nothing
in any SACL asked for it and the calling token's own audit policy forced it.
The distinction is how volume gets diagnosed: a flood of policy is a
property of the token and is fixed on the token, while a flood of sacl is
a property of the object and is fixed on its SACL.
Carried by:
B.44.5 trigger.open-audit #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the object's SACL required the open itself to be audited. The registry writes its key-open audit record only when this is true, so it is the reason such a record exists.
Carried by:
No event carries this field yet.
B.44.6 trigger.privilege-audit #
- Type:
bool - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Whether the use of a privilege required its own audit record.
Not emitted today. The registry computes it during the open check and carries it across to the kernel's C side, where nothing reads it, so no privilege-use record is produced for a registry open.
Carried by:
No event carries this field yet.
B.44.7 trigger.sacl-match #
- Type:
uint.flags - Values:
0x1 success-audit matched·0x2 failure-audit matched - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
Which flavour of audit ACE matched. No other bits are ever set. Distinct
from trigger.kind, which says whether a SACL was involved at all — this
says which side of one fired, and both bits can be set at once.
Carried by:
B.44.8 trigger.stratum.index #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The position, within the mount's stack, of the stratum whose contents
blocked an operation. Present exactly when trigger.stratum.path is.
Carried by:
No event carries this field yet.
B.44.9 trigger.stratum.path #
- Type:
str.path - Asserted: no
- Carried in: the payload
- Defined in:
kernel.evman
The filesystem path of the stratum whose contents blocked an operation: the layer holding the entry that made a creation collide, a directory non-empty, or a name the wrong type. Without it a refusal names the error but not which layer an administrator must look in to clear it.
Carried by:
No event carries this field yet.
Generated from kernel.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.
B.45 verdict-log.*
Peios / Using Peios / Events / Field Index
Every field the catalogue defines under verdict-log, in path order. Each entry gives the field's definition and, under Carried by, every event type that writes it.
B.45.1 verdict-log.capacity #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
How many verdict records NTFE's private ring holds before the oldest is overwritten.
Carried by:
No event carries this field yet.
B.45.2 verdict-log.lost #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
How many verdict records the private ring has destroyed by overrun since boot. With no reader attached the ring wraps within moments under real traffic, so a large count usually means nothing was reading rather than that something was slow.
Carried by:
No event carries this field yet.
B.45.3 verdict-log.sequence #
- Type:
uint - Asserted: no
- Carried in: the payload
- Defined in:
ntfe.evman
The sequence number of a verdict record on NTFE's private ring. It rises by one per record from boot, so a gap is the confessed evidence of records lost to overrun, visible only to a reader that was present to see it.
Carried by:
No event carries this field yet.
Generated from ntfe.evman by pkm/tools/gen-events-book.py. Edit the fragment, not this page.