Peios Learn
Products
PePeios pkpekit PvProvium UDUniversal Directory TrTrail Ckclackatoo PrProject WiWispist
Using Peios Security Basics Technical Documentation Source
Using Peios Security Basics Technical Documentation Source
Peios

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 #

  1. 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.
  2. 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.
  3. 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 lps reference in a terminal.
  • Sign in remotely from a terminal or manage SSH keys: Signing in over SSH.
  • Change your own password: The passwd command.
  • 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 net command 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 #

  1. Check the address and port against Sign in from a browser. A machine configured to listen only on 127.0.0.1 is not reachable from another device.
  2. If the browser shows a certificate warning, follow The machine's certificate. Do not enter credentials when the certificate fingerprint does not match.
  3. At a console you can still reach, use net status to inspect the address, link and readiness. Check the GXWI service, gxwid, through Controlling services.
  4. 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.

Caution

Writing the USB medium erases the USB stick. Installing Peios erases the entire target disk, including other operating systems and files. Back up anything you need from both devices before starting. The installer does not offer a dual-boot or encrypted-disk layout.

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 .iso image. 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.

Warning

Ventoy and similar multi-ISO tools are not supported. Peios looks for a whole disk carrying an ISO9660 filesystem with rootfs.squashfs on it. A stick that only contains the .iso as a file can stop at no boot medium carrying /rootfs.squashfs found.

On Linux:

  1. Identify the stick by size, model, USB connection, and removable status (RM is 1):

    lsblk -o NAME,SIZE,TRAN,MODEL,RM
    ls -l /dev/disk/by-id/ | grep usb
    
  2. Unmount any of the stick's filesystems that the desktop mounted. Otherwise pending filesystem writes can overwrite part of the new image later.

  3. 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.

    sudo dd if=peios.iso of=/dev/disk/by-id/usb-REPLACE_WITH_VERIFIED_STICK_ID bs=4M oflag=direct conv=fsync status=progress
    
  4. 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:

lspci          # network, graphics, and storage controllers
net status     # interfaces and their assigned addresses

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 #

SymptomCheck or next step
Firmware reports a security violation or refuses to start the stickCheck that Secure Boot is off.
no boot medium carrying /rootfs.squashfs found after 50 attemptsCheck 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 menuCheck the raw image write and that firmware allows UEFI boot rather than only legacy boot.
The installer cannot see an internal diskLook 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:

cat /var/state/gxwi/certificate.sha256

Do not enter credentials if the fingerprints differ. See the machine's certificate for verification and browser-trust details.

  1. Choose the installation operation.
  2. Select the internal disk by its device, model, size, and bus. The USB medium is marked and cannot be selected.
  3. 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.
  4. Confirm the install. In the browser, hold down the button that starts it.
  5. 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.

Note

Peios 2026.8: after assigning a new address to the interface your browser uses, the setup page may stay on its starfield rather than follow the machine over HTTPS. Once setup finishes, open https://<new-address>:7780 yourself. The browser can warn again at the new address; compare the certificate with the fingerprint you verified. The general first-boot guide describes automatic reconnection, so keep this release-specific workaround in mind.

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 #

MachineDocumented 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 requiredPassword requiredNo such principal
login alicesigned inpromptsuser alice does not exist
login --try alicesigned inprompts, then falls back if wrongfalls back silently
login --try-no-password alicesigned infalls back silentlyfalls 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 #

OptionEffect
--try <name>Attempt name, falling back to a full prompt.
--try-no-password <name>Attempt name collecting nothing, falling back to a full prompt.
--consoleReset 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.
-pKeep the inherited environment instead of building a fresh one.
-h <host>Record the remote peer for a sign-in originated on its behalf. Unverified.
-HAccepted 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.

CodeMeaning
1A 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 #

MessageMeaning
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 #

CodeMeaning
0The password was changed.
1The change was refused or failed, or the authority could not be reached.
2The command line was wrong.

See also #

  • How authentication works — the conversation this is one kind of.
  • The lps command — 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:

SettingWhere it isShipped as
Whether it runsthe sshd service's Disabled valueon, on images that include the service
Its portMachine\Software\OpenSSH Port (REG_DWORD)22
Whether it accepts passwordsMachine\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 #

  1. 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 fingerprint
    

    Check that the server is running. Choose an address on an interface reachable from the other computer; Reading net status explains the interface blocks. For a direct connection, use the port shown by ssh-settings, which is 22 as shipped. The QEMU Makefile's host-forwarded port 2222 is a separate route described in the build quick start.

  2. 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.

  3. 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 Port value;
  • sshd's port reservation, the descriptor under Machine\System\Network\TcpIp\PortReservations that lets only sshd bind the port (see port reservations);
  • the ssh firewall rule, Machine\System\Network\Rules\Flow\ssh, whose DstPort.Equal is 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:

  1. 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 sets lps policy <name> either; retain that policy if GXWI password access is needed.
  2. Inspect lps show <name> and lps key list <name> to check the enabled account, enrolled key, credential policy and permitted remote-interactive logon 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.
  3. 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 sshd and 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 #

CodeMeaning
0Done, or shown.
1The change was refused or failed; the reason is printed.
64The 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 #

RIDThe next one. Never chosen, never reused.
uidThe RID, plus this machine's base — so RID 1000 signs in as 1001000.
Primary groupAuthenticated 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:

OptionEffect
--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.
--disabledCreate it unable to sign in.
--no-passwordCreate a principal that authenticates with no credential at all. See below.
--serviceCreate a principal that exists to run a service, and can do nothing else. See below.
--no-promptFail 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
TypeSigning in
interactiveat this machine: its console or desktop
remote-interactiveto a remote desktop or over SSH, including commands without a PTY and SFTP
networkto a network resource, such as a file share
network-cleartextover the network, with the password sent to this machine
batchas a scheduled job
new-credentialsas a second identity for outgoing connections
serviceas 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
PolicySigns in with
passwordtheir password
keyone of their SSH keys
eithertheir password or one of their SSH keys
nonenothing, where credential-free sign-in is permitted; SSH refuses this policy
deniednothing: 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 key or none, or one whose policy is password but 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 with lps key.
  • A key is removed by its fingerprint, the SHA256: value ssh-keygen -l prints for your .pub file, rather than by lps key list's ID.

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
OptionEffect
--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
TypeValues
int64Signed integers
uint64Unsigned integers
booleantrue/yes/1 or false/no/0
stringText
sidSIDs, in S-1-… form
octetHexadecimal, 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 #

CodeMeaning
0Succeeded
1lpsd refused the request, or could not be reached
2The 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 Administrators or Users. 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 .pub file, 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 Everyone or Network, 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 authd could 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.

StateWhat a principal gets
Key absentThe compiled floor — Everyone gets SeChangeNotifyPrivilege, and nothing else
Key presentExactly what the records say
Key present but unreadableNothing, 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:

PrincipalPrivilegesIntegrity
EveryoneSeChangeNotifyPrivilege, SeCreateSymbolicLinkPrivilege(Medium, by default)
Administratorsthe operational set below, plus both of the aboveHigh

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.
  • Owner and DefaultDacl have 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:

NameValue
Untrusted0
Low4096
Medium8192
High12288
System16384

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 #

  1. Open Processes. Use Find for a name, PID, person or service, or sort CPU or Memory to investigate resource use.
  2. 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.
  3. 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.
  4. 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.

TaskCommand
Read a process's protection, mitigations and GUIDlogonse psb --pid PID
List signed-in sessions and their visible processeslogonse list
Read a service's status and main processsvctl status NAME
Stop a servicesvctl stop NAME
List submitted jobssvctl job list
Read a submitted job's statussvctl job status ID
Stop a submitted jobsvctl job stop ID
Sign a session outlogonse end ID
Ask one process to terminatekill 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 stopUseWhy
One running processTask Manager's End process, or kill PIDAsks that process to end; unsaved work may be lost.
A managed service and its processesStop service, or svctl stop NAMEEnding only its main process can look like a crash and cause a restart.
A submitted jobStop job, or svctl job stop IDAsks 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 hasWhat to look for
Private memoryIts working space, shared by its own threads and separate from other processes.
Open resourcesFiles, connections, and other resources it holds open.
An identityWho it runs as; its token controls what it may reach.
A place in a family treeIts parent, shown by a parent PID (PPID).
A lifecycleWhether it is running, sleeping, stopped, or has finished.
One or more threadsThe 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 threadsWhat that means in practice
The process's memoryOne thread can read and change data another thread is using — cooperation is cheap, but concurrent access must be coordinated.
The open resourcesA file one thread opens is usable by all of them.
The identityBy 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

Warning

KILL gives a program no chance to save work or run its cleanup. Recheck the current process before acting on the PID again: an exited process's number can be reused. A process in uninterruptible sleep cannot finish terminating until the operation it is waiting on finishes.

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 PID shows a process's protection, mitigations and GUID, and with --mitigations turns mitigations on.
  • On the desktop, Task Manager shows them for the process you pick.
  • From a program, read /proc/<pid>/psb, or call peios_process_psb in 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.

ActionProcess right
Read the PSBPROCESS_QUERY_LIMITED
Read detailed process informationPROCESS_QUERY_INFORMATION
Send terminating signals, including TERM and KILLPROCESS_TERMINATE
Suspend or resume with STOP or CONTPROCESS_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 needStart here
Check, start, stop, restart, or reload a serviceControlling services
Understand Failed, Backoff, Skipped, or AbandonedRead service states and Troubleshooting
Find output from a failed run or hookService output and logging
Create a service or change its settingsDefining a service
Change automatic starts or a scheduleTriggers and timers
Diagnose a reduced boot or reboot loopBoot modes
Shut down or reboot deliberatelyShutdown

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 logsMeaningWhat to inspect
ServiceA named definition and its current state.svctl status <service>
JobOne 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.
OperationOne 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
DetailHow to use it
state, causeTell whether the service is running, waiting to retry, skipped, or failed, and why. See service states.
status_text, progressThe latest information the service reported. These clear on restart and may be absent.
current_jobIdentifies this execution. Use its GUID to separate its logs from previous runs.
current_operationIdentifies an action in progress. Poll its ID with svctl operation-status <id>.
healthhealthy, unhealthy, unknown (no check result yet), or null (no check configured).
warningsNotices such as leaked cgroups. Read these before trying another start.
definition_removedA deleted definition still has a draining instance. stop still works; start, restart, and reload do not.
grantedYour query, start, stop, and reload rights. A visible service does not imply permission to change it.
timersNext 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. list omits 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 BindsTo dependent stops with its target; a Requires dependent 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-config instead 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 #

CommandDoesRequired right
startRun the service through its full start sequence.SERVICE_START
stopSIGTERM, then SIGKILL after StopTimeout.SERVICE_STOP
restartStop then start, as one operation.SERVICE_STOP + SERVICE_START
reloadRe-read configuration (ExecReload, or SIGHUP).SERVICE_INTERROGATE
resetClear Failed/Abandoned/Skipped → Inactive.SERVICE_STOP
statusReport state, cause, PID, uptime, health, current job and operation, warnings, and when each timer fires.SERVICE_QUERY_STATUS
listList 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.

CommandDefaultWaits for
startwaitActive (Simple) / Completed or Inactive (Oneshot), or Failed
stopwaitInactive
restartwaitthe successful start target, or Failed
reloadno-wait(with --wait) the Reloading state to resolve
resetimmediate—

reload is the exception — it returns immediately by default, because a reload may have no observable completion. With --wait, the response carries a mode:

modeMeaning
confirmedThe service signalled READY=1 (and, for a command reload, the command exited 0).
advisoryThe reload was issued and the detection window elapsed without explicit confirmation.
failedThe 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.

Note

A connection blocked on a --wait operation is not counted as idle, so it is not closed by ConnectionTimeout. It stays open until the operation resolves, bounded by the operation's own timeout (e.g. StartTimeout).

System commands #

CommandDoesRequired right
shutdown <type>Graceful shutdown. type is poweroff, reboot, or halt.SYSTEM_SHUTDOWN
bootReport this boot’s mode, reason, and confirmation status.SYSTEM_QUERY_STATUS
reload-configRe-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.

CommandSocketDoesRequired right
job list [filters]controlList 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_IDcontrolThe job view.JOB_QUERY
job stop JOB_IDcontrolSIGTERM, 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...]jobsSubmit 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_IDjobsBlock 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 SIGNALjobsSend 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:

CommandInactiveStartingActiveReloadingStoppingCompletedBackoffFailedAbandonedSkipped
startStartMERGEALREADYALREADYQUEUEStartDEFERStartERRORStart
stopNOOPCancel+StopStopStopMERGEClearCancelNOOPERRORNOOP
restartStartQUEUERestartRestartQUEUEStartRestartStartERRORStart
reloadERRORERRORReloadMERGEERRORERRORERRORERRORERRORERROR
resetNOOPERRORERRORERRORERRORERRORERRORClearClearClear
statusOKOKOKOKOKOKOKOKOKOK

Legend:

  • MERGE — an operation of this type is already running; your command merges into it and you get its GUID.
  • DEFER — your start is 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:

CodeMeaning
ACCESS_DENIEDAccessCheck denied the command against the target descriptor.
UNKNOWN_SERVICENo such service definition (also returned for start/restart/reload on a definition-removed service).
UNKNOWN_OPERATIONNo such operation GUID — never existed, or dropped after its retention grace.
UNKNOWN_JOBNo such job GUID — never existed, or dropped after its retention grace. A known job you cannot query returns ACCESS_DENIED.
MALFORMED_REQUESTThe request line is not a single valid JSON object.
REQUEST_TOO_LARGEThe request exceeds MaxRequestSize.
INVALID_COMMANDThe command field is missing or unknown.
INVALID_ARGUMENTSA required field is missing or malformed (e.g. shutdown with no valid type).
INVALID_STATENot valid for the service's current state (an ERROR cell above), or rejected because the system is already shutting down.
OPERATION_TIMEOUTA --wait operation did not reach a terminal state within its timeout.
INTERNAL_ERRORpeinit hit an internal failure executing the command.

The jobs socket uses the same envelope and codes, plus two of its own:

CodeMeaning
QUOTA_EXCEEDEDThe submission would exceed MaxJobsPerSubmitter live jobs for your SID. Nothing was created.
BAD_TOKENThe 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 #

CodeMeaning
0The command succeeded. For job submit --wait, the job also completed.
1peinit refused the command — an access denial, unknown service or job, invalid state, or timeout — or a job submit --wait job ended other than completed.
64A usage error.
69peinit could not be reached — the socket is missing or refused the connection.
70A 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\:

KeyDefaultLimits
MaxControlConnectionsPerUser16Concurrent client connections one user may hold; SYSTEM is exempt (excess are refused at connect).
MaxControlConnections256Concurrent client connections in all (excess are refused at connect).
MaxRequestSize65536Bytes per request.
ConnectionTimeout30Seconds an idle connection may sit before it is closed.

The jobs socket has its own set, under the same key:

KeyDefaultLimits
MaxJobsConnections64Concurrent jobs-socket connections (excess are closed at connect, with no response).
MaxJobMessageSize32768Bytes per message — which bounds a submission's whole definition.
JobsConnectionTimeout30Seconds an idle jobs connection may sit before it is closed. A connection blocked on a wait or a pending submit is not idle.
MaxJobsPerSubmitter64Live 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 #

  1. Run svctl status <service> and note its state, cause, current job ID, and warnings.
  2. 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.
  3. Match the job GUID to isolate one execution. A restart has a new job ID.
  4. 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.

Warning

A service with TTYPath sends all three standard streams to its terminal. Its output is not captured for eventd. Check the terminal or change the service’s output arrangement if you need a log record.

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:

RecordPathWhat to expect
Service stdout/stderrpeinit’s output buffer and eventd’s log socketBest-effort; lines can be truncated or dropped under load.
Job, operation, access-check, and failure eventsKMES, then eventdIndependent 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:

CauseWhat happenedWhat to do
ValidationErrorThe 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.
DependencyFailureA 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.
AssertionErrorA start-time assert failed — a required precondition is missing.Create the missing precondition (path, file, directory, registry key), then start again.
PreHookFailureAn 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.
ParentSetupFailurepeinit 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.
PreExecFailureSetup after fork failed — token install, rlimits, environment.Usually identity: check Identity, that authd is up, and that RequiredPrivileges names real privileges.
ReadinessTimeoutThe 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).

Tip

A startup failure (ReadinessTimeout, PreHookFailure, PreExecFailure, ParentSetupFailure) is restart-eligible. Whether it was retried depends on the configured policy and budget; Failed alone does not prove multiple attempts occurred. Read the restart policy and the logs for the actual attempts.

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 Failed with RestartBudgetExhausted. It crashed RestartMaxRetries times faster than it could stay healthy for RestartWindow. 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 RestartWindow between 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=Always restarts even successful exits (cause CleanExitRestart). If the service is meant to exit, use OnFailure instead.

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 on restart.
  • 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 (start needs SERVICE_START, restart needs both stop and start, …).
  • shutdown and reload-config need SYSTEM_SHUTDOWN / SYSTEM_RELOAD_CONFIG in 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.

Note

If list shows fewer services than you expect, that is not a bug — list omits services you lack SERVICE_QUERY_STATUS on rather than denying them. You are seeing exactly what your token can see. job list does the same with JOB_QUERY.

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:

CodeWhat happenedWhat to do
connection refused, or an immediate closeYou 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_EXCEEDEDYour 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_TOKENThe 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_ARGUMENTSThe 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_STATEThe 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/BindsTo dependent 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. Use Readiness=Notify only if the program supports sending READY=1 from its tracked main process when ready; changing the setting does not add that support and can cause ReadinessTimeout. 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 Wants dependent waits for the optional target’s startup attempt to resolve, but starts even if the target failed. Use Requires when 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:

  1. Preserve peinit's console failure reason and the source's startup diagnostics. A failed registry operation does not prove the source process has exited.
  2. Identify the installed registryd provider and version, its hive declarations and actual database paths. LCS and sources explains the default loregd provider and its diagnostics.
  3. 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.
  4. 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 #

StateProcess?Satisfies dependents?What it means when you see it
InactiveNoNoNot started, or stopped cleanly and not set to restart. The neutral resting state.
StartingMaybeNoActivation in progress — conditions, hooks, fork, or readiness wait. Dependents are blocked.
ActiveYesYesRunning and ready. The normal state of a healthy Simple service.
ReloadingYesYesRe-reading configuration. Still counts as running.
StoppingYes (briefly)NoSIGTERM sent; waiting for exit or SIGKILL escalation.
CompletedNoYesOneshot finished successfully. Stays here only with RemainAfterExit=1.
BackoffNoNoA restart is pending; the service is waiting out its backoff delay before the next attempt.
FailedNoNoExited abnormally and restart policy is exhausted or not configured.
AbandonedYes (unkillable)NoSIGKILL was sent but the process survived in uninterruptible sleep (D-state). peinit has given up supervising it; its cgroup is leaked.
SkippedNoYesA 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 start honours the existing delay; stop cancels 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. reset clears 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

CauseMeaning
ExplicitStartAn administrator or a trigger started it.
DependencyStartStarted to satisfy another service's dependency.
RestartPolicyAn automatic restart, after a backoff delay.
BindsToRecoveryA bound target returned to Active; the dependent is auto-restarted. Does not consume the restart budget.

Why a service stopped

CauseMeaning
ExplicitStopAn administrator requested stop.
ConflictEvictionA conflicting service started and won.
BindsToPropagationA bound target stopped, so this one stops too.
ShutdownWaveThe system is shutting down.

Why a service failed or restarted — the diagnostic causes

CauseMeaning
ProcessCrashThe main process exited non-zero or died on a signal.
CleanExitA Simple process exited successfully, and policy is not Always — not a crash; goes straight to Inactive.
CleanExitRestartA Simple process exited successfully but RestartPolicy=Always, so it is restarted. Logged clearly as not a crash.
ReadinessTimeoutStartTimeout expired before the service became ready.
WatchdogTimeoutA WATCHDOG=1 keepalive did not arrive in time.
HealthCheckFailureHealthCheckRetries consecutive health checks failed.
PreHookFailureAn ExecStartPre hook exited non-zero.
ParentSetupFailurepeinit could not even fork — resource exhaustion, cgroup error. No child was created.
PreExecFailureSetup after fork but before exec failed (token install, rlimits, environment).
RestartBudgetExhaustedRestartMaxRetries reached within the window; no more retries.

Why a service was rejected — definition and graph problems

CauseMeaning
ValidationErrorThe definition failed validation (bad field, unresolvable conflict, illegal health-check timing…).
CycleDetectedThe service is part of a dependency cycle.
DependencyFailureA Requires dependency entered Failed or does not exist.
AssertionErrorA start-time Assert failed.
ConditionSkippedA start-time Condition was not met → Skipped (this is not a failure).
TtyUnavailableAnother service was holding the TTYPath this one names → Skipped (also not a failure). See Triggers and timers.
ProcessUnkillableThe process survived SIGKILL (D-state) → Abandoned.
ExplicitResetAn 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.

Tip

When a service is Failed, the cause is the first thing to read. A ProcessCrash or WatchdogTimeout points at the service's own code or health; a DependencyFailure points at something it needs; a ValidationError points at its definition. Troubleshooting peinit is organised around exactly this lookup.

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 → Starting is the retry; Failed is reached only when restarts are exhausted or disabled. This is why OnFailure fires once at the end, not on every retry.
  • Starting can fail before any process exists — a parent-side setup error, a failed pre-hook, a condition or assert. The cause records which.
  • Failed → Starting is how a manual start (or a recovery path) revives a dead service; automatic restarts never originate from Failed.
  • Stopping has two exits. A clean stop or a shutdown lands in Inactive. But a service stopped because it lost a Conflicts race (ConflictEviction) or because its BindsTo target went away (BindsToPropagation) comes to rest in Failed, carrying that cause — so an evicted or bound-out service shows as Failed in status and needs a reset (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.

Note

A crash while Reloading is still a crash. If a service's main process dies while it is re-reading its configuration, peinit treats that as an ordinary ProcessCrash and routes it through the restart policy — Reloading → Backoff if a restart is warranted, or Reloading → Failed if it is not. This is why you can see a service jump straight from Reloading into Backoff or Failed. It is distinct from an ExecReload command failing: a failed reload command leaves the running process untouched and the service stays Active, reporting the failure without changing state.

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:

  1. A service is in exactly one state at any moment.
  2. Only the transitions peinit defines are valid — there are no others.
  3. Only peinit changes a service's runtime state. No external process can reach in and set it.
  4. A service's securable identity (its ServiceSecurity descriptor — who can manage it) is independent of its process identity (its token — what it can access).
  5. Readiness is per start generation — a READY=1 from 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_job and current_operation give the GUIDs of the service's active execution and active action (or null).
  • operation-status <id> — polls one operation to a terminal state; a --wait lifecycle 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:

TypeCreated for
ServiceMainA service's main process.
PreExecHookOne ExecStartPre command.
PostExecHookOne ExecStartPost command.
HealthCheckOne health-check run.
SubmittedA program submitted over the jobs socket.

Their lifecycle is simpler than a service's, because a job only tracks a process — not policy:

StateMeaning
CreatedThe job object exists but the process is not forked yet (pre-hooks may be running).
RunningThe process is alive.
CompletedThe process exited successfully.
FailedThe process failed — or peinit classified the job failed before fork (a parent-side setup error).
AbandonedThe 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:

SourceCreated by
AdminA control client.
BootThe Phase 2 boot lifecycle.
ShutdownThe shutdown lifecycle.
DependencyPropagationA start pulling in a dependency.
RestartPolicyAn automatic restart.
TimerA timer firing.
BindsToRecoveryA bound target returning to Active.
BindsToPropagationA bound target stopping.
ConflictResolutionA Conflicts eviction.
OnFailureA 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
StateMeaning
PendingValidated and queued, waiting on a precondition (e.g. a prior stop to finish).
RunningExecuting — the service is transitioning.
CompletedAchieved its goal (start reached Active/Completed; stop reached Inactive/Failed).
FailedDid not achieve its goal — including timing out while still Pending.
MergedFolded into an identical operation already in flight (records the survivor's GUID).
CancelledTerminated while still Pending — never ran.
AbortedTerminated 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. backupd takes a request from a user, and wants the backup to run as that user, in its own cgroup, with a timeout, reporting progress, visible in job list — but supervised by peinit rather than by backupd itself.
  • 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.

RightGrants
JOB_QUERYRead the job view; wait on it; see it in job list.
JOB_STOPStop the job.
JOB_SIGNALSend 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:

CauseMeaning
parent_setup_failurepeinit could not prepare the launch — no token, no cgroup, no fork. No process existed.
pre_exec_failureSetup between fork and exec failed, or execve did.
readiness_timeoutA notify job never sent READY=1 in time.
timeoutThe job ran longer than timeout.
explicit_stopA stop or job stop was issued.
shutdownThe system was shutting down.
process_unkillableThe 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 #

  1. Read the current definition. Decide which field needs to change and when it will apply.
  2. Use svctl definition edit or Edit definition… in Services Manager. Check the reported changes before restarting a running service.
  3. Validate the saved definition with svctl definition validate <service>. This checks fields; identity, privilege names, and dependency targets also need their runtime checks.
  4. If launch settings require it, restart the service during an appropriate interruption window. svctl reload-config re-reads definitions but does not replace a running process.
  5. Run svctl status <service> again. If it fails, inspect the cause and logs.

Warning

A definition controls what runs and under which identity. Keep definition-write access limited to trusted administrators. Grant runtime start/stop rights through ServiceSecurity, rather than giving an operator permission to rewrite the program or its identity.

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;
    • yes or no;
    • a choice by its name (Type=Oneshot);
    • a list with one --set for 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.
  • edit names 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 DELETE on 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.

ClassWhen a change takes effectFields
Immutable at runtimeNext restart only.ImagePath, Type, Identity, RequiredPrivileges, ErrorControl, RemainAfterExit, and, while it is running, Triggers and Disabled
Apply on next startNext start or explicit graph reload — not while running.Requires, Wants, BindsTo, Conflicts, OnFailure, Conditions, Asserts
Hot-reloadedNext relevant event, no restart.ServiceSecurity (next control request)
Reloadable at runtimeNext 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.

Note

reload-config is the explicit, atomic way to make peinit re-read everything. It builds and validates a complete new graph snapshot and swaps it in only if validation passes; running services are not disturbed. It is covered with the rest of the command set in Controlling services.

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.

Important

Removing a definition is not how you stop a service. It stops future management, not work in progress. To actually stop a running service, stop it. To prevent it from auto-starting, set Disabled=1 (see Triggers and timers). To prevent it from being started at all, deny SERVICE_START in its ServiceSecurity descriptor.

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.

FieldDefaultPurpose
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.

FieldDefaultPurpose
TypeSimpleSimple (long-running) or Oneshot (run-to-completion).
ReadinessNotifyNotify (READY=1 via sd_notify) or Alive (ready when the process exists). Ignored for Oneshot.
RemainAfterExit0Oneshot 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.

FieldDefaultPurpose
Triggers—boot, boot:settled, timer:<schedule>, tty:released. Absent = demand-only.
Disabled0If 1, triggers must not activate the service (manual start still allowed).
SafeMode0Eligible 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.

FieldDefaultPurpose
IdentityLocalServicePrincipal name, SID, or Service for a distinct virtual service account.
RequiredPrivileges—Privilege allow-list; everything else is stripped from the token.
HookIdentityservice's IdentityIdentity for ExecStartPre/ExecStartPost hooks.

How it relates to other services — dependencies. See Dependencies and ordering.

FieldPurpose
RequiresHard dependencies — must be satisfied first; their failure fails this service.
WantsSoft dependencies — attempted first if present and enabled, but their failure does not block this service.
BindsToRuntime coupling — if the target stops, this stops too.
ConflictsMutual exclusion — starting this stops the named services.
OnFailureService to start when this one enters Failed.

How it is kept alive — supervision and health. See Keeping services running.

FieldDefaultPurpose
ErrorControlNormalNormal (stay Failed) or Critical (sync + reboot on irrecoverable failure).
RestartPolicyOnFailureNever / OnFailure / Always.
RestartMaxRetries, RestartWindow, RestartDelay5 / 120 / 1Restart budget, the window of health that resets it, and the backoff base.
HealthCheck, HealthCheckInterval, HealthCheckTimeout, HealthCheckRetries— / 30 / 5 / 3Active health-check command and its timing.
WatchdogTimeout0Expected interval between WATCHDOG=1 pings; 0 disables.

The transition phases — hooks and timeouts. See The execution environment and The service lifecycle.

FieldDefaultPurpose
ExecStartPre, ExecStartPost—Commands run before the binary / after readiness.
ExecReload(SIGHUP)Reload command or signal:<NAME>.
StartTimeout, StopTimeout30 / 10Seconds for the whole start sequence / between SIGTERM and SIGKILL.

The remaining knobs — scheduling, notify, fds, metadata.

FieldDefaultPurpose
TimerPersistent, TimerJitter1 / 0Catch up missed timer runs after reboot / random delay per firing.
NotifyAccessMainWho may send sd_notify messages (only Main is supported).
FdStoreMax0Size of the per-service fd store; 0 disables it.
ServiceSecurityinheritSecurity 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 #

SimpleOneshot
Process lifetimeLong-running; is the serviceRuns once, exits
ReadinessNotify (READY=1) or AliveSuccessful exit — Readiness ignored
Satisfies dependents whenActiveCompleted (with or without RemainAfterExit)
Successful end stateActive (until stopped)Completed, then Inactive (unless RemainAfterExit=1)
ExecStartPost firesAfter readinessAfter successful exit only
StartTimeout coversStartup until readinessThe whole execution
Watchdog & health checksSupervised while ActiveNone — WatchdogTimeout/HealthCheck are inert
Restart on successn/aNever (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 #

ReadinessReady 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.
AliveThe 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.

Warning

Using Readiness=Alive on a service that other services Requires is a common configuration smell. peinit will let it boot, but it logs a validation warning: Alive readiness gives no guarantee the service is actually serving before its dependents start. If a dependent connects on boot and gets connection-refused, this is usually why. Prefer Notify wherever the service can signal it.

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:

RemainAfterExitAfter a successful exit
0 (default)The service passes through Completed — releasing its dependents — then transitions to Inactive.
1The 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:

  • StartTimeout covers 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 raise StartTimeout accordingly (or send EXTEND_TIMEOUT_USEC as it works — see Keeping services running).
  • ExecStartPost runs only on success. Post-hooks fire after the successful exit. If the Oneshot fails, ExecStartPost does 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.

TriggerFormMeaning
BootbootStart during Phase 2, in dependency order.
Settled bootboot:settledStart after the boot set settles, with a bounded wait; useful for console prompts.
Terminal handovertty:releasedStart when the terminal named in TTYPath is released.
Timertimer:<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 startDisabled=1
Stop a service from being started at all, even manuallyDeny SERVICE_START in its ServiceSecurity descriptor
Stop a running serviceThe 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.

FieldFormIf omitted
DayOfWeekweekday name(s)any day (*)
DateYear-Month-Day*-*-* (any date)
TimeHour:Minute:Second00:00:00
Secondthe :Second of Time:00
TimezoneIANA zone namesystem-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:

OperatorExampleMatches
Wildcard*any value
List1,15any listed value
RangeMon..Fri, 8..17the inclusive range
Repetition0/150, then every multiple of the step — 0, 15, 30, 45
Range + step8..17/28, 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:

ShortcutEquivalent
minutely*-*-* *:*:00
hourly*-*-* *:00:00
daily*-*-* 00:00:00
weeklyMon *-*-* 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 #

ExpressionMeaning
*-*-* 02:00:00Every day at 2 am (system-local)
Mon *-*-* 00:00:00Every Monday at midnight
*-*-1,15 12:00:001st and 15th of each month at noon
*-*~01 00:00:00Last day of each month at midnight
Mon..Fri *-*-* 09:00:00Weekdays at 9 am
*-*-* *:00/15:00Every 15 minutes
*-*-* 02:00:00 Europe/LondonEvery 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.

TypeCurrent stateAction
OneshotInactive / Completed / FailedStart it (operation source Timer).
OneshotActive / StartingSet a single pending flag — one catch-up run when the current one finishes.
SimpleInactive / FailedStart it.
SimpleActive / StartingNo-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.

Note

The timestamp is written when the timer fires and the start is initiated, not when the run completes. A service that crashes mid-run is not re-triggered on the next boot — the run was attempted, not missed.

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.

EventBehaviour
NTP step or manual clock setArmed 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 / resumeA scheduled time that elapsed while suspended fires once on resume — once, not once per missed occurrence.
A missed occurrence within one uptimeFire 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.

Caution

If a machine boots with a wildly wrong clock (a dead RTC battery) and NTP corrects it only later, the boot-time persistent catch-up decision is made against the wrong time and may fire spuriously or not at all. The runtime half self-heals once NTP steps the clock, but the boot-time decision is already made by then. This is a known edge case short of NTP-aware rescheduling — worth knowing if you run timers on hardware with unreliable clocks.

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.

Note

Reach for this only to keep console output legible. If a service genuinely needs something to be running first, that is a dependency — say so with Requires. A console login uses both: boot:settled for when, and Requires on its authority for what must be true.

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:

ServiceTTYPathTTYPrecedenceTriggers
oobe/dev/console100boot: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 TTYPrecedence wins, 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:released or TTYPrecedence without one is rejected when the definition is read, rather than quietly doing nothing.
  • tty:released on its own starts nothing until somebody takes the terminal and lets it go. Give a service that must come up either way a boot or boot:settled trigger as well, as login-console has above — otherwise a machine with no oobe installed 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.

Note

This is deliberately not Conflicts, which fails both services — and the services that want a terminal are the interactive ones, so that answer is a machine you cannot log into. It is not a dependency either: a login prompt does not need first-boot setup to have run, it needs the console to be free.

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.

Warning

On an ErrorControl=Critical service, exhausting the restart budget causes an immediate sync and reboot. A bad health check can therefore reboot the machine repeatedly. Confirm console recovery access before changing supervision of a Critical service.

Restart policy #

When a service fails in a way that might be transient, peinit consults RestartPolicy:

PolicyValueBehaviour
Never0Never restart. The service goes straight to Failed.
OnFailure (default)1Restart on a crash or runtime failure. An exit code in SuccessExitCodes is not a failure and is not restarted.
Always2Restart 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"]

Tip

If a service flaps — restarting endlessly without ever sticking — and never hits its budget, check whether it stays Active for the full RestartWindow between failures. Merely reaching Active does not reset the counter. Check the health-command output too; the health-check timing constraint prevents one common form of restart loop.

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 Never or OnFailure, the cause is CleanExit and the service goes straight to Inactive, with no restart-policy consultation at all.
  • Under Always, the cause is CleanExitRestart and 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.

FieldDefaultControls
HealthCheck—The command to run (runs under the service's own token).
HealthCheckInterval30Seconds between runs.
HealthCheckTimeout5Seconds before a check is killed and counted as failed.
HealthCheckRetries3Consecutive 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.

Caution

Be extremely cautious with health checks on ErrorControl=Critical services. A false-positive health-check failure on a Critical service will, after exhausting the restart budget, reboot the machine — and a flaky check can do this repeatedly. Critical platform services are better served by a passive watchdog with a generous timeout: the service itself knows whether it is healthy and simply stops sending keepalives if not. peinit applies the same health-check semantics to Critical services as to any other — there is no special-casing to save you.

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:

PhaseBase timeoutMaximum deadline
StartingStartTimeoutStartTimeout × 4
StoppingStopTimeoutStopTimeout × 4
ReloadingStartTimeoutStartTimeout × 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:

LevelValueOn irrecoverable failure
Normal (default)0The service stays in Failed. The rest of the system carries on.
Critical1peinit 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 the OnFailure handler.
  • ErrorControl=Critical implies SafeMode. A Critical service is always eligible to start in Safe mode.
  • Critical services are OOM-protected. peinit sets their oom_score_adj to -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.

Warning

A Critical service that exhausts its restart budget on every boot can cause a reboot loop. The boot-attempt counter can escalate repeated unconfirmed boots to Recovery, provided peinit reaches and persists its increment. A confirmed boot resets the counter, so repeated failures after confirmation may never reach the Recovery threshold. See Troubleshooting peinit.

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 #

  1. Read the dependent’s definition with svctl definition show <service> and inspect each target with svctl status <target>.
  2. Decide whether the requirement is successful startup (Requires), optional startup ordering (Wants), runtime coupling (BindsTo), or mutual exclusion (Conflicts).
  3. Use a readiness level when the target must have reached a specific condition, such as a routed network.
  4. 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 with DependencyFailure and 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 (carrying BindsToPropagation), not Inactive — so a status query 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 you reset (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 (cause ConflictEviction) before A starts — and vice versa. If the loser will not stop within its StopTimeout, SIGKILL escalation applies. The evicted loser does not land in Inactive: it comes to rest in Failed (carrying ConflictEviction), so it shows as Failed in status and needs a reset before 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 orderTarget failure stops dependent?Target stop stops dependent?
Requirestarget first; dependent fails if target failsAt start time onlyNo
Wantstarget first if present; dependent starts regardlessNoNo
BindsTotarget first; dependent fails if target failsYes (and auto-recovers)Yes (and auto-recovers)
Conflictsstarting one evicts the othern/an/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:

  1. Collect all transitive Requires and BindsTo dependencies (and best-effort Wants).
  2. Validate that sub-graph (cycles, missing targets).
  3. Resolve Conflicts — stop anything that conflicts.
  4. 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:

  1. Every service that Requires the failed one transitions to Failed with DependencyFailure.
  2. Every service that merely Wants it is unaffected and starts normally.
  3. 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:

RelationshipMissing or disabled target
RequiresDependent marked Failed (DependencyFailure).
BindsToTreated as a missing Requires — Failed (DependencyFailure).
WantsSilently ignored — the entry is dropped.
ConflictsSilently 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 keyDefaultMeaning
Machine\System\Boot\MaxParallelStarts10Maximum 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.

Note

Boot order is emergent, not hardcoded. The installed definitions and their identity dependencies determine the sequence. Change the dependencies and you get a different order. The platform daemons come up first because everything else, directly or transitively, requires them.

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 RequiredPrivileges allow-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 RequiredPrivileges is 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:

ContextRuns as
Main processThe service's Identity.
ExecStartPre / ExecStartPostHookIdentity if set, otherwise the service's Identity.
Health checksThe service's Identity, always.
ExecReload (command form)The service's Identity, always — HookIdentity does not apply.
Submitted jobsThe 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.

Warning

peinit does not validate filesystem permissions on hook binaries. If HookIdentity grants elevated privileges, it is the administrator's responsibility to ensure the hook binary and every parent directory are not writable by a lower-privileged identity — otherwise a less-trusted principal could replace the hook and run code as the elevated identity. (FACS enforces KACS descriptors on managed filesystems, but peinit itself performs no such check — protecting hook binaries still depends on correct SDs from packaging and controlled paths.)

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:

  1. peinit never shares its SYSTEM token. Even Identity=SYSTEM services get a separately minted token.
  2. 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.
  3. Privileges are subtractive only. RequiredPrivileges removes; it can never add.
  4. Identity is deterministic. Every service runs as a known principal. SYSTEM must be declared explicitly; an empty Identity is the minimal LocalService, never SYSTEM.
  5. 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.

  1. Compiled-in base. A fixed floor peinit always provides — currently just PATH: /bin.
  2. Global EnvVars. Each value under Machine\System\Init\EnvVars\ becomes a variable (value name → variable name). EnvVars\PATH overrides the base PATH; other names add. Every service gets this layer except registryd, which serves the key itself — see the warning below for why that exemption exists.
  3. Per-service Environment. The definition's own KEY=VALUE entries, which override layers 1–2.
  4. Protocol variables. NOTIFY_SOCKET (always) and LISTEN_FDS / LISTEN_FDNAMES (only when stored fds are injected). These have the highest precedence and cannot be overridden — a service that clobbered NOTIFY_SOCKET would 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.

Warning

Write access to Machine\System\Init\EnvVars\ is equivalent to compromising every service peinit starts — a principal who can set it can inject LD_PRELOAD, LD_LIBRARY_PATH, and the like into all of them. peinit deliberately does not filter variable names; the key's security descriptor is the control boundary. Keep it tight — the recommended default is SYSTEM full, Administrators read-only.

This is also why registryd is the one service that never receives this layer: it is the daemon that serves the key. Letting the key inject into the process that enforces who may write it would make the control boundary self-referential. Every other service, platform daemons included, gets the ordinary layering.

Working directory and limits #

FieldEffect
WorkingDirectoryThe process's working directory. Must be a non-empty absolute path; defaults to /.
LimitNOFILERLIMIT_NOFILE — the maximum number of open file descriptors.
LimitCORERLIMIT_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.

Warning

Attaching a terminal suppresses log capture — the pipes eventd would read are closed, so the service's output goes to the terminal and nowhere else. Don't set TTYPath on a service whose output you expect to find in the logs.

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.

  • ExecStartPre runs before the main binary. Each command runs in sequence, in the hooks/ sub-cgroup; any hook exiting non-zero aborts the start (the whole cgroup is killed and the service Fails with PreHookFailure). Use it for prerequisites that must hold before the service starts — creating a runtime directory, waiting on a precondition.
  • ExecStartPost runs 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:

TypeFormPasses when
pathpath:<path>the path exists (any type)
filefile:<path>a regular file exists
directorydirectory:<path>a directory exists
registryregistry:<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 exactly Machine\System\Services, Machine\System\Init, and Machine\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 a stat() on the event loop, because stat() 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=1 with the fd attached (and optionally FDNAME=<name>; the default name is stored). peinit holds it.
  • It removes one with FDSTOREREMOVE=1 and FDNAME=<name>.
  • On the next start, peinit injects the stored fds starting at fd 3, sets LISTEN_FDS to the count and LISTEN_FDNAMES to the colon-separated names, then clears the store. (This is the same LISTEN_FDS convention 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:

DescriptorQuestionStoredEnforced by
Registry key SDWho can read or edit the definition?On the Machine\System\Services\<name> keyLCS, at key-open time
ServiceSecurity SDWho can manage the running service?As the ServiceSecurity binary value on that keypeinit, 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 own ServiceSecurity value. 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 ServiceSecurity on Machine\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\Services for 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, and WRITE_OWNER for 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:

RightBitGrants
SERVICE_QUERY_STATUS0x0001Query state, PID, cause, health, warnings.
SERVICE_START0x0002Start the service.
SERVICE_STOP0x0004Stop the service.
SERVICE_INTERROGATE0x0008Reload the service.
SERVICE_ALL_ACCESS0x000FAll 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:

GenericMaps to
GENERIC_READSERVICE_QUERY_STATUS
GENERIC_WRITESERVICE_START | SERVICE_STOP | SERVICE_INTERROGATE
GENERIC_EXECUTESERVICE_START | SERVICE_STOP | SERVICE_INTERROGATE
GENERIC_ALLSERVICE_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.

Warning

A ServiceSecurity value carries the service's audit policy as well as its permissions, and it is written whole. Anyone who may write it — that is, anyone who may write the service's registry key, or Machine\System\Services — can remove its SACL and so stop its denials being recorded. Keep those keys writable only by administrators, as they are by default. To let someone manage a service, grant them rights in its ServiceSecurity; do not give them write access to its key.

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:

  1. peinit captures the caller's token from the kernel (the KACS_SO_PEER_TOKEN socket option) — the caller's effective identity at connection time, so if the caller is impersonating, the impersonated identity is what is checked.
  2. peinit resolves the target service and its ServiceSecurity descriptor.
  3. peinit runs AccessCheck: the caller's token against the descriptor, for the right the command needs.
  4. Denied → return ACCESS_DENIED. The kernel records the attempt — caller, target service, rights requested and granted — as a kacs.audit.access.checked event, because the descriptor's SACL asks it to.
  5. Granted → execute the command.

Note

Every denial is recorded by default: the built-in descriptor's SACL audits every refusal, for everyone, and peinit names the service in each check so the record says which one it was. Look for it in the event viewer as kacs.audit.access.checked with object kind service and the service's name. A ServiceSecurity you write yourself is used as it is, so keep a SACL on it (for example S:(AU;FA;0xf;;;WD)) if you still want its denials recorded. If you are debugging a denial, the audit record has everything you need; see Debugging a denial.

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:

RightBitGrants
SYSTEM_SHUTDOWN0x0001Initiate poweroff, reboot, or halt.
SYSTEM_RELOAD_CONFIG0x0002Re-read all definitions and rebuild the graph.
SYSTEM_QUERY_STATUS0x0004Ask how this boot went (svctl boot).
GenericMaps to
GENERIC_READSYSTEM_QUERY_STATUS
GENERIC_WRITESYSTEM_RELOAD_CONFIG
GENERIC_EXECUTESYSTEM_SHUTDOWN
GENERIC_ALLall 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. status is per-service access-controlled; list filters. The same holds for jobs: job status is checked, job list filters.

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 boot is not available there.

Important

Recovery needs physical, IPMI, or serial console access; there is no remote recovery service. Confirm that access before a change that may leave the machine unable to boot. Recovery offers no TCB guarantee.

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."

Note

SafeMode=1 and ErrorControl=Critical are filters within the boot-triggered set. They do not make a demand-only service auto-start in Safe mode — a service with no boot trigger stays demand-only regardless.

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.

Important

Recovery mode requires console access — physical, IPMI, or serial. There is no remote recovery in the current design (emergency SSH and registry historical reversion are noted as post-v1 work). Plan console access for any machine you need to be able to recover.

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 with peios.bootattempts=N on the kernel command line, or set it to 0 to 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=1 forces 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 #

KeyDefaultControls
Machine\System\Boot\MaxParallelStarts10Services starting concurrently (must be > 0; invalid → Recovery).
Machine\System\Boot\BootSuccessGrace30Seconds a Critical service must hold a satisfying state before boot counts as successful.
Machine\System\Boot\ShutdownTimeout90Maximum seconds for the whole shutdown sequence.
Machine\System\Boot\PostKillTimeout5Seconds a service cgroup may take to drain after SIGKILL before it counts as stuck.
Machine\System\Boot\SettleTimeout5Seconds 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.

TokenEffect
peios.safemode=1Force Safe mode.
peios.recovery=1Force Recovery mode, whatever the counter says.
peios.bootattempts=NOverride the boot-attempt threshold; 0 disables the check.
peios.notifysocket=PATHMove 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=NHow 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 ownsEverywhere else
peios.quiet=0peinit writes anywayeverything
peios.quiet=1 (default)only messages that mean the machine is about to be losteverything
peios.quiet=2only messages that mean the machine is about to be losterrors 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.

Warning

Suppressed messages are dropped, not stored. Until peinit can forward them to eventd, peios.quiet=1 means you lose peinit's narrative from the moment a login prompt appears. On a machine you are debugging, boot with peios.quiet=0.

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.

Note

peios.quiet governs what peinit writes, and nothing else. The kernel's own console output is governed separately by the standard loglevel parameter, which shipped images set to 4 — errors and worse — so that peinit's narrative is not buried under the kernel's. The two are independent: setting peios.quiet=0 to debug a boot does not bring the kernel's messages back, and loglevel=7 ignore_loglevel does not make peinit any louder. Note also that raising loglevel alone will not undo a shipped ignore_loglevel, and that the image ships no dmesg, so at the default a kernel warning is not recoverable after the fact.

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.

Note

There is no token that selects services. Which services start is decided entirely by what is defined under Machine\System\Services — see Defining a service. A console shell or a login prompt is an ordinary service definition with a TTYPath, not a boot flag.

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, /dev are 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:

  1. 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.
  2. Mount the remaining virtual filesystems — /dev/pts, /dev/shm, /run, /sys/fs/cgroup — mounting each only if absent. A failure here → Recovery.
  3. 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.
  4. 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.
  5. Set the clock from the hardware RTC, so early timestamps and the boot counter are meaningful. A failure here → Recovery.
  6. 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.
  7. 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 marked Required=1 that cannot be provisioned sends boot → Recovery. The individual keys are cataloged in the registry key reference.
  8. 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.

Caution

Packaged images, VM templates, and live ISOs must not ship a populated /lcl/etc/machine-id or a /var/state/peinit/random-seed file. A shipped machine-id gives every clone the same identity, and a shipped seed is a public value that is not acceptable entropy. Clone and reset tooling should remove or truncate /lcl/etc/machine-id so peinit generates a fresh ID on the next boot, and should never bake a seed into the image — if you need strong first-boot randomness for a stateless image, provide a real kernel entropy source (hardware RNG or virtio-rng) instead.

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:

  1. Read all definitions under Machine\System\Services\. (A registry read timing out here → Recovery.)
  2. Build and validate the dependency graph from the boot-triggered services and their transitive dependency closure. Validation runs before anything starts.
  3. 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):

TypeResult
poweroffStop everything, unmount, power off.
rebootStop everything, unmount, reboot.
haltStop everything, unmount, halt (CPU stopped, power stays on).

Signals. As PID 1, peinit treats a handful of signals as shutdown requests:

SignalMeaning
SIGINTReboot — the kernel sends this on Ctrl-Alt-Del.
SIGTERMPoweroff.
SIGPWRPoweroff — 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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 cause shutdown. 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.
  5. Stop services in reverse dependency order, in waves. Each graceful-stop-eligible service (those in Active or Reloading) gets SIGTERM and StopTimeout seconds to exit; if it does not, peinit SIGKILLs its whole cgroup. A service is never stopped until everything that Requires or BindsTo it has already stopped. Services that are Starting are not stopped gracefully — their startup is cancelled and the cgroup SIGKILLed.
  6. 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.
  7. 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.
  8. 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 final sync() and the kernel, never blocking shutdown. The root filesystem (/) is never unmounted — once the rest are handled, peinit remounts root read-only.
  9. 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 #

Warning

Forced shutdown kills processes without waiting for graceful completion. Unfinished application work can be lost. Use it only when the normal shutdown cannot finish.

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.

SignalBehaviour
SIGCHLDReap children; match exits to services and submitted jobs. Also reaps orphaned processes the system reparented to PID 1.
SIGINTReboot request. Repeated within the window → forced reboot.
SIGTERMPoweroff request.
SIGPWRPoweroff request — compatibility path for power-button/power-failure policy.
SIGHUPIgnored — PID 1 has no controlling terminal.
SIGPIPEIgnored — a broken control-socket pipe must never crash PID 1.

All other signals are ignored.

Note

Reaping orphans matters: when any process anywhere on the system dies leaving children, those children are reparented to PID 1. peinit reaps them so they do not accumulate as zombies — even though they were never its services. This is part of the job of being PID 1, distinct from supervising services.

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.

Note

The registry records a value’s type, not its application meaning. Use regman and the field’s documentation before changing a default. peinit can reject a value when it reads and validates it, even if a raw registry write succeeded.

Registry value types #

Service definition fields map onto registry value types as follows:

Schema typeRegistry typeIs
stringREG_SZA UTF-8 string.
multi_stringREG_MULTI_SZAn ordered list of strings.
dwordREG_DWORDA 32-bit unsigned integer.
binaryREG_BINARYA raw byte sequence.
(timestamp)REG_QWORDA 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

FieldTypeDefaultMeaning
ImagePathstring(required)Absolute path to the service binary.
Argumentsmulti_string—Command-line arguments.
WorkingDirectorystring/Working directory (non-empty absolute path).
TTYPathstring—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.
TTYPrecedencedword0Who 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.
Environmentmulti_string—KEY=VALUE pairs added to the environment.
RuntimeDirectoriesmulti_string—Directories created directly under /run just before the main process starts, each secured for SYSTEM, Administrators, and the service's own SID.
LimitNOFILEdword—RLIMIT_NOFILE.
LimitCOREdword—RLIMIT_CORE, in bytes.

Note

Each RuntimeDirectories entry is a plain relative name — just foo, never a path — and must not contain /, \, ., .., NUL, or control characters. For an entry foo, peinit creates /run/foo immediately before launching the main process and applies a Peios file security descriptor granting full access to SYSTEM, Administrators, and this service's own SID. If the directory can't be created or the 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 that the next boot clears for you.

Type and readiness — see Simple and Oneshot services

FieldTypeDefaultMeaning
Typedword0 (Simple)0 = Simple, 1 = Oneshot.
Readinessdword0 (Notify)0 = Notify (READY=1), 1 = Alive. Ignored for Oneshot.
RemainAfterExitdword0Oneshot only — stay Completed after a successful exit.
SuccessExitCodesmulti_string—Non-zero exit codes treated as success (each 0–255).

Activation — see Triggers and timers

FieldTypeDefaultMeaning
Triggersmulti_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.
Disableddword0If 1, triggers must not activate the service.
SafeModedword0Eligible within the boot-triggered set in Safe mode. Critical implies SafeMode; neither adds a boot trigger.
Conditionsmulti_string—Start-time checks; a failure skips the service.
Assertsmulti_string—Start-time checks; a failure fails the service.
PreStartCheckTimeoutdword5Seconds 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).
TimerPersistentdword1Catch up a missed timer run after a reboot.
TimerJitterdword0Maximum random delay (seconds) added to each firing.

Identity — see Service identity and privileges

FieldTypeDefaultMeaning
IdentitystringLocalServicePrincipal name, SID, or Service for the service’s distinct virtual account.
RequiredPrivilegesmulti_string—Privilege allow-list; all others are removed from the token.
HookIdentitystring(service's Identity)Identity for ExecStartPre/ExecStartPost.

Dependencies — see Dependencies and ordering

FieldTypeDefaultMeaning
Requiresmulti_string—Hard dependencies. Each entry may carry a readiness level (netd:routed).
Wantsmulti_string—Soft dependencies.
BindsTomulti_string—Runtime coupling.
Conflictsmulti_string—Mutual exclusion. A level parses here but is inert.
Providesmulti_string—Roles this service fills, so others can depend on the role rather than the implementation. Bare names only — a level is rejected.
OnFailurestring—Service to start when this one enters Failed.

Supervision and health — see Keeping services running

FieldTypeDefaultMeaning
ErrorControldword0 (Normal)0 = Normal, 1 = Critical (sync + reboot on irrecoverable failure).
RestartPolicydword1 (OnFailure)0 = Never, 1 = OnFailure, 2 = Always.
RestartMaxRetriesdword5Consecutive restarts before Failed.
RestartWindowdword120Seconds of Active health that resets the restart counter.
RestartDelaydword1Backoff base (doubles per failure, capped at 60).
HealthCheckstring—Periodic health-check command.
HealthCheckIntervaldword30Seconds between health checks.
HealthCheckTimeoutdword5Seconds before a check is killed and failed.
HealthCheckRetriesdword3Consecutive failures before unhealthy.
WatchdogTimeoutdword0Seconds between expected WATCHDOG=1 pings; 0 disables.

Transition phases — see The service lifecycle and Controlling services

FieldTypeDefaultMeaning
ExecStartPremulti_string—Commands run before the binary (sequential; any failure aborts start).
ExecStartPostmulti_string—Commands run after readiness / successful exit (failure logged, not fatal).
ExecReloadstring— (SIGHUP)Reload command, or signal:<NAME>.
StartTimeoutdword30Seconds for the whole start sequence.
StopTimeoutdword10Seconds between SIGTERM and SIGKILL.

Notify, fds, security, metadata

FieldTypeDefaultMeaning
NotifyAccessdword0 (Main)Who may send sd_notify (only Main is supported).
FdStoreMaxdword0Max fds in the fd store; 0 disables it.
ServiceSecuritybinaryinherit parentDescriptor controlling who may manage the service.
DisplayNamestring—Human-readable name for status display.
Descriptionstring—Description of the service.

Service definition keys #

KeyTypePurposeSee
Machine\System\Services\(parent key)Parent of all service definitions; each child key is a service.Defining a service
Machine\System\Services\SchemaVersiondwordSchema version guard (currently 1).Defining a service
Machine\System\Services\<name>\LastTimerRunREG_QWORDLast-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\:

KeyTypeDefaultPurposeSee
MaxParallelStartsdword (> 0)10Maximum services starting concurrently. Missing → default; zero/invalid → Recovery.Boot and boot modes
BootSuccessGracedword30Seconds a Critical service must hold a satisfying state before boot is successful.Boot and boot modes
ShutdownTimeoutdword90Maximum seconds for the whole graceful shutdown.Shutdown
SettleTimeoutdword5Seconds to wait for the boot set to settle before starting boot:settled services anyway.Triggers and timers
PostKillTimeoutdword5Seconds 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\:

KeyTypeDefaultPurposeSee
ControlSecuritybinarySYSTEM and Administrators all; Authenticated Users queryDescriptor for system-level control operations.Who can manage a service
MaxControlConnectionsPerUserdword16Maximum concurrent control-socket connections one user may hold; SYSTEM is exempt.Controlling services
MaxControlConnectionsdword256Maximum concurrent control-socket connections in all.Controlling services
MaxRequestSizedword65536Maximum control-socket request size (bytes).Controlling services
ConnectionTimeoutdword30Seconds before an idle control connection is closed.Controlling services
MaxJobsConnectionsdword64Maximum concurrent jobs-socket connections.Controlling services
MaxJobMessageSizedword32768Maximum jobs-socket message size (bytes); bounds a submission's whole definition.Controlling services
JobsConnectionTimeoutdword30Seconds before an idle jobs connection is closed.Controlling services
MaxJobsPerSubmitterdword64Live submitted jobs one submitting SID may hold; SYSTEM is exempt.Jobs and operations
MaxLogLineLengthdword8192Maximum bytes per service output line before truncation.Service output and logging
MaxLogBufferPerServicedword65536Maximum bytes buffered per service pipe before back-pressure.Service output and logging
LogReadBytesPerEventdword16384Bytes 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
PreEventdBufferdword1048576Total 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)emptyDefault 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)emptyBoot-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>\Kindstring(required)directory or file — what to create at Path.Boot and boot modes
ProvisionedPaths\<name>\Pathstring(required)Absolute path to create or verify.Boot and boot modes
ProvisionedPaths\<name>\SecuritybinarySYSTEM + Administrators full, users read/traversePeios file security descriptor to apply to the object.Boot and boot modes
ProvisionedPaths\<name>\Requireddword0If 1, a failure to provision this entry sends boot to Recovery mode before Phase 2 starts.Boot and boot modes

Note

A few ProvisionedPaths rules worth knowing before you rely on them: Kind=file never truncates an existing file (it only makes sure the file is present), and peinit does not create missing parent directories for you — define each directory you need as its own entry, or depend on the package that owns the parent. A malformed entry is logged and skipped rather than blocking boot; only an entry you mark Required=1 can send boot to Recovery mode, and only when it genuinely can't be created or secured.

Keys peinit reads from other subsystems #

LogSocketPath is read by peinit. MaxLogDatagramBytes is included for related eventd configuration; it is not a peinit input.

KeyTypePurposeSee
Machine\System\eventd\LogSocketPathstringPath of eventd's log datagram socket, where peinit forwards service output.Service output and logging
Machine\System\eventd\MaxLogDatagramBytesdwordeventd’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 status
resolv status

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 seeNext step
No carrier, or readiness absentCheck the cable or link and the interface's verdict in net status.
IGNORE, DOWN, or no profileInspect 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 routeCheck Route.Offered or the pinned Route.Gateway, then read back the result.
An address works but a name does notUse name-resolution checks.
A particular service is unreachableCheck its listener, port reservation and firewall decision.
policy REFUSEDThe 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.

Warning

A live change can interrupt the connection you are using. Profile edits restart the affected interfaces' address clients. Do not use a daemon restart as a harmless reload: the netd manual documents leased addresses and routes being removed until DHCP answers, and advertised IPv6 settings waiting for a new advertisement. Keep a local recovery path and follow the change checklist.

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 under Profiles\.
  • IGNORE leaves the interface as it is, including existing settings.
  • DOWN keeps 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:

NeedSetting and effect
Accept the network's addressAddress.Offered: DHCPv4 and IPv6 router discovery, limited by Address.Families. IPv6 uses stable-privacy addresses; DHCPv6 supplies DNS information, not addresses.
Set an address yourselfAddress.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 DHCPAddress.LinkLocal: a 169.254/16 fallback while DHCP discovery continues; removed when a lease arrives.
Add rotating IPv6 addressesAddress.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 #

LevelWhat it tells you
absentThe joined interface is down or has no carrier.
linkIt is up with carrier, but has no usable address.
addressedIt has a usable address, but no default route. A 169.254… address counts.
routedIt 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.conf nor /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 Status keys 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 default affects 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, untrusted and home profile examples assume those profiles already exist.

Warning

Direct reg writes do not use Network Manager's Keep this change? countdown. Do not assume an automatic rollback. If you use Network Manager, read which changes it can undo before applying one. A daemon restart can interrupt leased and autoconfigured connectivity; it is not needed to apply these settings.

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 #

net rules
net profiles
net status
resolv status
  1. Confirm the rule names the intended profile and matches the intended interface. net profiles lists each profile's own values, not a flattened result; account for inherited values.
  2. Check that net status has no policy REFUSED line. Read the active verdict, rule and profile, then the address, gateway, lease and warnings.
  3. Check the servers and domains in resolv status, and test a relevant name with resolv query <name>.
  4. Test the service you actually need. Readiness routed proves 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.

FactMatchesExample
Interfacethe kernel's interface nameenp0s3
Interface.Kindwired, wireless, loopback, otherwired
Interface.Idthe stable id from net status3f2a1b8e-…
Interface.Macthe hardware address52:54:00:12:34:56
Interface.Paththe bus pathpci-0000:00:03.0
Interface.Driverthe kernel drivervirtio_net
Network.Namethe Name you gave the network's recordpalfrey-home
Network.Trustthe Trust you gave itcorporate

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 status
net rules
net profiles

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 status for the intended verdict, rule and profile, no policy REFUSED line, and the expected addresses and gateway. net profiles shows each profile's own values, so inherited settings still matter.
  • DNS: check resolv status and a relevant resolv query <name>.
  • Firewall rule or network label: as an administrator, run net policy wait, then net policy. A returned registry write is not yet proof that the kernel accepted it.
  • Connectivity: test the required service. routed means 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 #

CommandDoes
net statusHostname, 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 reconcileRe-run the reconciler immediately rather than waiting for an event.
net policyThe 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 rulesThe interface layer, one rule per line: path, priority, conditions, actions.
net profilesThe 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 #

CodeMeaning
0Done; for wait, the level was reached; for policy wait, the changes are in force.
1netd refused or could not be reached; for wait, the timeout passed; for policy wait, the timeout passed or the kernel refused the generation.
2Usage 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 net command, 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 #

SymptomCheckWhat to do with the result
No connectivitynet statusFind 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 addressnet status, then net profiles and net rulesCheck Address.Offered, Address.Families and the selected rule. An unanswered-discovery warning can mean no server or blocked DHCP traffic.
Only a 169.254… addressnet statusThis is link-local fallback, not a routed lease. DHCP discovery continues. Check server reachability and the baseline DHCP rules.
Address present, no gatewaynet statusCheck Route.Offered or your pinned Route.Gateway. An unreachable pinned gateway is logged as a failed route addition.
IP connection works, names failresolv status, resolv query <name>Check servers, domains, netd connection, and notfound versus unavailable; use DNS troubleshooting.
One TCP service failsss -lnt on the receiving machine; nc -vz 192.0.2.1 443 from the callerConfirm 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 changenet status; as administrator, net policy wait and net policyCheck interface and firewall acceptance separately. A refused generation leaves the previous one active.
TLS certificate errorsStrict OpenSSL checkCheck both trust and the expected peer identity. SNI alone is not hostname verification.

For daemon errors, use the service logs:

evctl 'LOGS FROM netd SINCE 1h ago TAKE 40'
evctl 'LOGS FROM resolvd SINCE 1h ago TAKE 40'

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:

CommandPurposeExample
rsyncSynchronize file contents locally or over SSHrsync -rlt source/ destination/
opensslInspect TLS, certificates and cryptographic materialopenssl x509 -in certificate.pem -noout -text
curlTransfer data using URLscurl https://example.org/
wcurlDownload files using curlwcurl https://example.org/file.txt
pingCheck ICMP echo replies and round-trip timeping -c 3 192.0.2.1
tracepathInvestigate a route and its path MTUtracepath 192.0.2.1
digSend DNS queries to a chosen DNS serverdig @127.0.0.53 example.org A
ssInspect socketsss -lnt
ncSend or receive TCP and UDP streamsnc -vz 192.0.2.1 443
whoisQuery domain and IP address registration recordswhois example.org
iperf3Measure TCP or UDP throughput between cooperating hostsiperf3 -c 192.0.2.1
socatRelay data between sockets, files, terminals and programssocat - TCP:192.0.2.1:443
tcpdumpCapture packets or decode saved captures using libpcaptcpdump -nr capture.pcap
mtrRepeatedly probe a route and report latency and lossmtr -r -c 10 192.0.2.1
tracerouteProbe each hop towards a destinationtraceroute -n 192.0.2.1
arpingSend ARP probes on a local IPv4 linkarping -I eth0 -c 3 192.0.2.1
nmapDiscover hosts and inspect listening servicesnmap -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.

TaskStart hereCheck afterwards
Copy directory contents locally or over SSHSynchronizing contentsCheck the destination contents and native access policy. rsync is not a security-descriptor backup.
Create private key or other secret outputPrivate outputsUse the tool's named-output option and a private directory; shell redirection has different protection.
Diagnose a TLS serverTLS diagnosticsRequire 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:

rsync -rlt source/ destination/
rsync -rlt source/ alice@server:destination/

-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:

rsync -rlt --inplace source/ destination/

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:

openssl s_client -connect example.org:443 -servername example.org \
  -verify_return_error -verify_hostname example.org

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 #

resolv status
resolv query example.org
resolv lookup example.org

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:

resolv query example.org --no-cache

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:

  • FallbackServers is used only when no up interface supplies a server. It is not failover for an interface whose servers time out.
  • ExtraSearchDomains adds 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 status and 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:

  1. An exclusive scope, if it has a server and is at least addressed.
  2. The scope with the most-specific matching search domain.
  3. For a reverse lookup, a scope whose subnet contains the address.
  4. A scope that claims the default route, otherwise the lowest-metric eligible scope.
  5. Fallback servers only when no up scope has a server.

Warning

Dns.Exclusive = 1 alone is not a guarantee against DNS going through another network. An exclusive interface with no server, or one only at link, does not qualify. Verify its address and server lines before relying on it for VPN name privacy.

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:

reg new Machine/System/Network/Dns/Hosts
reg set Machine/System/Network/Dns/Hosts printer 10.0.2.9

Then verify both directions:

resolv query printer
resolv reverse 10.0.2.9

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 #

CommandWhat to check
resolv statusScopes, 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 flushClear 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 symptomMeaning and next check
notfound with source dns or cacheThe 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 nameCheck search-domain lines in resolv status. A scope without servers contributes no search domains.
unavailableNo 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 scopesresolvd 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 listedThese 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 answerCompare a --no-cache query. A static name can override DNS; administrators can flush the cache if needed.
resolv: access deniedCheck the native control object's query right. It is separate from reachability of the DNS stub.
Daemon unreachable or repeated failuresRead 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 #

  1. 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.
  2. Check what is active. As an administrator, run net policy to see enforcement, generation and refusals. Use net status separately for interface rules. A refused update leaves the previous accepted policy active.
  3. 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.
  4. 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.
  5. Apply, then verify. After a registry edit, run net policy wait, then net policy, and test the intended connection. Check net status too if interface rules, profiles or network labels changed.

Warning

Policy writes apply live, without rebooting. Existing connections are re-judged; they are not exempt. Direct registry writes and the PNP viewer do not provide Network Manager's documented 30-second keep/undo flow. Read Making a change before relying on that recovery mechanism.

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 as DstPort.Equal = 22. Every condition must hold. A rule with no conditions matches everything.
  • Actions: a list such as PASS, DROP, REJECT(Prohibited) or COUNT(dns).
  • Priority: a number, inherited by children and defaulting to 0 at a root. Enabled = 0 disables 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:

LayerUse it for
Rules\FlowWho 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\PacketConditions that must be checked per packet. The baseline passes tracked traffic here and leaves connection decisions to Flow. Untracked traffic is decided here.
Rules\RawPacketWire-side frame rules before connection tracking on ingress. Usually unnecessary for host access policy.
Rules\InterfaceWhich 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; REJECT on an established TCP connection tears down both ends.
  • A consulted Time.* condition changes. Time.Hour.Equal = 9-17 can stop an existing connection at 18:00 UTC, on its next packet. Use Start.Hour if 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 #

ActionWhat the operator or peer sees
PASSThis layer allows the traffic; other layers can still refuse it.
DROPSilent 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.
NULLNo 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 is IGNORE. 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. Present is 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, DROP beats REJECT beats PASS, and Refused beats Prohibited. An interface's verdict order is DOWN, 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 wait before 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.

Warning

Keep the protocol and port conditions on the gated verdict. Without them, the rule can refuse every packet from a source whose DNS count crossed the threshold, including traffic for your remote session.

Where things are visible #

  • net status: interface verdict, selected rule/profile and refusal.
  • net policy and net 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.reported events requested by REPORT effects.

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 findGo to
Addresses, gateway, DNS, hostname or MTUProfiles
Where a setting belongsRegistry layout
A match value or comparisonConditions
Allow, drop, reject, join or observeActions
Which layer can see a factLayers
Default image rulesDevelopment baseline
A policy pattern to adaptWorked 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.

BundleValueTypeMeaningDefault
AddressOffered0/1take the address the network offers (DHCPv4, IPv6 autoconfiguration)0
Familieslistipv4, ipv6: which families the bundle deals in at all; filters Static tooboth
StaticlistCIDR addresses, either familynone
LinkLocal0/1self-assign 169.254/16 while nobody answers; dropped when a lease arrives0
Temporary0/1add daily-rotating IPv6 privacy addresses beside the stable one0
OnExpiryDrop / Keepan offered address the network stops renewingDrop
RouteOffered0/1take the way out, and extra routes, the network offers0
Gatewaylistpinned way out, either familynone
Metricnumberrank of this interface's way out100 wired, 600 wireless
DnsOffered0/1take the servers and search domains the network offers, after our own0
Serverslistown servers, in ordernone
Domainslistdomains these servers answer fornone
Default0/1/absenttake names no domain claims; absent follows the default routeabsent
Exclusive0/1take all queries while at least addressed and supplying a DNS server0
HostnameOffered0/1adopt the network's name for us if Hostname is unset0
Announce0/1tell the network our name0
MtuOffered0/1take the packet size limit the network offers0
Valuenumber, 68+pin itleave 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.

Warning

Do not rely on a restart being invisible. The netd startup chapter documents leased addresses and routes disappearing until DHCP answers, and IPv6 settings needing fresh advertisements. Profile writes apply live and restart the affected clients; use a recovery path before changing the profile carrying your remote session.

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 #

ValueTypeMeaning
ActionsREG_MULTI_SZThe action list, one expression per element. Absent or empty = NULL (abstain).
PriorityREG_DWORD / REG_QWORDCollation weight. Inherited by subkeys; a root defaults to 0.
EnabledREG_DWORD 0 or 10 disables the rule and its whole subtree. Default 1.
<Fact>.<Operator>see belowA condition. Any other value name is an error that refuses the generation.

The Rules key's own value #

ValueTypeMeaning
CurrentReportingLevelREG_DWORD 1..6REPORT(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 #

OperatorFamiliesValue
EqualallMembership: 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.
GreaterThanintegerA single integer; strictly greater.
LessThanintegerA single integer; strictly less.
HasflagsA flag name or list; all listed bits set.
HasntflagsA flag name or list; all listed bits clear.
Presentall, 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 #

FactFamilyValuesPresent when
Directionstringin, outalways. Flow: the originator's side, for the flow's whole life
Interfacestringinterface name, e.g. eth0always. Flow, outbound: the route's device
EtherTypeintegernumber, or ipv4, ipv6, arpalways. Not a Flow fact (a flow's family is its addresses')
SrcMacMACaa:bb:cc:dd:ee:ffframe has an Ethernet header; at the IP seats outbound, the machine's own device address
DstMacMACaa:bb:cc:dd:ee:ffframe has an Ethernet header. Not a Flow fact
VlanintegerVLAN idthe frame is VLAN-tagged (device seats) or the interface is a VLAN device (IP seats)
SrcAddr, DstAddraddressexact (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
Protocolintegernumber, or tcp, udp, icmp, icmpv6, sctpIP packet
TtlintegerTTL / hop limitIP packet. Not a Flow fact
Dscpinteger0..63IP packet. Not a Flow fact
Fragmentinteger0 or 1IP packet (only ever 1 before defragmentation, i.e. at the RawPacket seat). Not a Flow fact
SrcPort, DstPortinteger0..65535TCP, UDP or SCTP
TcpFlagsflagsFIN, SYN, RST, PSH, ACK, URG, ECE, CWRTCP. Not a Flow fact
IcmpType, IcmpCodeintegerICMP / ICMPv6 type and codeICMP or ICMPv6
Lengthintegerpacket length in bytes at the seat (stack view)always. Not a Flow fact
FlowStatestringnew, established, related, invalid, untrackedPacket layer at its proper seat; never at the RawPacket or Flow layers
Relatedinteger0 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.Secondintegerwall 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.Secondintegerthe wall clock when the flow began, UTCFlow layer only; fixed for the flow's life, never expires a sentence
Localstringprogram, kernel, shared, none: what stands at this machine's end of the flowFlow layer only; always present there
Local.UserSIDS-1-5-19, or a well-known name (SYSTEM, LocalService, NetworkService, Everyone, Administrators, Users, …)Flow, Local is program
Local.GroupSID setSIDs 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.Integrityintegera level, or untrusted, low, medium, high, systemFlow, Local is program
Local.ConfinementSIDthe confinement SIDFlow, the program is confined
Local.CapabilitySID setany of the confinement's capability SIDsFlow, the program is confined
Local.ServiceSIDa service name (resolvd), or its S-1-5-80-… SIDFlow, the program is a service
Local.Processstringthe process GUID, 8-4-4-4-12 hex (case-insensitive)Flow, Local is program
Remote, Remote.*as Localthe other end of the flow, when it is on this machine tooFlow, loopback flows only
Interface.Kindstringwired, wireless, loopback, tunnel, bridge, otherInterface layer only; always
Interface.Idstringthe stable interface id (the inventory key name)Interface layer only; always
Interface.MacMACaa:bb:cc:dd:ee:ffInterface layer only; the interface has a hardware address
Interface.Pathstringbus position, e.g. pci-0000:00:03.0Interface layer only; it is hardware
Interface.Driverstringkernel driverInterface layer only; it is hardware
Network.Idstringthe 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.Namestringthe operator's Name on the recordevery layer; the record has one
Network.Truststringthe operator's Trust on the recordevery layer; the record has one
Network.Kindstringthe Interface.Kind the network was seen onInterface layer; a network has been identified
Tag.<name>integerthe flow tag's valuethe flow carries the tag (Packet and Flow layers; never RawPacket)
Counter.<name>[(...)]integera counter view, see belowthe 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:

ArgumentFormMeaning
window<n>s, <n>m, <n>h, <n>d — at most 1dSliding window; omitted = the cumulative total since the cell was created.
keySrcAddr, 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).

ExpressionSpeciesMeaning
NULLabstentionNothing. A rule whose actions yield no verdict abstains.
PASSverdictThis layer approves.
DROPverdictRefuse silently.
REJECT / REJECT(Refused)verdictRefuse; look like nothing is listening — TCP RST, else ICMP port-unreachable.
REJECT(Prohibited)verdictRefuse; say policy did it — ICMP admin-prohibited for every protocol.
PROMPT(Handler[, Fallback])deferralAsk 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])effectSet flow tag Name to n (default 1).
TAG(Name, Add[, n])effectAdd n (default 1) to the tag; an absent tag counts as 0 first.
TAG(Name, Clear)effectRemove the tag; it reads as absent afterwards.
COUNT(Name[, n])effectEmit n (default 1) into stream Name.
COUNT(Name, Length)effectEmit the packet's byte length — the bandwidth primitive.
REPORT(level)effectEmit 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 layerBring the interface up and stand in the named profile: a path under Profiles\, /- or \-separated.
IGNOREverdict, Interface layerNever touch the interface; something else owns it.
DOWNverdict, Interface layerKeep 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. JOIN must 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 were NULL.

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 #

RawPacketPacketFlow
Judgedat the device seats, for all traffic, per packetonce per traversal, at the richest seat, per packetonce per flow (per local endpoint), at the IP seats, on the flow's first packet
Orderfirst inbound, last outboundbetweenlast inbound (after Packet), first outbound (before Packet)
FlowStatenever (the seat stands before conntrack)yesnever (the layer is the judgment of a flow)
Related, Start.*neverneveryes
Local, Local.*, Remote.*neverneveryes (Remote on loopback only)
Network.Id, Network.Name, Network.Trustyesyesyes
Per-packet facts (Length, TcpFlags, Fragment, Ttl, Dscp, EtherType, DstMac)yesyesnever
Tag.<name> readsnevertags RawPacket or Packet rules writeany tag
TAG writesyesyesyes
Fragmentmay be 1always 0—
Effects runper packetper packetper evaluation of the flow
Verdict scopethe packetthe packetthe 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
Judgedper interface, by netd, on interface and generation change
FactsInterface, Interface.*, Network.* (Network.Id, Network.Name and Network.Trust are the packet layers' too; the rest is this layer's alone)
VerdictsJOIN(profile), IGNORE, DOWN; strictness in that order, rising
EffectsREPORT only
BackstopIGNORE: an interface no rule speaks for is left as the kernel left it
Resultan 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.

LimitValueWhen exceeded
Distinct tags per flow64the write is refused and counted
Keys per counter table4096idle keys are reaped; if none are idle the new key is refused and counted
Windows per (stream, key)8the generation is refused
Longest window1 daythe generation is refused
Rule nesting depth12the generation is refused
Rules per layer4096the generation is refused
Attribution path in events96 bytestruncated
Tags reported per flow in the flows dump8the 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:

RuleConditionsActions
all—PASS

Rules\Packet — passes every tracked packet through to Flow, and the untracked housekeeping a host needs:

RuleConditionsActions
trackedFlowState.Equal = new, established, relatedPASS
outbound-okDirection.Equal = outPASS
loopbackInterface.Equal = loPASS
arpEtherType.Equal = arpPASS
icmpv6-housekeepingProtocol.Equal = icmpv6, IcmpType.Equal = 130-137, 143PASS
dhcp-clientProtocol.Equal = udp, SrcPort.Equal = 67, DstPort.Equal = 68PASS

Rules\Flow — the decisions:

RuleConditionsActions
outbound-okDirection.Equal = outPASS
loopbackInterface.Equal = loPASS
dhcpv6-clientDirection.Equal = in, Protocol.Equal = udp, SrcAddr.Equal = fe80::/10, SrcPort.Equal = 547, DstPort.Equal = 546PASS

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:

KeyValues
Profiles\defaultAddress.Offered = 1, Address.LinkLocal = 1, Route.Offered = 1, Dns.Offered = 1
Rules\Interface\wiredInterface.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 #

QuestionWhere 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.

Warning

Editing here writes live registry policy and can cut off this viewer or another remote session. Keep the old rule values and a local recovery path. The viewer does not document Network Manager's keep/undo countdown. After a save, check the generation or refusal banner and test the intended connection; an administrator can also use net policy wait.

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.

Note

Some firewall displays use example data today. This version of Network Manager does not yet read the live listeners, open connections, counters and recent audit events for those displays. It builds examples using this machine's addresses and the desktop connections really open now, then judges them against your real rules. Every affected section says Example data, and the status line says Example. The rules and everything outside the firewall are real. On Experimental, use the PNP viewer for the engine's own verdicts and live flows, subject to its access warning.

Choose a task #

TaskUseVerify afterwards
Find why the machine is offlineOverview, then the affected interfaceCarrier, address, gateway, selected rule/profile and any refusal.
Set an address, gateway or DNS serverProfiles; Profile rules if selecting a different profileThe profile's Result, affected interfaces, then live Overview and DNS.
Treat a recognized network differentlyNetworksWhat this changes before saving; profile and firewall results after.
Diagnose a nameDNS → Look up a nameActual answer, source, interface and server, not just the saved setting.
Allow or block a serviceRules → Test a connection, then the ruleAccepted generation and an actual connection test. Exposure and Activity contain example data.
Let a program listen on a portPort reservationsThe 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 doStart here
Check store health or inspect a certificate authoritySeeing what is in force, then the trust command reference
Add or distrust a certificate authorityCheck who may change it, then follow Adding and distrusting certificates
Check that a change took effectVerify the change; a recorded registry decision alone is not proof that consumers use it
Investigate degraded healthWhen 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.

InputWhere it livesWho changes it
Mozilla's root store/usr/share/ca-certificates/mozilla.crt, shipped by the ca-certificates packageAn upgrade of that package
Additions and distrustsMachine\System\Trust\Certificates in the registryAn 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.

Note

There is no update-ca-certificates and no directory to drop a .crt into. Both exist on other systems because trust policy lives in files there; here it lives in the registry, where changing it is an access check against a key's descriptor rather than a question of who can write to a directory.

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:

EventWhen
trustd.root.addedA root entered the set: an addition, a distrust lifted, or a root a bundle upgrade brought in
trustd.root.distrustedA distrust took a root out of the set
trustd.root.removedA 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's ControlSecurity (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:

  1. Run trust status. If health is degraded, follow the recovery steps below. If entries were skipped, read the warnings in trustd's log; a skipped entry does not establish trust.
  2. Run trust list to inspect the effective set. For an addition, use trust show <fingerprint> to check its full fingerprint, source and purposes. trust list --purpose ServerAuth filters the socket reply; it does not inventory or constrain the compatibility files. See the purpose limitation.
  3. For a distrust, check that the certificate is absent from trust list and that the decision appears in trust list --distrusted. The latter reads the registry, so it is not enough on its own. After trust 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/ssl files.
  • For a distrust list that would empty the store, review trust list --distrusted and 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; the rendered paths 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 successful trust list proves 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 #

CommandReadsWrites
trust list [--purpose P]socket—
trust list --distrustedregistry—
trust show <fingerprint> [--pem]socket—
trust statussocket—
trust add <name> <file|-> [--purposes A,B]the fileregistry
trust remove <name>—registry
trust distrust <fingerprint|file> [--reason R]socket, thenregistry
trust restore <fingerprint>registryregistry
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 #

StatusMeaning
0Done.
2Asked about something that is not there — no root matches, no such addition, not distrusted.
1It went wrong: the daemon is unreachable, the file is not a certificate, the registry refused the write.
64The 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 #

PathWho reads it
/etc/ssl/certs/ca-certificates.crtGo probes this first; most software is configured with it
/etc/ssl/cert.pemOpenSSL's default CAfile on this system
/etc/ssl/certs/<subject-hash>.0OpenSSL'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:

ValueEffect
1 (default)Rendered and kept current.
0No files. Programs that read a path find nothing and fail with certificate errors.
2Reserved. 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.

Note

The rendered files come out readable by everyone, which is correct — the set of certificate authorities a machine trusts is not a secret, and every program on the machine needs it. What is protected is changing it, which is a registry write.

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 settling or spike: 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.
  • reach is the last eight polls in octal, newest in the low bit: 377 is eight for eight, 376 means the newest poll was missed, and 0 means 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:

OptionMeaning
preferLead 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.

Important

Name at least three, with independent failure modes. One source has nothing to check it against. Two that disagree cannot be told apart, and the machine reports itself unsynchronised rather than picking one. Three can reject one wrong source when the other two are correct and agree; names alone do not guarantee independent or correct clocks.

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 #

CommandReadsActs
clock statussocket—
clock sourcessocket—
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:

StateMeaning
synchronisedNormal.
settlingBeing steered, but the frequency estimate is still converging. Usual for the first few minutes after boot.
spikeA large offset has appeared and is being timed to see whether it is real. The clock is deliberately untouched meanwhile.
unsynchronisedNothing 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
MarkStateMeaning
*system-peerChosen. The machine takes its stratum and root figures from this one.
+candidateAgrees with the majority and contributes to the combined answer.
-outlierAgrees, but too noisy to be worth including.
xfalsetickerDisagrees with the majority. Go and look at this one.
?unusableAnswering, but saying it is not synchronised itself.
unreachableNot 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 #

StatusMeaning
0Done.
2Asked about something that is not there — no sources are configured.
1It went wrong: timed is unreachable, or refused.
64The 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.

reg set -p Machine/System/Locale LANG sz:en_GB.UTF-8

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.

Note

Both settings screens save LANG and all five format categories together. If you have configured format categories individually, use reg for a one-value change: changing Language in the screen can also replace or remove those format overrides.

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:

reg new Users/S-1-5-21-1-2-3-1000/Locale
reg set Users/S-1-5-21-1-2-3-1000/Locale LANG sz:en_GB.UTF-8
reg set -p Users/S-1-5-21-1-2-3-1000/Locale LC_NUMERIC sz:de_DE.UTF-8

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:

locale
locale charmap
locale -k decimal_point

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.

KindWhat it isWhere it comes from
LogsLines of text that services and jobs writeThe service manager, peinit, which captures stdout and stderr except for services attached to a terminal with TTYPath
EventsStructured records of something that happened, such as a service started or an access refusedThe kernel and system programs
MetricsNumbers measured over time, such as how many requests a server has answeredPrograms 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 as sshd/ExecStartPre[0]. A job someone started carries jobs/ 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:

SourceWhat records it
ProgramsSystem programs such as peinit, the service manager
KernelThe kernel's event channel, KMES
SecurityKACS, the kernel's access control
RegistryLCS, 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: evctl runs 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 TTYPath writes 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:

evctl --format jsonl 'EVENTS kacs.* SINCE 1h ago TAKE 50' > incident-events.jsonl 2> incident-events.err
capture_status=$?
printf 'evctl exit status: %s\n' "$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:

StatusMeaningNext step
0The finite query completed and output publication succeeded.Keep the result with the diagnostics and context.
2Command-line usage error.Read the diagnostic, correct the invocation, and use new filenames.
1Query, 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.

  • CLI options and output channels
  • Query completion and publication
  • Exit status and broken-pipe handling, with failure classifications
  • Temporary spool and final copy

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-viewer opens on the events, gxwi-event-viewer --metrics on your dashboards, gxwi-event-viewer --settings on eventd's settings, and gxwi-event-viewer --logs NAME on what NAME logged.

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, so peinit.job.* is every job event and *.rejected every 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 saysWhich means
NothingYou 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 readOnly names the policy lists for you are shown
You can't read events on this machineNothing of that kind is yours to read
Some logs may be hidden from youThe 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 sshd for logs or kacs for events, covers that name and every name under it, such as kacs.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.
  ...
FactTells you
TierHow 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.
GatingWhat 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.
CardinalityHow 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 meansevman — the manual
Which events have happenedthe Event Viewer, or a query against eventd
Whether an event type is recorded at allits 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.

Note

The listen setting's built-in default is 127.0.0.1:7780, which is reachable only from the Peios machine itself. An image or administrator must configure a reachable address before another device can connect. See Settings for all users. Do not change the listen address casually: changing it can disconnect open desktops when GXWI next starts.

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 #

FileWhat it isWho can read it
/var/state/gxwi/key.pemThe private key, ECDSA P-256SYSTEM alone
/var/state/gxwi/certificate.pemThe certificateEveryone
/var/state/gxwi/certificate.sha256Its SHA-256 fingerprintEveryone

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.

GroupSectionsKept in
For YouWallpaper, Clock, Dock, Keyboard Shortcuts, Default AppsYour own part of the registry, under CurrentUser\
All UsersSign-In Screen, Desktop, Keyboard Shortcuts, Default Apps, AdvancedThe 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:

  1. Your choice, if you have made one.
  2. The machine's, set under All Users, for everyone who hasn't chosen.
  3. 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 #

  • Wallpaper, clock and dock
  • Keyboard shortcuts
  • Default apps
  • Settings for all users

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 #

ActionIdBuilt-in default
Toggle LauncherLauncherCtrl+Space, Alt+P
Open Dock ItemDockItemAlt (with 1 to 9)
Focus Previous TilePreviousTileAlt+J
Focus Next TileNextTileAlt+K
Previous WorkspacePreviousWorkspaceCtrl+Alt+←, Ctrl+Alt+J
Next WorkspaceNextWorkspaceCtrl+Alt+→, Ctrl+Alt+K
Move Window to Previous WorkspaceMoveToPreviousWorkspaceCtrl+Alt+Shift+←, Ctrl+Alt+Shift+J
Move Window to Next WorkspaceMoveToNextWorkspaceCtrl+Alt+Shift+→, Ctrl+Alt+Shift+K

Where they are kept #

Each action's shortcuts are one REG_SZ value, named by its id, in:

KeyWhose
CurrentUser\Software\Fenestra\ShortcutsYours
Machine\Software\Fenestra\ShortcutsThe 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:

  1. your choice for its exact type;
  2. your choice for its family;
  3. the machine's choice for its exact type;
  4. 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

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 are Machine\Generic\Icons Themes (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.

SettingIsKept in
Listen AddressThe address and port GXWI serves on. The default, 127.0.0.1:7780, is reachable only from this machine.Machine\Software\GXWI Listen
CompositorThe program each desktop session runs, as its user. Default /usr/bin/fenestra.Machine\Software\GXWI Compositor
ShellThe programs that provide the panels, dock and launcher, separated by spaces. Default /usr/bin/fenesh.Machine\Software\Fenestra Shell
OverlayA program shown to everyone in place of the sign-in screen, such as an installer.Machine\Software\GXWI OverlaySession
Overlay AccountThe 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 #

  • Desktop Settings

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.

QuestionSupported viewInterpretation limit
Which process is involved?Task Manager, ProcessesSome 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 --allThis selects the primary token; a thread may be impersonating.
What identity is a thread impersonating?token show --pid PID --tid TID --allIdentify the failing thread and record the time; it may have reverted since the failure.
What is this process's protection?logonse psb --pid PIDThe 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 inThe session list and the visible-process list have different access limits.
What rules protect a file?sd show ./report.txt --allQueries 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/AppThe default view is owner, group and DACL; SACL reading needs separate authority.
What was recorded about an earlier attempt?Event Viewer or evctlAudit 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_INFORMATION on the process, PIP dominance, and TOKEN_QUERY on the token's own descriptor. Administrator membership does not bypass PIP or a changed token descriptor.
  • Another process's PSB: PROCESS_QUERY_LIMITED on 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_CONTROL plus PIP dominance for owner, group and DACL inspection. SACL inspection additionally needs ACCESS_SYSTEM_SECURITY. See Inspecting processes.
  • The kernel's full session listing: Administrators or SYSTEM, through the descriptor on /sys/kernel/security/kacs/sessions. logonse reports 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 naming OWNER RIGHTS suppresses the owner's implicit READ_CONTROL | WRITE_DAC grant.
  • Mandatory label: the first applicable SYSTEM_MANDATORY_LABEL_ACE in the SACL and its policy bits.
  • Process-trust label: an applicable SYSTEM_PROCESS_TRUST_LABEL_ACE and 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.requested and access.granted. The requested mask is already generic-mapped to the object's rights.
  • access.denied-integrity and access.denied-trust, when present, identify bits withheld by the mandatory-integrity or process-trust checks.
  • trigger.ace identifies the audit ACE that caused a SACL-driven record; it is not a trace naming the DACL ACE that denied access.
  • fields.attestation.userspace marks 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:

  1. Could the impersonation token be used for access at all?
  2. Did mandatory integrity control (MIC) withhold the requested rights?
  3. Did process integrity protection (PIP) withhold them?
  4. Did the DACL grant them for this caller?
  5. Did a restricted-token pass, confinement or central policy narrow the grant?
  6. 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 #

  1. Keep the failing action, time, caller, object, requested rights and exact error together.
  2. Identify the process in Task Manager; inspect the relevant token and PSB using the documented commands and your existing authority.
  3. Read the descriptor components you are allowed to see. Mark inaccessible components as unknown.
  4. For a file, use sd check --explain with the identified target and rights; record which inputs the rehearsal does and does not reproduce.
  5. Correlate available audit evidence. Account for missing records and changes since the failure.
  6. 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 questionRead withWhat to check
Principaltoken userThe user SID; a displayed name is a label for that identity.
Groupstoken groupsGroup SIDs and attributes. Enabled groups can match allows; deny-only groups match denies.
Privilegestoken privsPresent and enabled are different. A disabled privilege cannot contribute, and presence alone does not bypass other policy.
Integritytoken integrityThe token's level; compare it with the object's label and mandatory policy when diagnosing MIC.
Logon contexttoken logon, token statsLogon type, logon SID and session identity. The LogonSession ID is separate from interactive-environment scope.
Confinement capabilitiestoken capsWhich capabilities are present; combine them with the confinement identity from the fuller report and the object's rules.
Claimstoken claimsUser/device attributes available to conditional ACEs; the application may additionally supply per-call local claims.
Default owner, group and DACLtoken owner, token group, token default-daclDefaults for objects created with this token, not the security descriptor of every existing object.
Origin and sourcetoken origin, token sourceThe originating session of a derived token and information about what minted it.
Type, impersonation level, restrictions, elevation and mandatory policytoken show --allThe 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 own TOKEN_QUERY grant. 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 /proc token 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 used privilege 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:

lsblk
mount

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 taskClassResult when an SD is missing
Use a provisioned system or application filesystemfacs_deny_missingDeny 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 SDsfacs_synthesize_ephemeralDerive 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 accessedfacs_synthesize_persistentDerive an SD and schedule write-back. Verify stored SDs before switching to deny-missing.
Use kernel pseudo-filesystems such as /proc and /sysunmanagedFACS 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 #

  1. Choose the class using Policy classes.
  2. Check storage and preservation requirements in SD storage by filesystem.
  3. Use Managing mounts to apply the choice, verify the result and interpret failures.

The commands #

  • lsblk: inspect block devices, filesystems and SD-derived owner/mode.
  • mount: list mounts, or attach a filesystem with policy= and an optional synthesis template.
  • umount: detach by mount point or source. Read its lazy, forced, recursive and all-targets behaviour before using those variants.

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.

Important

The original operator description says to use kacs_set_sd with WRITE_DAC, while warning that the caller may be unable to acquire the required access. The kernel reference separately documents an O_PATH/AT_EMPTY_PATH repair route under SeRestorePrivilege, and intermediate-traverse exceptions. Follow the documented repair contract for the deployed version; do not assume an inaccessible file can be repaired merely by changing its mount policy.

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.
  • /sys uses the kernel's per-operation rules, including writes restricted to BUILTIN\Administrators and 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:

  1. Parent-directory inheritance, using inheritable ACEs as for a newly-created child. See Inheritance.
  2. The mount-level template, commonly needed at the filesystem root where no parent on that filesystem supplies a usable descriptor.
  3. The fallback: owner and group SYSTEM; GENERIC_ALL for SYSTEM and BUILTIN\Administrators; GENERIC_READ | GENERIC_EXECUTE for 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 #

TransitionCheck before changing
Persistent synthesis to deny-missingConfirm the required files have stored SDs; files still missing one will be denied.
Ephemeral to persistent synthesisConfirm that saving metadata is intended and supported. Each later missing-SD access saves its newly derived result.
Deny-missing to a synthesis classTreat 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 #

  1. Identify the source and destination filesystem types with lsblk and the live mount listing.

  2. 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.

  3. For an offline ext image or unmounted ext filesystem, check the root xattr:

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

    Replace the example device only after identifying the target. This confirms presence on inode 2, not validity or coverage of every copied file.

  4. Verify stored descriptors on child files and directories as well, using filesystem-appropriate offline inspection on an unmounted target; do not use mounted sd output 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. The mke2fs -d path uses user.peios.sd as its source carrier; ordinary canonical-xattr copying is not a substitute for that population contract.

  5. 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:

Filesystemxattr name
ext4, XFS, Btrfs, tmpfs, others with generic xattr supportsecurity.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_sd call with an error indicating the filesystem cannot hold the SD.
  • Or, in the facs_synthesize_persistent case, refusing the write-back and treating the file as if it has no SD (re-synthesising next time).

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 #

Warning

The original guidance below describes ephemeral synthesis and udev-provided descriptors. The kernel reference instead documents deny-missing defaults for TMPFS_MAGIC, including devtmpfs, with kernel-seeded roots. Confirm the mounted instance's actual policy and descriptors; do not assume udev or synthesis supplied them.

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 #

Note

An unknown principal and a missing descriptor are not interchangeable. The guide's rationale below mentions unrecognised Windows principals; the kernel reference identifies a specific missing-$Secure case. A valid stored SD is evaluated; a corrupt one is denied even under ephemeral synthesis.

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 #

Warning

The forwarding description below includes StrataFS, but the StrataFS kernel contract says it does not ask a provider's policy to synthesise a missing descriptor: access through StrataFS is denied. Do not rely on a readable direct stratum path proving the merged path is readable. The live-boot example also assumes an explicitly synthesising squashfs; the kernel default is deny-missing and requires descriptors in the image.

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 #

FilesystemSD storageTypical mount class
ext4security.peios.sd xattr; ea_inode for large valuesfacs_deny_missing (for system mounts)
XFSsecurity.peios.sd xattr, native large supportfacs_deny_missing
Btrfssecurity.peios.sd xattr; verify size limitsfacs_deny_missing
tmpfssecurity.peios.sd xattr in memoryfacs_synthesize_ephemeral
devtmpfsxattr in memory, populated by udevfacs_synthesize_ephemeral
NTFS (ntfs3)system.ntfs_security xattr, NTFS-nativefacs_synthesize_ephemeral (typical)
FAT / exFATNo SD storage; in-memory onlyfacs_synthesize_ephemeral
NFS clientNo client-side storage; synthesise locallyfacs_synthesize_ephemeral
squashfsNo SD storage unless the image was built with onefacs_synthesize_ephemeral
overlayfsNone of its own — reads the real layerfacs_deny_missing
StrataFSNone of its own — reads the providerfacs_deny_missing, not settable
/procn/a — no FACSunmanaged
/sysn/a — kernel-managed SDsunmanaged

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:

lsblk
mount

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:

  1. Open the launcher and type disk.
  2. Select Mounts, then the intended mount to see its options and policy.
  3. 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 #

Warning

In kernel source 8e0e22de3a59cad506bbbf8873de456e16ad272d, both policy reads and writes accept enabled SeManageVolumePrivilege or SeTcbPrivilege; recording the privilege as used must also succeed. Administrator membership alone does not satisfy this gate. This is a pinned source finding, not a test of the kernel deployed on your machine or a historical release boundary.

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 #

Warning

Ephemeral synthesis avoids writing back synthesised security descriptors; it does not guarantee an unchanged disk. Even a read-only ext4 mount can replay its journal. If preservation requires no disk writes, use a supported inspection procedure for that filesystem. How the installer's disk scan stays read-only explains its additional safeguards; a policy choice alone is not that procedure.

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 mount exit 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 TARGET pair. Supply both, or use --source / --target.
  • A lone target is valid only for -o remount and for a standalone propagation change (--make-*), which act on an existing mount point.

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

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

Canonicalisation #

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

VerbHow to request itWhat it does
New mountmount [-t T] SRC TGTAttach a fresh instance of the filesystem at SRC onto TGT.
Bind-B / --bind, or -o bindMake an existing subtree visible at a second location.
Recursive bind-R / --rbind, or -o rbindBind a subtree together with every mount underneath it.
Move-M / --move, or -o moveRelocate an existing mount to a new mount point.
Move beneath--beneath SRC TGTAttach SRC beneath the mount currently at TGT (the kernel enforces several constraints; violations surface as exit 32).
Remount-o remount[,...] TGTChange the options of an existing mount (see Remounting).
Propagation--make-* / --make-r*, or the -o tokens belowChange how mount/unmount events propagate across a subtree.
Listmount / mount -lPrint 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 tokenRecursive flagRecursive -o tokenMeaning
--make-sharedshared--make-rsharedrsharedEvents propagate to and from peer mounts.
--make-slaveslave--make-rslaverslaveEvents propagate in from the master but not back out.
--make-privateprivate--make-rprivaterprivateNo propagation either way.
--make-unbindableunbindable--make-runbindablerunbindablePrivate, 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 #

FormResolution
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 fileUsed 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 fileAttached 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).

OptionEffect
ro / rwRead-only / read-write.
suid / nosuidHonour / ignore set-user-ID bits.
dev / nodevAllow / disallow device nodes.
exec / noexecAllow / disallow execution.
atime / noatimeUpdate / never update access times.
relatime / norelatimeRelative-atime updates on or off.
strictatime / nostrictatimeStrict-atime updates on or off.
diratime / nodiratimeDirectory access-time updates on or off.
nosymfollowDo 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 #

OptionEffect
sync / asyncSynchronous / asynchronous writes.
dirsyncSynchronous directory updates.
lazytime / nolazytimeLazy on-disk timestamp updates on or off.
iversion / noiversionInode version counting on or off.
silent / loudSuppress or emit certain kernel messages.
mand / nomandObsolete (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=:

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

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 #

OptionEffect
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=DIRAttach 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.noloopSuppress the implicit loop device for a regular-file source.
X-mount.auto-fstypes=LISTConstrain 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:

OptionPolicy class
-o policy=deny-missingfacs_deny_missing — a file with no SD is unreachable.
-o policy=synth-ephemeralfacs_synthesize_ephemeral — a missing SD is synthesised in memory only.
-o policy=synth-persistfacs_synthesize_persistent — a missing SD is synthesised and written back.
--synth-sddl SDDLProvide 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.

Warning

The peiosutils 0.8.18 diagnostic still says the policy operation requires SeTcbPrivilege. That is not the full gate in the pinned kernel. See Check policy access before interpreting this as a need to grant TCB; administrator membership alone is not enough, and automatic escalation or a global CAP_SYS_ADMIN remap is not a remedy.

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 #

FlagEffect
-t, --types TYPEFilesystem type, or auto.
-o, --options LISTMount options (above); repeatable.
-r, --ro, --read-onlyMount read-only (-o ro).
-w, --rw, --read-writeMount read-write and forbid the automatic read-only fallback on a write-protected device (-o rw).
--source SRC / --target DIRName the operands explicitly.
--target-prefix DIRPrepend DIR/ to the target.
-B, --bind / -R, --rbind / -M, --move / --beneathThe structural verbs.
--make-*, --make-r*Propagation changes.
--exclusiveForce 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 UUIDSelect the source by filesystem label / UUID.
-c, --no-canonicalizeDo not canonicalise paths.
-f, --fakeSkip mount and policy syscalls; target preparation still runs. With --mkdir or X-mount.mkdir, directories may be created. See execution limits.
-v, --verboseNarrate resolved values and each syscall; drain the kernel fs_context log. Repeatable but -vv is the same as -v.
-l, --show-labelsIn list mode, append each filesystem's label.
--onlyonceAccepted 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 NSOperate 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-onlyDo not invoke a mount.<type> helper.
-n, --no-mtabAccepted and ignored (Peios has no mtab).
--synth-sddl SDDLKACS synth-policy template SD (above).
-h, --help / -V, --versionStandard.

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.
  • --fake can 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=0700 for 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 #

CodeMeaning
0Success.
1Incorrect 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".
2System error (out of memory, no free loop device, cannot fork).
4Internal error (an invariant failure).
8Interrupted (SIGINT), after signal-safe cleanup.
32Mount failure — a syscall in the flow failed, a label/UUID had no match, a probe was undetermined, or a --beneath/move/--exclusive kernel refusal.
126An 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, umount refuses with an error naming the candidates — resolve it by naming a specific mount point, or use -A to unmount all of them.

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

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

Recursive and all-targets unmounts #

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

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

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

Flags #

FlagEffect
-l, --lazyDetach the filesystem now and clean up references later (MNT_DETACH).
-f, --forceForce the unmount (MNT_FORCE), e.g. for an unreachable server.
-R, --recursiveUnmount the target and everything under it (see above).
-A, --all-targetsUnmount every mount point of the given source (see above).
-d, --detach-loopAfter 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-onlyIf the unmount fails, remount the filesystem read-only instead (via mount_setattr). Mutually exclusive with -R.
-g, --gracefulExit 0 when the target is absent or not mounted (rather than failing). Applied unconditionally.
-q, --quietSuppress "not mounted" messages.
-c, --no-canonicalizeDo not canonicalise paths; selects UMOUNT_NOFOLLOW. Applied unconditionally.
-v, --verboseSay what is being unmounted. Repeatable.
-N, --namespace NSEnter 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-mtabAccepted and ignored (no mtab on Peios).
-i, --internal-onlyDo not invoke a umount.<type> helper (none ship).
--fakeDry run: resolve operands and report, but skip the unmount syscalls.
-h, --help / -V, --versionStandard.

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 #

CodeMeaning
0Success (or a -g no-op on an absent target).
1Incorrect invocation or a permission denial — including -R with -r, a missing operand, an ambiguous source, an embedded NUL, or an authorisation failure.
2System error (e.g. cannot read the mount table).
8Interrupted (SIGINT).
32Unmount failed, or the target is not mounted (unless -g).
64Several source/target arguments were given and at least one succeeded while another failed.
126An 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/block unpatched, 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 — OWNER and MODE, read the same way ls -l reads them.
  • The /dev/disk/by-* symlink farm — ID-LINK. Populated by the device manager once it has run; there is deliberately no /run/udev/data parser, so the column reads the links themselves and is empty in the initramfs, where no device manager runs.

Output modes #

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

FlagMode
(default)Indented tree.
-l, --listFlat list — the same columns, no tree glyphs.
-J, --jsonJSON. Flag columns render as bare booleans and MOUNTPOINTS as an array; children nest under a children key.
-P, --pairsKEY="value" pairs, one device per line.
-r, --rawRaw, 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):

FlagColumn set
-o, --output LISTExactly the comma-separated columns named (case-insensitive; an unknown name is a usage error).
-O, --output-allEvery available column.
-f, --fsFilesystem view: NAME, FSTYPE, FSVER, LABEL, UUID, MOUNTPOINTS.
-m, --permsPermissions 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 #

FlagEffect
-a, --allInclude empty (zero-size) devices, which are hidden by default.
-d, --nodepsDo not print a device's holders or slaves (drop the children).
-I, --include LISTShow only devices with these major numbers (comma-separated).
-e, --exclude LISTExclude devices by major number. The default is 1 (RAM disks); an explicit -e replaces that default, and -a clears exclusions entirely.
-s, --inversePrint dependencies in inverse order (holders above the devices they depend on).
-M, --mergeCollapse a subtree shared by several parents (a multipath device) to a single occurrence.
-E, --dedup COLUMNDrop rows whose COLUMN value duplicates an earlier one.
-x, --sort COLUMNSort siblings by COLUMN (numeric columns sort by value, not text).

Formatting #

FlagEffect
-b, --bytesPrint SIZE as an exact byte count instead of a human-readable value.
-p, --pathsPrint full /dev paths in the NAME column.
-n, --noheadingsOmit the header row.
-i, --asciiDraw the tree with ASCII characters instead of box-drawing glyphs.
-y, --shellRender column keys shell-safe (MAJ:MIN becomes MAJ_MIN).
-w, --width NUMTruncate each table row to NUM columns wide.
--sysroot DIRRead sysfs, the mount table and /dev from DIR instead of / (chiefly for testing against a fixture).
-h, --help / -V, --versionStandard.

Exit status #

CodeMeaning
0Success.
1Incorrect 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.

part list
lsblk
mount

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.

Warning

The device paths in these guides are examples. Before any partition-table write or format, verify the whole disk or partition, preserve data you need, and release mounts and other users of that device. Never run an example against a disk merely because its name matches the page.

Where to start #

What you need to doGuide
Find software, configuration, service state or user dataWhere files live
Inspect disks, partitions and mounts on the desktopDisk Manager, currently inspection-only
Diagnose block or inode pressure without changing dataFind why a filesystem is full
Write or inspect a GPTPartitioning
Create an ext filesystem with its root permissions already presentFormatting with security descriptors and mke2fs
Copy the live system onto bootable storageInstalling to disk
Record a filesystem, partition or hardware identityStable device names
Attach existing storage and choose missing-SD behaviourMount policies
Explain a file in a merged system directoryStrataFS

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 #

PackageContents
net.sourceforge.e2fsprogsThe tools below.
net.sourceforge.e2fsprogs-develHeaders, linker symlinks and pkg-config files for building against the libraries.
net.sourceforge.e2fsprogs-staticStatic archives.
net.sourceforge.e2fsprogs-libext2fs, net.sourceforge.e2fsprogs-libe2p, net.sourceforge.e2fsprogs-libcom-err, net.sourceforge.e2fsprogs-libss, net.sourceforge.e2fsprogs-libuuidThe runtime shared libraries, packaged separately so a consumer can depend on one without pulling the tools.

The tools themselves:

ToolPurpose
mke2fs, mkfs.ext2, mkfs.ext3, mkfs.ext4Create a filesystem. This is where Peios security descriptors enter.
e2fsck, fsck.ext2, fsck.ext3, fsck.ext4, fsckCheck and repair a filesystem.
tune2fsChange parameters on an existing filesystem, including enabling features after the fact.
resize2fsGrow or shrink a filesystem.
dumpe2fsPrint superblock and block-group information.
debugfsInteractive low-level access to a filesystem image, including reading and writing extended attributes directly.
blkid, findfsIdentify filesystems by label, UUID or type.
e2label, e2image, e2undo, e2freefrag, filefrag, badblocks, logsaveLabelling, imaging, undo, fragmentation and block-scanning utilities.
chattr, lsattrRead and set ext2/3/4 inode attributes.
uuidgenGenerate 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:

ToolPurpose
mkfs.fat, mkfs.vfat, mkfs.msdosCreate a FAT12/16/32 filesystem.
fsck.fat, fsck.vfat, fsck.msdosCheck and repair a FAT filesystem.
fatlabelRead 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 forLocation 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:

stratafs list
stratafs list /etc
stratafs resolve /bin/sh

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:

  1. /system/retc: generated registry output.
  2. /lcl/etc: local configuration, also the create stratum.
  3. /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.

Note

These current-source /etc and /lib64 descriptions differ from the install-destination specification. Check the deployed view before choosing an override location.

/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.

  1. Inspect disks and partitions with part list and lsblk.
  2. Choose the identity that matches the object you mean: filesystem UUID, partition UUID or hardware identity.
  3. 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:

DirectoryKeyed byExample
by-uuidthe filesystem's UUID, written when it was formatted4ED7-A6AC -> ../../vda2
by-labelthe filesystem's labelPEIOS -> ../../vda
by-partuuidthe GPT partition entry's UUID2036ae68-…-f118299d5446 -> ../../vda2
by-partlabelthe GPT partition entry's name
by-idthe hardware's own identity: model and serial, or WWNnvme-QEMU_NVMe_Ctrl_peiosnvme1 -> ../../nvme0n1
by-paththe bus position the device sits at
by-diskseqthe 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.

df -hT /affected/existing/path
df -i /affected/existing/path

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:

du -x -h --max-depth=1 /known/tree

For suspected inode pressure, count entries instead of allocated space:

du -x --inodes --max-depth=1 /known/tree

Pass the directory itself, not a shell * expansion that omits dotfiles. Start narrowly and drill into one observed large directory at a time.

Important

--max-depth=1 limits printed depth, not traversal. du still walks the tree to compute totals and can cause substantial I/O. -x skips subdirectories on a different device; it does not bound the work or exclude every same-device bind/mount alias. Do not begin with a whole-system scan merely to get a short report.

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 -x skips other devices.
  • Access failures can hide contents from the walk.
  • By default, du counts 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.

Warning

A partition-table write has no undo in part. Back up needed data and check the disk's identity, size, existing contents and mount state. A safety refusal is a reason to stop and inspect, not an instruction to add --force.

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 thereWhat part says
an MBR"an MBR (dos) partition table, which part cannot manage"
an Apple, BSD, Sun or SGI labelnames 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 #

Warning

The following example replaces the target disk's partition layout, then creates a 512 MiB ESP and a root partition using the remaining largest run. /dev/vda is an example whole disk, not a safe default. Confirm its identity, preserve any needed data and release all mounted partitions before running it. --yes is the destructive-operation confirmation; there is no prompt.

# 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:

AliasMeaning
espEFI system partition
linuxLinux filesystem data
swapLinux swap
msdataMicrosoft 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 #

CodeMeaning
0success
1usage error, or the operation failed
2could not read or write the device
3refused 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.

diskpartpart
how you use itan interactive shell: select disk 0, then act on itone command, one device, one job
what it coverspartitions, formatting, drive letters, dynamic disksthe 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:

  1. Identify the partition using Partitioning and Stable device names.
  2. Back up needed data and ensure the target is not in use. Formatting is destructive; it is not a repair for one missing descriptor.
  3. Choose the root ACL and its inheritance deliberately. A private home tree needs per-directory rules; a single root ACL cannot express every policy.
  4. If populating from a source tree, check its user.peios.sd carriers before invoking mke2fs -d.

The root SD, and what it reaches #

Warning

mke2fs creates a new filesystem and destroys the previous contents of its target. Confirm that /dev/vda2 is the intended unmounted partition before using this example. This example grants full control to SYSTEM and Administrators; it does not grant ordinary users read access.

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.

Important

A node with no source carrier is documented as receiving no stored SD during -d population. The same guide says it inherits on first access, while deny-missing denies missing descriptors. Verify the population and chosen mount policy before relying on such a node; this discrepancy is unresolved.

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.

Warning

Formatting destroys the target's previous filesystem contents. Identify and unmount the intended partition, preserve needed data, and replace the example /dev/vda2 only after checking its identity. Do not use formatting to repair permissions on a filesystem you need to keep.

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.

OptionMeaning
root_sddl=SDDLThe descriptor as an inline SDDL string.
root_sd_file=PATHThe 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 nodeParent in the new filesystemResult
Has user.peios.sdHas a descriptorExplicit and inherited ACEs merged, written to security.peios.sd.
Has user.peios.sdHas noneThe explicit descriptor written verbatim.
Has no user.peios.sdEitherNothing 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.

Important

The population contract above leaves nodes without user.peios.sd unstamped, yet says they inherit on first access. That needs reconciliation with deny-missing policy. Check stored SDs before depending on those nodes on a strict mount; access under synthesis is not proof that metadata was preserved.

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.

SettingUpstreamPeiosReason
inode_size256512Keeps a typical security descriptor in the inode's inline extended-attribute space, where reading it costs no extra I/O.
default_mntoptsacl,user_xattruser_xattrPeios uses security descriptors, not POSIX ACLs.
ext4 features—+ ea_inodeLets an oversized descriptor spill into its own inode rather than failing to fit.

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

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

Resolution order for the profile, highest first:

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

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

Exit status #

CodeMeaning
0Success.
1Failure. 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. A root_sd_file is 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 quota feature 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.

Important

Disk Manager is currently inspection-only. Its partition, format, mount and other change forms run checks but end with a status message saying nothing changed. The privileged disk service has not been built. Use part, mke2fs and mount for supported command-line operations.

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 ManagerPolicyMeaning for a missing SD
Deny Accessdeny-missingSD-based access is denied until a valid descriptor is supplied.
Give Permissions, Not Savedsynth-ephemeralDerive from parent/template and keep it in memory. No SD is written to the disk.
Give Permissions and Save Themsynth-persistDerive an SD and schedule it to be saved.
Not ManagedunmanagedFACS 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:
GoalOperationEffect
Replace everything on a disk with a new Peios systemInstallErases the disk; first-boot setup creates the administrator account.
Keep an installed system and move to the medium's newer releaseUpgrade an installationUpdates eligible packages and boot files; does not run first-boot setup again.
Restore boot files, check the root filesystem, or reseed its root descriptorRepair an existing systemRuns the repair you select; it is not a reinstall.

Caution

Installation erases the whole selected disk, including other operating systems, recovery partitions, encrypted volumes, and files. A filesystem the installer cannot inspect may have no contents listed in the confirmation; that does not protect it from erasure. A failed install after partitioning has already erased the disk.

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 #

  1. Choose the operation on the first page.
  2. 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.
  3. 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.
  4. 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.
  5. 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 reboot does; the surface itself has no restart privilege.

Console controls #

KeyAction
Tab / Shift+TabMove between fields, including disabled fields. The footer explains why a field is disabled.
Up / DownMove through a list; at either end, move to the next field.
EnterPress the highlighted action or advance from a field.
SpaceToggle a checkbox.
EscAsk before leaving.
F1Show key help.
Ctrl+LRepaint 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:

cat /var/state/gxwi/certificate.sha256

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.

  1. Select the disk and compare the installed and medium releases on the confirmation page. Both show the edition and full package version.
  2. Check that Upgrade is enabled. It is enabled only for a newer version of the same edition, using peipkg's ordering including revision.
  3. 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:

  1. Upgrade the edition and its dependency closure, using the alternate-upgrade bypass for the medium's fixed, intentionally stale repository index.
  2. Stage the release's shared autoapply seeds with upgrade-peios --seeds-only. Install-only setup seeds are not repeated.
  3. Upgrade other packages for which the medium carries a newer revision, in every root including the initramfs. Edition dependencies are floors rather than pins.
  4. 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.

Important

A rebuild without a version or revision increase is not an upgrade. Peipkg orders by version and then revision. If the edition package has the same version and revision as the installed one, the medium is already current even if other build contents changed. A build intended for upgrade must bump the edition and every changed package it wants upgraded.

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.

OperationWhat it doesLimit or next step
Repair boot filesRegenerates 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 filesystemRuns 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 descriptorsRe-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.

Caution

These commands format their targets. Device names below are examples, not recommendations. Check the actual disks and partitions with part list before substituting your targets. The script copies live accounts and credentials; read the account warning before using the result.

peios-install --yes --whole-disk /dev/vda      # partition the disk, then install
peios-install --yes /dev/vda1 /dev/vda2        # format and install onto existing partitions

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:

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

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.

Caution

Treat a copied development account as publicly accessible: it may retain a known image password or a passwordless policy. Secure it before using the machine on a network that matters. For the live peios account, set a password and then require password sign-in:

lps password peios
lps policy peios password

Setting the password alone does not change a passwordless account's sign-in policy. See Creating accounts.

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:

mke2fs -t ext4 -E root_sddl="O:SYG:SYD:(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)(A;OICI;GRGX;;;WD)(A;OICIIO;GA;;;S-1-3-0)" /dev/vdb2

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:

sd show /

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:

ValueSetting
OverlayUsernamepeios, 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.
  • Understand format-time descriptors.
  • Inspect the boot hooks.
  • Understand missing-descriptor failures.

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.

Warning

Until setup is complete, anyone who can open the machine's address or use its console can choose the administrator account and password. Keep an unconfigured machine off networks you do not trust.

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:

  1. Enter an address with its network prefix, such as 192.168.1.20/24.
  2. Add a gateway and name servers if needed. A gateway must be on the address's network.
  3. 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, or Authenticated 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:

  1. Create the account.
  2. Set the machine name.
  3. 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:

cat /var/state/gxwi/certificate.sha256

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.

Note

The PC installation guide records a 2026.8 HTTPS limitation: after a manual address change, the page can remain on its starfield instead of reconnecting. Once setup finishes, open https://<new-address>:7780 manually. A browser may warn again for the new address; verify the fingerprint rather than assuming that any new warning is safe. See First boot on a PC.

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.

Warning

While the temporary account exists, any sign-in mechanism accepting a passwordless account can sign in as it. It has no groups; its intended access is setup itself, which is already available at the machine's address. Network isolation remains important until setup has finished.

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 #

SymptomNext step
Account, password, or machine name is rejectedRead the field error and compare it with the rules above. Validation happens before final application.
No network or Wi-Fi joining is disabledContinue 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 changeCheck the address on the machine's screen and open the new HTTPS address yourself. See the 2026.8 caveat above.
Browser setup is absentThe 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 failedIt 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 existsBoot the install medium for recovery. Do not assume you can sign in to repair setup locally.
Console text is damagedPress Ctrl+L to repaint.
Form no longer fits after resizing a serial-terminal windowThe 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.

ServiceResponsibility
oobedRuns as SYSTEM to create the first account and set the machine name. Serves the MSIP conversation on /run/oobed.sock.
oobe-tuiDraws 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:

stratafs list
stratafs list /bin

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:

stratafs resolve /bin/sh

The per-stratum states are:

StateMeaning
providerThis object currently provides the name.
participantThis lower directory contributes to the merged directory.
shadowedA higher object of the same type wins.
maskedA provider of another type hides this object.
absentThis 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:

stratafs origin /bin/sh

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

Inspecting local state #

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

stratafs sweep /bin

Each result is classified as:

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

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

To inspect the content of an override:

stratafs diff /bin/sh

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

Structured output and status #

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

Exit status has probe-friendly meaning:

StatusMeaning
0Success; for sweep, no entries; for diff, no difference.
1sweep found entries or diff found a difference.
2Usage, 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 resolve and diff before removing the override.
  • A routing answer is not permission to mutate. write: copy_up still 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:

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

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

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

The base Peios topology #

The 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:

ViewStrata, 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.

Warning

Ownership is not a recovery guarantee. An OWNER RIGHTS ACE can suppress the owner's implicit READ_CONTROL and WRITE_DAC, and other access layers can still deny a change. Taking ownership preserves the existing DACL, including that suppression. Before removing access, establish which authorized principal can still read and repair the policy. See Ownership and implicit rights.

1. Inspect the path and current policy #

These commands only inspect the example file; replace ./report.txt with the intended path:

sd show ./report.txt --all
sd show ./report.txt --sddl
sd check ./report.txt read --explain

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 resultDocumented command familyWhat to check first
Add an allow or deny ACEsd allow, sd denyPrincipal, exact rights, ACE order and inheritance flags. --replace removes existing rules for that principal and kind before adding.
Remove a principal's DACL rulessd removeIt removes every allow and deny rule for each named principal. Removing a deny can increase access.
Change owner or primary groupsd owner, sd groupWRITE_OWNER and the new-owner SID restrictions below; ownership does not grant file-data access.
Change audit rulessd audit, sd unauditSACL authority. unaudit removes every SACL rule for the named principals.
Change an integrity labelsd integrityIntended 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 childrensd inherit, sd reset, sd propagateWhether explicit child rules or protection should survive; inspect the affected descendants first.
Replace a reviewed descriptor or selected componentssd setThe 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.

Warning

Do not add --recursive as a convenience. It changes descendants as well as the named object, implies no-follow-symlinks, and is not a tree-wide transaction. sd propagate reports descendants it cannot change and continues. Protected descendants keep their own rules, but files below them can still be refreshed from those descendants. Review failures and partial results individually.

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:

sd allow ./report.txt @self:read

@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:

sd show ./report.txt --all
sd check ./report.txt read --explain

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 #

ComponentReading needsChanging needs
Owner or primary groupREAD_CONTROLWRITE_OWNER; new-owner SID validation also applies.
DACLREAD_CONTROLWRITE_DAC
SACLACCESS_SYSTEM_SECURITYACCESS_SYSTEM_SECURITY, gated by SeSecurityPrivilege.
Integrity label onlyREAD_CONTROLWRITE_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() needs WRITE_DAC in the fd's granted mask; otherwise it fails with EACCES (EBADF for an O_PATH fd). Legacy opens request this as a compatibility right.
  • chmod() runs a fresh check for WRITE_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 needStart here
Find, install or remove softwareInstalling and removing packages
Apply updates or reverse a committed changeKeeping a system current
Add or repair a package sourceRepositories and trust
Understand an unexpected plan or a separate approvalDependency resolution
Check files or identify their ownerInspecting and verifying
Recover after a failed or interrupted operationTransactions and recovery
Choose which package supplies a shared nameClaims
Work on an offline installation or initramfsNamed roots
Turn a feature on or remove its setupUsing feat
Assemble a new image's package treeComposing 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].

CommandDoes
installInstall packages, with dependencies, from a repository or a local .peipkg file.
remove (alias uninstall)Remove installed packages.
upgradeUpdate installed packages under the available sources and constraints.
downgradeMove one package to a specific older version.
undoReverse the most recent committed transaction as a new change.
claimShow or change which package holds a claim — a shared name that exactly one package may own.
refreshUpdate the cached metadata of the configured repositories.
repoConfigure repositories — add, list, remove.
rootManage named roots — add, list, remove, show.
listList the installed packages.
infoShow one installed package's details.
filesList the files a package owns.
ownsReport which package owns a given path.
searchSearch the configured repositories for a package.
verifyCheck installed files against what was recorded at install.
historyShow the transaction log.
recoverRecover pending work; cross-root recovery may roll forward.
cleanDelete 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 #

SectionShowsDoesThe command
InstalledEvery installed package, searchable, with its version and where it came fromOpens one: its details, what it needs and provides, and its files. Check Files, Update to…, Remove…list, info, files, verify, upgrade <name>, remove
AvailableWhat the repositories offer that isn't installedInstall, and Install from File… for a .peipkg filesearch, install
UpdatesThe newer versions waiting, after refreshing the trusted repositoriesUpdate one, or Update Allrefresh, upgrade --dry-run, upgrade
HistoryEvery change, newest first, by what it didUndo… the most recent, and Resolve an interrupted onehistory, undo, recover
RepositoriesEach repository, its address, how many packages it offers, when it was refreshed, and whether it is out of date or not yet trustedRefresh All, Add Repository…, Trust…, Remove…repo list, refresh, repo add, repo remove
RolesEach claim, what holds it, and its linksChoosing another package moves the claim to itclaim
Installation RootsThe named rootsNothing: roots are added with peipkg rootroot 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:

  1. The plan. What will be installed, updated, moved back or removed, with sizes, before anything happens.
  2. 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.
  3. The plan's own question: Install, Update, Remove or Undo. For a removal, Cancel is the keyboard's.
  4. Progress, through downloading and checking, preparing, putting the changes in place and recording them.
  5. 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-stale does, 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-unowned does. 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's default_root can 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.

OptionEffect
--dry-runProduce and print the plan, then stop before its approval questions and package application. Planning can still refresh repository metadata for trust-freshness checks.
--yes, -ySkip the proceed? prompt and apply the plan.
--no-claimInstall a provider without taking any claim it offers.
--allow-staleProceed 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-allForce-claim every claim the installed packages provide, overriding incumbents.
--dangerously-bypass-path-restrictionsPermit 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.

Warning

The installation validation reference says the special-package bypass skips the destination check entirely, including /lcl/policy; earlier operator guidance said that path was excluded. This documentation does not establish a narrower boundary. Only grant the bypass after reviewing the package's destinations and the authority of the account running it.

--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.

OptionEffect
--cascadeAlso remove every installed package that depends on the ones named.
--dry-runPrint the plan and stop.
--yes, -ySkip 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 #

CodeMeaning
0The operation succeeded — or, for --dry-run, the plan was produced. A declined prompt is also 0: nothing failed.
1The operation failed — a package was not found, a dependency could not be satisfied, a download or verification failed, or a file operation was denied.
2A 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 situationCommand
Apply available updatesupgrade, after refresh and plan review
Reverse the most recent committed transactionundo, after checking history and its dry run
Restore one package to a particular older versiondowngrade <package> <version>
An operation was interrupted and is pendingrecover, 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:

OptionEffect
--dry-runPrint the plan and stop. A good way to preview what an upgrade would move.
--yes, -ySkip the proceed? prompt.
--no-recurseConfine the upgrade to the current root only — disable the cascade into nested named roots.
--allow-staleProceed 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 #

CodeMeaning
0The operation succeeded — including a dry run, a plan with nothing to do, and a declined prompt or authorisation (nothing failed).
1The 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).
2A 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 #

TaskCommand or check
See configured sourcespeipkg repo list
Inspect trust and freshnesspeipkg repo list --json
Add a new sourcepeipkg repo add <name> <base-url> --anchor <fingerprint>
Trust configuration already supplied by an imagepeipkg repo add <name> after checking that configuration
Fetch current metadatapeipkg refresh <name>
Stop using a sourcepeipkg 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-stale when 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:

  1. peipkg fetches the repository's descriptor and its signing keys.
  2. It checks the fetched keys against the anchors you supplied — a fingerprint must match bit for bit.
  3. 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.

OptionDefaultEffect
--anchor FINGERPRINT—A signing-key fingerprint to trust. Repeatable.
--priority N50Resolution priority — a lower number wins. See below.
--policy required|optionalrequiredWhether a valid signature is required. See Signature policy.
--min-index-version N0A freshness floor for the index. See Freshness.
--max-trusted-age-days N30How stale the repository's trust state may grow before operations force a refresh. See Freshness.
--max-index-staleness-days N90How old the index's own generated_at may be before operations force a refresh. See Freshness.
--insecureoffPermit 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_anchors under the required policy, because there would be nothing to verify the descriptor against.
  • A failed ceremony leaves the .repo file 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:

StatusMeaning
activeIn current use for signing.
transitioningBeing phased in or out — honoured until its stated expiry.
revokedWithdrawn — 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.

Warning

The refresh reference says this additional index-age check is not implemented, in conflict with the documented option here. This guide preserves the option and its stated limits, but does not establish which behaviour your build implements. Inspect index_generated and the publisher's current metadata rather than relying on that bound alone.

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.

base_url         = "https://pkgs.peios.org"
priority         = 10
signature_policy = "required"
trust_anchors    = ["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 #

CodeMeaning
0The operation succeeded — the repository was added, listed, or removed.
1The 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.
2A 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 seeWhat to do
A pending transaction after an interruptionRun peipkg recover, then inspect history and verify files
A committed update you want to reverseReview undo or downgrade
A rolled-back record but damaged or missing filesFollow failed-rollback checks; history alone is not proof of restoration
A committed change with a maintenance warningRead the warning; a failed post-commit side effect does not undo the package change
An interrupted feature scriptUse feat recovery; package recovery does not reverse feature setup

Warning

The failed-rollback reference retains an earlier closed-journal failure description: history could say rolled-back even though files were not restored. The source-verified cross-root path below instead reports failed preparation rollback and retains the affected pending journal. Inspect the actual state rather than applying either description to every version. Filesystem visibility also differs from database durability: files can change before package state commits. Do not assume a failed operation left the system unchanged or restored.

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
  1. 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.
  2. 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 by recover.
  3. Read the result and inspect history again. Recovery can itself fail.
  4. Verify the affected packages, or all packages as above. Review each difference against intentional edits and any .peipkg-new file.

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:

  1. Retain the original named-root topology and make every participating root present and reachable. Run peipkg recover explicitly from the same system anchor, using the original --root TARGET if 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.
  2. 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.
  3. 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 recovery in the transaction. Keep the diagnostics and treat any refused or unreachable root as unresolved.
  4. 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 depmod or man-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 verify to 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 reinstall verb: 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
OptionEffect
-n NShow at most N transactions. -n 0 shows all of them. The default is 20.
--jsonEmit 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 #

NameMeaning
<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 #

  • Reverse a completed update
  • Inspect files and ownership
  • Check named roots
  • Understand package audit records

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 replaces or --cascade remove 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 --cascade only 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:

ActionWhy it is elevated
A downgradeMoving 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 replacesA 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 providesA 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:

  • --yes does not satisfy it. --yes skips the routine proceed? 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. Generic y or yes, 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 #

  • Apply an install or removal
  • Check repository trust and priority
  • Choose a claim holder

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:

OptionEffect
--no-claimClaim 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-allForce-claim every claim the installed packages provide, overriding incumbents.

Two combinations are hard errors:

  • --claim-all together with --claim.
  • --claim-all together 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.

OptionEffect
--yes, -ySkip 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 #

CodeMeaning
0The operation succeeded — the status was reported, or the grant or revoke was applied (a declined prompt is also 0: nothing failed).
1The 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.
2A 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 /.

QuestionCommand
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 #

  1. Use info and files to establish the package, version and affected paths.
  2. Compare the result with intended edits. For configuration under /usr/etc/ or legacy /etc/, a .peipkg-new sibling 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.
  3. 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.
  4. Repair deliberately, then verify again. There is no reinstall command; 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 #

CodeMeaning
0The command succeeded. For plain verify, every checked file was intact. verify --json also exits 0 when it finds problems: inspect the array.
1The command failed — a named package is not installed, a path is owned by nothing, or, for plain verify, at least one file had diverged.
2A 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.

OptionEffect
--purgeAlso 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.

OptionEffect
--jsonEmit JSON — for each entry its name, its stored path, its resolved_path, its status, and its children.
--treeRecurse 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.
OptionEffect
--jsonEmit 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 --root was given. Passing --root with any value, including a named one, states the target outright and suppresses default_root re-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:

  1. Lock the participating roots and check pending recovery, then fetch and verify the packages for all participants before extracting any.
  2. 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.
  3. 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.

OptionEffect
--no-recurseConfine 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 #

CodeMeaning
0Success.
1Failure — 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.
2Usage 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 #

  1. 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.

  2. Choose an output directory that does not exist. Compose refuses an existing tree; do not point it at a live installation.

  3. Resolve and record the package set, then build from that lock:

    peipkg-compose lock image.toml
    peipkg-compose build image.toml --out ./root --locked
    
  4. 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.

  5. Inspect the successful output's package records and files, for example with peipkg --root ./root list and peipkg --root ./root verify where 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 --locked to require an existing matching lock and do no metadata resolution. Package bytes must still be available.
  • Use --update to resolve again and overwrite the lock before building.
  • With neither flag, a missing lock is created; an existing lock must match the manifest. --locked and --update cannot be combined.
  • --out is 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 .repo files 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 #

CodeMeaning
0Success, or help output (-h, --help, help)
1Resolution, verification, fetch, lock or filesystem failure
2Usage 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 wantTool
Install or remove the files that supply a featurepeipkg or Package Manager
Set up a feature without turning it onfeat install <name>
Set it up and turn it onfeat add <name>
Turn it off but keep its setupfeat disable <name>
Undo its setup before removing its packagefeat 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:

StateMeans
Not installedNothing it would set up is in place.
InstalledIts 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 #

  1. Read the failed script's output and inspect feat info <name> or feat list to identify the interrupted phase.
  2. Address the reported cause, such as missing authority or a resource the script needs. Updating the recorded state alone does not finish the work.
  3. Run the same command again to retry the step from the start. Feature scripts are required to tolerate that retry.
  4. 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 #

CommandDoes
feat listEvery 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 #

CodeMeans
0Done, or nothing needed doing.
1The command was wrong.
2No such feature.
3Refused: you may not change the feature's state.
4Not possible in the feature's state: turning on one that isn't installed.
5A 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:

StateMeans
Not InstalledNothing it sets up is in place.
Installed, OffWhat it sets up is in place, but not in use.
OnIt is set up and in use.
InterruptedOne of its scripts didn't finish. The card says which, and what each way on does.
Definition MissingIts 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 #

ButtonDoesThe command
Turn OnOn a feature that isn't installed, installs it and turns it on. On one that is, turns it on.feat add, feat enable
Install OnlyInstalls it without turning it on.feat install
Turn OffTurns 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 AgainOn 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 ls Machine/System/KMES -l
reg get Machine/System/KMES BufferCapacity -L
regman 'Machine\System\KMES' BufferCapacity
  • reg ls shows the values and their types.
  • reg get -L shows the effective value, its winning layer and sequence. It does not list all the entries hidden by that winner.
  • regman explains 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 #

  1. Read the setting's regman entry. Check Type, Valid and Applies, including any restart or reboot requirement.
  2. Record the old value and winning layer. For recovery that must retain permissions and layer entries, take a backup.
  3. 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.
  4. Change the value with its explicit type, then read it back.
  5. 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 #

PartOperator meaning
HiveA top-level namespace. Machine holds system-wide configuration; Users holds users' keys.
KeyA container with its own permissions, such as Machine\System\KMES.
ValueA 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 #

SymptomCheck next
The write fails with access deniedKey rights and layer rights; parent access is not enough.
The old value still winsLayer precedence and write order.
The new value is stored but behavior is unchangedConsumer validation and application timing.
A deleted value reappearsDeletion removes one layer's entry.
A write times out or the source failsRead state back before retrying.
A change needs to be undoneLayer recovery or backup and restore.

Warning

Removing a layer does not undo changes to a key's owner or permissions. Restore can replace those permissions throughout its target subtree. Plan security recovery separately from reverting a value.

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:

reg get Machine/System/KMES BufferCapacity
reg ls Machine/System/KMES -l

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:

TypeMeaning to a readerreg set prefix
REG_DWORD32-bit integerdword:
REG_QWORD64-bit integerqword:
REG_DWORD_BIG_ENDIANBig-endian 32-bit integerdword-be:
REG_SZTextsz:
REG_EXPAND_SZText whose references the consumer expandsexpand:
REG_MULTI_SZList of stringsmulti:
REG_BINARYByteshex: or bin:
REG_LINKA link target on a link keylink:
REG_NONENo typenone:

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:

reg get 'Machine\System\KMES' BufferCapacity

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 #

  • Make and verify a change
  • Understand which layer wins
  • Check key permissions

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:

regman -k buffer
regman 'Machine\System\KMES' BufferCapacity
reg ls Machine/System/KMES -l
reg get Machine/System/KMES BufferCapacity -L
  1. Search installed documentation with regman -k, using a word from the setting's name or purpose.
  2. Look up the returned key and value; check type, valid values, default and application timing. A key-only lookup lists its documented values.
  3. Inspect live values with reg. -L shows 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 forStart hereWho gives it meaning
A service definitionMachine\System\Services\<name>peinit reads service definitions. See the service key reference.
Init-wide settingsMachine\System\Initpeinit reads global control limits, environment and boot-time path provisioning.
Network configuration and reported stateMachine\System\Networknetd, resolvd and policy consumers use distinct subtrees. See the network registry layout for details.
Shared authentication policyMachine\Generic\Authn\PolicyPeios policy about a principal's authority, shared across authentication-authority implementations.
Shared security-descriptor suggestionsMachine\Common\SecurityDescriptorBuilderA claim catalogue for programs that build security descriptors. Common holds stable shared formats that are not part of the Peios specifications.
authd's implementation configurationMachine\Software\Authdauthd's Sources subkeys say which principal sources may register and what they are trusted to assert.
A particular user's settingsUsers\<SID>; CurrentUser for the callerEach 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 Status and readiness facts. Its Status keys 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.

regman 'Machine\System\KMES' BufferCapacity

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 #

reg ls Machine/System/KMES -l
reg get Machine/System/KMES BufferCapacity -L

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:

reg set Machine/System/KMES BufferCapacity qword:8388608 --layer base
reg get Machine/System/KMES BufferCapacity -L

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 #

  1. Confirm the effective value and type with reg or Registry Editor.
  2. Follow Applies: allow the live consumer to process the change, or arrange its documented restart or reboot if required.
  3. Inspect that component's status and relevant events. A watch notification or a successful reg set is not an acceptance acknowledgement.
  4. 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 #

reg watch Machine/System/KMES --subtree --filter value

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:

reg get Machine/System/KMES
reg get Machine/System/KMES BufferCapacity -L

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:

reg get Machine/System/KMES BufferCapacity -L
reg layer ls -l

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:

  1. Highest precedence wins.
  2. 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 #

  1. Run reg layer ls -l and inspect important values with reg get KEY NAME -L.

  2. 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.

  3. If appropriate, disable the layer to stop its entries participating in ordinary resolution while retaining them:

    reg layer set NAME --disable
    
  4. Re-read the affected keys. The next winning entries may become visible, or values and keys may become absent. Verify the consuming services again.

  5. Re-enable with reg layer set NAME --enable if that is the intended recovery. Delete with reg layer del NAME only 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 #

Warning

A key's security descriptor is not layered. Removing a layer does not undo changes to the owner, DACL or SACL of a surviving key. Save and review permissions separately when they are part of a change.

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 #

  • Remove one value or key entry
  • Restore a subtree from a backup
  • Understand security changes that outlive layers

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 #

IntentOperationResult
Withdraw one layer's valuereg del KEY NAME --layer LAYERRemoves that layer's entry; another may surface.
Keep a value absent through a layerreg mask KEY NAME --layer LAYERAdds a tombstone that competes with other entries.
Withdraw one layer's key namereg del KEY --layer LAYERRemoves that layer's claim; another may remain.
Mask a key name through a layerreg hide KEY --layer LAYERAdds a hidden-key entry.
Withdraw a whole configuration layerreg layer del LAYERRemoves 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 #

  1. Read reg get KEY NAME -L and confirm the destination layer.
  2. Remove only the intended layer's entry.
  3. Read the value again. It may be absent or replaced by another winner.
  4. 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 LAYER walks 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 #

  • Recover using a layer
  • Key and layer access checks
  • Verify the consumer after a change

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 #

reg sd Machine/System/KMES

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 operationRequired right on the key
Read valuesKEY_QUERY_VALUE
Write or delete valuesKEY_SET_VALUE
Create a child keyKEY_CREATE_SUB_KEY on the parent
List child keysKEY_ENUMERATE_SUB_KEYS
Watch changesKEY_NOTIFY
Create a linkKEY_CREATE_SUB_KEY and KEY_CREATE_LINK on the parent, plus SeTcbPrivilege or Administrator membership
Delete or hide the keyDELETE
Read its descriptor and metadataREAD_CONTROL
Change its DACL or ownerWRITE_DAC or WRITE_OWNER, respectively
Read or change its SACLACCESS_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.

Warning

Do not create Machine\System\Registry\Layers\base just to repair a denied write. Creating it replaces the fallback with that key's actual descriptor. Ordinary inheritance from Machine grants users read access, which can prevent their writes even under their own user keys. Review the base-layer authorization rules before changing this key.

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 #

Warning

A key's owner and permissions are direct properties of the key object. Removing or disabling a layer does not restore an earlier descriptor on a surviving key. Plan and verify permission recovery separately.

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 #

  • Security descriptors and inheritance
  • Default descriptors for newly created objects
  • Privileged backup and restore
  • LCS access flow for the kernel details

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 stateDescriptor the convention selects
AbsentThe component's compiled-in default. The key need not exist.
Present and valid SDDLThe registry override.
InvalidThe 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 #

  1. Read the value's manual and understand when its component stamps the object.
  2. Save the old value and the relevant recovery state.
  3. Make the intended SDDL change with the correct type and destination layer.
  4. Let the component create or stamp an object through its documented workflow.
  5. 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; authd obtains that from the principal's DefaultDacl policy 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 #

  • SDDL and security descriptors
  • Inheritance
  • Make and verify registry changes

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:

reg get Machine/System/Registry
regman 'Machine\System\Registry'

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.

Warning

Deleting a registry tuning value is not a reliable way to reset its running value to the compiled default. Read the parameter documentation, write the intended valid value if a reset is needed, and verify the result.

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 #

  • Source availability and recovery checks
  • Backup and restore, including destructive replacement
  • Kernel boot sequence and self-watch implementation

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 #

NeedToolImportant limit
Review or transfer visible keys and valuesreg export --json KEY FILE and reg apply FILEExport does not save key security descriptors or the complete layered store.
Recover a subtree with its descriptors and layer entriesreg backup KEY FILE and reg restore KEY FILEPrivileged; restore replaces the target contents and descendants.
Recover a managed configuration bundleThe role or policy workflow, or a reviewed layer operationLayer 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:

reg backup Machine/System/KMES kmes.snap

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.

Warning

A backup's layer manifest records layer information for validation; it does not recreate layer definitions. A layer definition is included only if its metadata key under Machine\System\Registry\Layers\<Name> is part of the backed-up subtree.

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:

  1. Confirm the backup is trusted and corresponds to the intended target.
  2. Save the current target if you need a way back from the restore itself.
  3. Review the target subtree, affected consumers and restored permissions.
  4. Confirm needed layer metadata exists or is included, and arrange any required privileges or consumer restart/reload.
  5. Run restore only after approving the replacement scope.

For the earlier example:

reg restore Machine/System/KMES kmes.snap

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.

Warning

Restore writes the backed-up security descriptors, including the target key's descriptor. Its privilege effectively permits rewriting owners and permissions throughout the target subtree.

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 -L and 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 #

  • Change a setting and verify it
  • Recover using a layer
  • LCS backup and restore implementation

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 questionStart 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:

  1. Save the exact failed operation, key and error. reg exit status 3 means access denied; 5 includes source and syscall failures. Avoid treating every failure as a permissions problem.
  2. 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.
  3. Establish why the source failed before restarting it or replacing data. When reads work again, re-read the affected keys and verify consumers.
  4. 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 startup and self-configuration
  • LCS source registration and dispatch
  • Registry Source Interface specification
  • SDK guide for source authors

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 #

  1. Browse to the exact key and inspect its current values. Select a value to see its winning layer and the registry manual beside it.
  2. Check the manual's expected type, valid values and application timing. A saved value can still be rejected by the component that reads it.
  3. Check Writes go to in the bar. Your change goes to that layer, which may differ from the layer currently winning the value.
  4. 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.
  5. 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 PATH opens on the key at PATH, such as gxwi-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 saysTypeShown as
TextREG_SZthe text
Text with variablesREG_EXPAND_SZthe text, with any %NAME% left as written
LinkREG_LINKthe path the link points to
List of textREG_MULTI_SZthe items, separated by ·
NumberREG_DWORDthe number, then in hexadecimal
Number, big-endianREG_DWORD_BIG_ENDIANthe number, then in hexadecimal
Large numberREG_QWORDthe number, then in hexadecimal
BytesREG_BINARYthe bytes, in hexadecimal
No typeREG_NONEnothing

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 #

Warning

Changing a key's permissions is not a layered edit. Disabling or deleting the selected layer does not restore the key's old owner or permissions. Existing open handles keep their granted rights until they are reopened.

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 apply reads 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:

FieldTells you
TypeThe value's registry type (REG_QWORD, REG_SZ, …).
DefaultThe value used when nothing is set.
ValidThe range, set, or constraint a sensible value must satisfy.
AppliesWhen 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 validregman — the manual
What the value is set toreg — a live query against the store
What the subsystem is actually running onthe 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 nowreg
Look up what a value means — its type, default, valid range, when it appliesregman

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:

PathMeaning
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 .reg convention: 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:

PrefixRegistry typeData syntax
sz:REG_SZUTF-8 string (rest of token, verbatim).
expand:REG_EXPAND_SZUTF-8 string, %VAR% left unexpanded on disk.
dword:REG_DWORD42 or 0x2A; must fit u32.
dword-be:REG_DWORD_BIG_ENDIAN42 or 0x2A; must fit u32.
qword:REG_QWORD42 or 0x2A; must fit u64.
multi:REG_MULTI_SZComma-separated; \, escapes a literal comma.
hex: / bin:REG_BINARYHex bytes; :, -, and space separators are ignored.
link:REG_LINKAn absolute key path (a symlink target).
none:REG_NONENo 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 shapeInferred type
all decimal digits, fits u32REG_DWORD
all decimal digits, fits u64 but not u32REG_QWORD
0x… hex, ≤ 8 significant hex digitsREG_DWORD
0x… hex, ≤ 16 significant hex digitsREG_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.

OptionEnv varEffect
--jsonREG_JSON=1Emit structured JSON instead of human text. watch emits JSON-lines.
-v, --verboseREG_VERBOSE=1More detailed output.
-q, --quiet—Suppress non-essential output. (A surprising type coercion is still reported.)
--sep=CHARREG_SEPPath display separator: \ (default) or /.
--help—Print help for the command and exit 0.
--version—Print the version and exit 0.

Mutating subcommands additionally accept:

OptionEnv varEffect
--layer NAMEREG_LAYERTarget layer for the write (default: the base layer).
-y, --yesREG_ASSUME_YES=1Skip 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.

OptionEffect
-L, --layersAnnotate the value with the layer it resolved from and its sequence number.
--rawWrite the value's raw bytes to standard output verbatim — for piping REG_BINARY data.
--no-followOperate 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.

OptionEffect
-l, --longLong form: for subkeys, child and value counts; for values, type and byte size.
--keys-onlyList subkeys only.
--values-onlyList 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.

OptionEffect
--depth NLimit recursion to N levels.
--valuesInclude 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.

OptionEffect
--no-followInspect 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.

OptionEffect
--layer NAMEWrite into layer NAME instead of the base layer.
-p, --parentsCreate any missing ancestor keys.
--expected-seq NCompare-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.

OptionEffect
--layer NAMECreate the key in layer NAME.
-p, --parentsCreate any missing ancestor keys.
--volatileCreate 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.

OptionEffect
--layer NAMEDelete from layer NAME.
-r, --recursiveDelete 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, --yesSkip 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.

OptionEffect
--layer NAMEThe layer that carries the tombstone.
--allBlanket 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.

OptionEffect
--layer NAMEThe 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.

OptionEffect
-l, --longLong 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.

OptionEffect
--precedence NPrecedence (higher wins). A precedence above 0 requires SeTcbPrivilege; the tool attempts it and reports any denial.
--owner SIDRecord an informational owner SID, stored as a binary SID. Without it, the owner is whoever creates the layer.
--disabledCreate 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.

OptionEffect
--precedence NChange the precedence.
--enable / --disableEnable or disable the layer.
--owner SIDChange 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).

OptionEffect
--set SDDLApply the given SDDL. Without it, sd prints the current descriptor.
--ownerScope to the owner.
--groupScope to the group.
--daclScope to the DACL.
--saclScope 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.

OptionEffect
--layer NAMECreate 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.

OptionEffect
--subtreeWatch descendant keys too, not just this key.
--filter LISTComma-separated subset of value, subkey, sd (default: all).
--count NExit 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.

OptionEffect
--dir DIRApply every batch file in DIR (sorted), each as its own transaction. A missing or empty directory applies nothing and succeeds.
--once-deleteDelete each batch file after it applies successfully (a drain).
-y, --yesSkip 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.

{"keys": [
  {"path": "Machine\\Generic"},
  {"path": "Machine\\Generic\\Events", "descriptor": "S:(AL;CI;0x10002;;;WD)"}
]}

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.

OptionEffect
--layer NAMEExport 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 typeHuman formJSON form
REG_SZ / REG_EXPAND_SZthe string{"type":"sz","data":"…"}
REG_DWORD / REG_QWORDdecimal{"type":"dword","data":42}
REG_MULTI_SZone element per line{"type":"multi","data":["a","b"]}
REG_BINARYhex{"type":"binary","data":"deadbeef"}
REG_LINKthe 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 #

CodeMeaningTypical cause
0Success.—
1Usage error.Bad arguments; a required option missing.
2Key or value not found.ENOENT.
3Access denied.EACCES, EPERM — the kernel refused the operation.
4Invalid specification.Bad path, type, or literal; a cross-hive batch; a name too long (EINVAL, EXDEV, ENAMETOOLONG).
5Syscall or source failure.EIO, ETIMEDOUT, ENOSPC, ENOMEM.
6Compare-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 you reg set.
  • Keys, values, and types — the data model reg addresses.
  • Layers — what "effective" resolves against, and what --layer, mask, and hide operate on.
  • Access control on keys — the check every reg operation 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 #

  • Effective values and layers
  • Key and layer permissions
  • Private-layer attachment and limits

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:

reg apply reviewed.json

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 DIR applies 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 apply errors 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 loregd supports 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 #

Warning

A timeout after commit dispatch means the operation may or may not have committed. Read the affected state and inspect consumer events before retrying. A timed-out transaction status is not proof of rollback.

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 #

  • reg batch formats and exit statuses
  • Restore a saved subtree
  • LCS transaction scope and lifetime
  • Isolation and conditional writes

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:

reg info KEY --no-follow
reg get KEY @ --no-follow

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 #

CommandPurpose
cpCopy files and directories.
mvMove or rename files and directories.
rmRemove files, and with -r, directories.
rmdirRemove directories, but only empty ones.
mkdirCreate directories.
mkfifoCreate named pipes (FIFOs).
mknodCreate special files — device nodes and FIFOs.
touchCreate empty files, or update a file's timestamps.
lnCreate hard links and symbolic links.
link and unlinkThe single-purpose, low-level versions of "make a hard link" and "remove a file".
shredDestroy a file's contents by overwriting them, so they cannot be recovered.
ddCopy data block by block, converting it on the way.
dfReport how much space each file system has, and how much is free.
duReport how much space files and directories use.
truncateShrink or extend a file to an exact size.
mktempCreate a temporary file or directory with a safely unique name.
sdInspect selected file security components and change file security descriptors.
mkexecMark 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:

OptionEffect
-r, -R, --recursiveCopy directories, and everything inside them, recursively.
-a, --archiveCopy 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:

AttributeWhat it carries from the source
ownerThe owner SID.
daclThe whole DACL — every access rule on the file, inherited rules included.
saclThe whole SACL — the audit rules and the integrity label.
daclniThe DACL with inherited rules stripped out — only the rules set explicitly on the source.
saclniThe SACL with inherited rules stripped out.
timestampsThe access and modification times, not creation or change time.
linksHard-link relationships among source entries selected in this command. Links to names outside that set are not recreated.
execExecutable-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.
securityExtended attributes whose names begin with security., except security.peios.sd, which is handled through the descriptor components above.
xattrsExtended attributes outside the security.* namespace. xattr is an alias.

You rarely name those individually. These options select sensible sets:

OptionPreserves
-ptimestamps and exec, both required.
--preservetimestamps only. This differs from -p.
--preserve=LISTThe comma-separated attributes you name, with no implicit attributes. Other preservation options can add or remove attributes.
--preserve-allEvery supported attribute in the table above, including exec. The full DACL/SACL selections take precedence over their inherited-rule-stripping variants.
--sdowner, dacl, sacl, including inherited ACL entries. The descriptor's group component is not selected.
--sd-explicitowner, daclni, saclni — the owner and the source's explicitly set ACL rules. The group component is not selected.
--no-preserve=LISTTurns 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.

  • --sd passes the owner and the whole source DACL and SACL, including inherited entries, to the destination's descriptor-setting operation.
  • --sd-explicit carries 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.

OptionEffect
-i, --interactiveAsk before overwriting an existing file.
-n, --no-clobberNever 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, --forceIf an existing destination cannot be opened for writing, remove it and try the copy again.
--remove-destinationRemove 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.

OptionEffect
-L, --dereferenceAlways follow symbolic links — copy the file they point to.
-P, --no-dereferenceNever follow symbolic links — copy the link itself.
-HFollow only the symbolic links named directly on the command line, not links found while recursing.
-dCopy symbolic links as links, and preserve hard-link structure.

Other ways to copy #

cp can also produce something other than a plain duplicate.

OptionEffect
-l, --linkCreate a hard link to the source instead of copying its data.
-s, --symbolic-linkCreate 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-onlyCreate the destination and copy its attributes, but not its data.

Other options #

OptionEffect
-t, --target-directory=DIRCopy every source into DIR. Useful when the directory is not the last argument.
-T, --no-target-directoryTreat dest as a plain file even if it is a directory.
--parentsRecreate the source's leading directories under the destination.
-x, --one-file-systemDo not cross into a different file system while recursing.
--strip-trailing-slashesStrip any trailing slashes from each source argument before using it.
-v, --verbosePrint each file as it is copied.
--debugExplain in detail how each file was copied. Implies -v.
--progressShow a progress bar.

Exit status #

CodeMeaning
0No 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.
1A 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.

OptionEffect
-i, --interactiveAsk before overwriting an existing file.
-f, --forceDo not prompt before overwriting.
-n, --no-clobberNever 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 #

OptionEffect
-t, --target-directory=DIRMove every source into DIR.
-T, --no-target-directoryTreat dest as a plain file even if it is a directory.
--strip-trailing-slashesStrip any trailing slashes from each source argument.
-v, --verbosePrint each file as it is moved.
--debugExplain in detail how each file was moved. Implies -v.
--progressShow a progress bar.

Exit status #

CodeMeaning
0No error was reported. This includes successful skips; it does not mean every source was moved.
1A 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:

OptionEffect
-r, -R, --recursiveRemove directories and all of their contents, recursively.
-d, --dirRemove 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 #

OptionEffect
-f, --forceNever prompt. Also ignore files that do not exist instead of reporting them.
-iPrompt before every removal.
-IPrompt 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.

OptionEffect
--preserve-rootRefuse to recurse on /. This is the default; you do not need to ask for it.
--no-preserve-rootDisable 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 #

OptionEffect
-v, --verbosePrint each file as it is removed.
--progressShow 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 #

CodeMeaning
0Every requested removal succeeded (or, with -f, the file did not exist).
1A 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 #

OptionEffect
-p, --parentsRemove 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-emptyDo 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, --verbosePrint a line for each directory as it is removed.

Exit status #

CodeMeaning
0Every directory was removed.
1A 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:

OptionEffect
-p, --parentsCreate any missing parent directories along the way. Also makes mkdir succeed quietly if the directory already exists.
-v, --verbosePrint 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.

FlagArgumentEffect
--ownerSIDMake this principal the owner, instead of you.
--groupSIDSet the object's group.
--labellevelGive the object a mandatory integrity label.
--no-inherit—Do not inherit rules from the parent directory; lock the object to its owner.
--sddlSDDL stringSupply 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 #

CodeMeaning
0Every directory was created.
1A 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 #

CodeMeaning
0Every FIFO was created.
1A 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:

TypeCreates
bA block device — a device addressed in fixed-size blocks, such as a disk.
c or uA character device — a device addressed as a stream of bytes, such as a terminal.
pA 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 #

CodeMeaning
0The special file was created.
1It 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, touch creates it, empty;
  • if the file does exist, touch updates 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.

OptionEffect
-aChange only the access time.
-mChange only the modification time.
--time=WORDChoose which time to change by name: access, atime, or use for the access time; modify or mtime for the modification time.
-d, --date=STRINGUse the time described by STRING instead of the current time.
-t STAMPUse the time given as [[CC]YY]MMDDhhmm[.ss] instead of the current time.
-r, --reference=FILEUse the timestamps of FILE instead of the current time.
-h, --no-dereferenceIf 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:

OptionEffect
-c, --no-createDo 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 #

CodeMeaning
0Every file was created or updated as requested.
1A 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 named link_name.
  • ln target — create a link to target in the current directory, with the same final name.
  • ln target... directory — create a link to each target inside directory.
  • ln -t directory target... — the same, with the directory named first.

Options #

OptionEffect
-s, --symbolicMake symbolic links instead of hard links.
-f, --forceRemove an existing destination file so the link can be created.
-i, --interactiveAsk before removing an existing destination.
-r, --relativeFor a symbolic link, write the target as a path relative to the link's own location, rather than an absolute path.
-n, --no-dereferenceIf 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, --logicalWhen the target is a symbolic link, link to what it points to.
-P, --physicalMake a hard link to a symbolic link itself, rather than to its target.
-t, --target-directory=DIRCreate all links inside DIR.
-T, --no-target-directoryTreat 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=SUFFIXUse SUFFIX for backup file names.
-v, --verbosePrint 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 #

CodeMeaning
0Every link was created.
1A 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:

CodeMeaning
0The operation succeeded.
1The 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 #

OptionEffect
-n, --iterations=NOverwrite 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, --zeroAdd a final pass that writes zeros, so the file does not visibly look shredded afterwards.
-s, --size=NShred N bytes, rather than the whole file. Accepts size suffixes such as K, M, G.
-x, --exactDo 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=FILETake the random bytes for the overwrite passes from FILE.
-v, --verboseShow progress as each pass runs.
-f, --forceOverride 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 #

CodeMeaning
0Every file was shredded successfully.
1A 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 #

OperandMeaning
if=FILERead input from FILE.
of=FILEWrite output to FILE.
bs=BYTESRead and write BYTES at a time. Sets both the input and output block size at once.
ibs=BYTESInput block size (default 512).
obs=BYTESOutput block size (default 512).
cbs=BYTESConversion block size — used by the block and unblock conversions.
count=NStop after N input blocks, rather than reading to the end.
skip=NSkip N input blocks before copying. Also spelled iseek=N.
seek=NSkip N output blocks before writing. Also spelled oseek=N.
conv=LISTApply the comma-separated conversions below.
iflag=LISTComma-separated input flags — how the input is opened and read.
oflag=LISTComma-separated output flags — how the output is opened and written.
status=LEVELHow 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=) #

ValueEffect
ucase / lcaseConvert the data to upper-case / lower-case.
swabSwap every adjacent pair of bytes.
syncPad each input block out to its full size — with zeros, or with spaces alongside block/unblock.
block / unblockConvert between newline-terminated lines and fixed-size cbs records.
sparseWhere an output block is all zeros, seek past it instead of writing it.
exclFail if the output file already exists.
nocreatFail if the output file does not already exist.
notruncDo not truncate the output file when opening it.
noerrorContinue past read errors instead of stopping.
fdatasync / fsyncFlush 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=) #

FlagApplies toEffect
count_bytesinputInterpret count=N as a number of bytes, not blocks.
skip_bytesinputInterpret skip=N as a number of bytes.
fullblockinputWait for a full ibs of data on each read.
seek_bytesoutputInterpret seek=N as a number of bytes.
appendoutputOpen the output in append mode.
directbothUse direct I/O, bypassing the cache.
dsync / syncbothUse synchronised I/O — for data, or for data and metadata.
nonblockbothUse non-blocking I/O.
noatimebothDo not update the file's access time.
nocachebothAsk the system to drop the file's cached pages.
directorybothFail 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 #

CodeMeaning
0The copy completed.
1The 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.

OptionEffect
-h, --human-readableShow sizes with unit suffixes — 456G, 12M — using powers of 1024.
-H, --siLikewise, but using powers of 1000.
-B, --block-size=SIZEShow sizes scaled to SIZE — -BM reports in megabytes.
-kUse 1024-byte blocks.

Choosing what to report #

OptionEffect
-a, --allInclude dummy and otherwise-hidden file systems that df would normally skip.
-l, --localReport only local file systems, skipping network-mounted ones.
-t, --type=TYPEReport only file systems of type TYPE.
-x, --exclude-type=TYPEReport everything except file systems of type TYPE.
--totalAdd a final line giving the grand total across everything reported.

Changing the output #

OptionEffect
-i, --inodesReport 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-typeAdd 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, --portabilityUse a fixed, simple column layout that does not wrap a long device name onto its own line.

Sync behaviour #

OptionEffect
--syncFlush pending writes before reading the usage figures, for the most up-to-date numbers.
--no-syncDo not flush first. This is the default and is faster.

Exit status #

CodeMeaning
0The report was produced.
1A 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 #

OptionEffect
-a, --allList every file, not only directories.
-s, --summarizePrint only a single total for each argument — no per-subdirectory breakdown.
-d, --max-depth=NShow totals only down to N levels below each argument. -d 0 is the same as -s.
-S, --separate-dirsGive each directory's own size, excluding its subdirectories.
-c, --totalAdd a final grand-total line.
--inodesCount inodes used rather than space used.

What "size" means #

OptionEffect
-h, --human-readableSizes with unit suffixes — 4.2G — using powers of 1024.
--siLikewise, using powers of 1000.
-B, --block-size=SIZEScale sizes to SIZE.
-k / -mUse 1024-byte / 1024×1024-byte units.
--apparent-sizeReport 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, --bytesReport plain byte counts. Equivalent to --apparent-size --block-size=1.

Following symbolic links #

By default du does not follow symbolic links.

OptionEffect
-L, --dereferenceFollow all symbolic links and count what they point to.
-D, -H, --dereference-argsFollow only the symbolic links named directly on the command line.
-P, --no-dereferenceFollow no symbolic links. This is the default.
-x, --one-file-systemDo not cross into a different file system while walking.
-l, --count-linksCount a hard-linked file every time it is encountered, instead of once.

Limiting the output #

OptionEffect
--exclude=PATTERNSkip files and directories whose name matches PATTERN.
--exclude-from=FILESkip everything matching any pattern listed in FILE.
-t, --threshold=SIZEShow only entries at least SIZE (or, with a negative SIZE, at most that big).
--files0-from=FTake 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, --nullEnd each output line with a NUL character instead of a newline.
-v, --verboseReport extra detail about what is being processed.

Exit status #

CodeMeaning
0The report was produced.
1A 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:

PrefixMeaning
+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 #

OptionEffect
-s, --size=SIZESet or adjust the size to SIZE, as described above.
-r, --reference=RFILEBase 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-createDo not create a file that does not exist; skip it instead.

Exit status #

CodeMeaning
0Every file was resized.
1A 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 #

OptionEffect
-d, --directoryCreate 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=SUFFIXAppend SUFFIX after the random part of the name — for giving the file an extension.
-u, --dry-runDo 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, --quietPrint no error message if creation fails; rely on the exit status.

Exit status #

CodeMeaning
0The temporary file or directory was created; its name was printed.
1Creation 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 #

GroupSubcommandDoes
InspectshowShow owner, group, DACL and the integrity-label view on a path.
checkSimulate an access check against the path.
DACLallowAdd an allow rule for one or more principals.
denyAdd a deny rule for one or more principals.
removeDrop every DACL rule for the named principals.
AuditingauditAdd an audit rule to the SACL.
unauditDrop every SACL rule for the named principals.
OwnershipownerSet the descriptor's owner.
groupSet the descriptor's group.
IntegrityintegritySet the mandatory integrity label.
InheritanceinheritTurn inheritance protection on or off.
resetDrop the file's own rules and re-inherit from the parent.
propagatePush inheritance down to descendants.
WholesalesetWrite the supplied descriptor components; kernel merge and validation rules apply.

Naming a principal #

Wherever a subcommand takes a PRINCIPAL, it accepts any of:

FormExampleMeaning
@self@selfThe user SID of the token running sd.
@owner@ownerA placeholder that the access check substitutes with the file's own owner.
A well-known labelEveryone, Administrators, LocalSystemA named built-in principal.
A two-letter aliasWD, BA, SYThe short alias for a well-known principal.
A raw SIDS-1-5-32-544Any 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:

FormExampleMeaning
Single lettersrwx, r, mr read, w write, x execute, d delete, m modify, f full, c change-permissions, o take-ownership.
Wordsread,write, modifyThe same set, spelled out.
Fine-grained namesread-data,append,traverseIndividual low-level rights, for precise rules.
Raw hex0x1F01FFAn 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
FlagEffect
--sddlRender the queried components as SDDL.
--rawRender SIDs in raw S-1-… form only; this is not raw descriptor export.
--labelRender SIDs as their labels where known.
--allAdd the queried byte length and, in human output, the raw control value.
--jsonEmit 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 / flagEffect
PERMSThe access to test for.
--pid PIDCheck against process PID's token instead of your own.
--explainShow 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
FlagEffect
--flags LISTACE inheritance flags — CI container-inherit, OI object-inherit, NP no-propagate, IO inherit-only; none clears them.
--if EXPRMake it a conditional rule, applied only when EXPR is true.
--replaceDrop any existing rules for this principal and kind first, instead of appending.
--recursive, -rApply 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
FlagEffect
--allow-emptyPermit the result to be a present-but-empty DACL, which denies everyone. Without this, sd refuses to produce one.
--recursive, -rApply 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.

FlagEffect
--policy BITSThe label's policy bits, comma-separated: NW no write-up, NR no read-up, NX no execute-up.
--recursive, -rApply 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.

FlagEffect
--strip-inheritedWhen turning protection off, also drop the inherited rules already on the file.
--recursive, -rApply 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.

FlagEffect
--saclPush the audit rules (SACL) down as well. Needs SeSecurityPrivilege.
--jsonReport 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 / flagEffect
SDDLThe new descriptor as an SDDL string. - reads it from standard input.
--binary FILEInstead of SDDL, read the raw descriptor bytes from FILE (- for standard input).
--components LISTOverride which parts of the descriptor (owner, group, DACL, SACL) the operation writes.

Common flags #

These apply across the subcommands:

FlagEffect
--recursive, -rApply the change to every descendant of the path. Implies --no-follow-symlinks.
--no-follow-symlinks, -POperate on a symbolic link itself, not the file it points to.
--jsonEmit 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 #

CodeMeaning
0The operation succeeded — or, for sd check, the access would be allowed.
non-zeroThe 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 #

CodeMeaning
0Every file was marked.
1A 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 #

CommandPurpose
lsList the contents of a directory. The everyday command for "what's in here?".
dir and vdirls with a fixed output style. dir lists in columns; vdir lists in long format.
statShow the detailed status of one file — its type, size, timestamps, and raw inode metadata.
pwdPrint the directory you are currently working in.
readlinkPrint where a symbolic link points.
realpathResolve a path to its canonical, absolute form — every symlink followed, every . and .. removed.
basenameStrip the directory part off a path, leaving just the final name.
dirnameThe opposite of basename — strip the final name, leaving the directory.
pathchkCheck whether a path name is valid and portable before you try to create it.
dircolorsProduce 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 -l surfaces 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.
  • stat is 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. stat reports 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.txt is an existing ordinary file.
  • latest is a symbolic link whose stored target is notes.txt.
  • report.txt does not exist yet.
  • planned and its child directory drafts do 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. ls leaves hidden entries out unless you ask for them with -a or -A.
  • Entries are sorted by name. Alphabetical order, case-sensitive. Other sort orders are available.
  • The layout adapts to where output goes. When ls is 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:

CharacterType
-Regular file
dDirectory
lSymbolic link
pNamed pipe (FIFO)
sSocket
cCharacter device
bBlock 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.

OptionEffect
-a, --allShow hidden entries too, including . (the directory itself) and .. (its parent).
-A, --almost-allShow hidden entries, but leave out . and ...
-d, --directoryList 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, --recursiveDescend into every subdirectory and list it too.
-B, --ignore-backupsSkip entries whose name ends in ~.
--ignore=PATTERNSkip entries whose name matches the shell pattern. May be given more than once.
--hide=PATTERNLike --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.

OptionSort order
-tBy modification time, newest first.
-SBy size, largest first.
-XBy file extension, alphabetically.
-vBy version: runs of digits in the name sort numerically, so f2 comes before f10.
-UNo sorting at all — entries appear in the order the directory stores them. Fast for very large directories.
-r, --reverseReverse the current sort.
--sort=WORDChoose the sort key by name: none, time, size, extension, version, or width.
--group-directories-firstList 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.

OptionLayout
-CColumns, filled top-to-bottom. The default when writing to a terminal.
-xColumns, filled left-to-right (across the rows) instead.
-1One entry per line. The default when output is not a terminal.
-mAll entries on as few lines as possible, separated by commas.
-l, --longThe long format described above.
--format=WORDChoose the layout by name: across, commas, horizontal, long, single-column, or vertical.
-w, --width=COLSAssume 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.

OptionEffect
-F, --classifyAppend a type symbol to each name: / for a directory, @ for a symlink, | for a FIFO, = for a socket, and * for an executable file.
--file-typeThe same, but without the * on executables.
-pAppend only the / on directories.
--indicator-style=WORDChoose 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.

OptionEffect
-h, --human-readablePrint sizes with a unit suffix — 4.0K, 234M, 56G — using powers of 1024.
--siLike -h, but using powers of 1000, so the suffixes are decimal.
-s, --sizePrint the disk space each entry occupies, in blocks, before its name.
-k, --kibibytesUse 1024-byte blocks for the size figures and directory totals.
--block-size=SIZEScale 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.

OptionTimestamp
-uAccess time — when the file was last read.
-cStatus-change time — when the file's metadata last changed.
--time=WORDChoose the timestamp by name: atime/access, ctime/status, mtime/modification, or birth/creation.
--time-style=STYLEChoose the date format: full-iso, long-iso, iso, locale, or +FORMAT for a custom layout.
--full-timeShorthand 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.

OptionEffect
-L, --dereferenceAlways report on the file a link points to, not the link.
-H, --dereference-command-lineDereference only the links named directly on the command line, not links found inside a listed directory.
--dereference-command-line-symlink-to-dirDereference 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.

OptionEffect
-i, --inodePrint each entry's index number (its inode number).
-Z, --contextPrint each entry's security context. Available only when the build enables it.
-b, --escapePrint non-printable characters in names using C-style backslash escapes.
-q, --hide-control-charsReplace non-printable characters in names with ?. The default when writing to a terminal.
--show-control-charsPrint names verbatim, control characters and all.
-N, --literalPrint names exactly, with no quoting.
-Q, --quote-nameWrap each name in double quotes.
--quoting-style=WORDChoose the quoting scheme: literal, shell, shell-always, shell-escape, c, escape, and others.
-T, --tabsize=COLSAssume tab stops every COLS columns when laying out output.
-fList 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, --diredEmit extra position markers designed for the Emacs dired editing mode.
--zeroEnd each line with a NUL character instead of a newline, and list one entry per line.

Exit status #

CodeMeaning
0Success — everything requested was listed.
1A minor problem — for example, a named file could not be accessed. The rest of the listing still printed.
2A 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:

CommandLayoutEquivalent ls
dirColumns, regardless of where output goes.ls -C
vdirLong 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:

CodeMeaning
0Success.
1A minor problem — a named file could not be accessed.
2A 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.

OptionEffect
-c FORMAT, --format=FORMATPrint FORMAT for each file, substituting % directives. A newline is added after each file.
--printf=FORMATLike --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):

DirectiveSubstitutes
%nFile name.
%NQuoted file name; for a symlink, with the target shown.
%FFile type in words (regular file, directory, …).
%sTotal size, in bytes.
%bNumber of allocated blocks.
%BSize in bytes of each block counted by %b.
%oPreferred I/O transfer block size.
%iInode number.
%hNumber of hard links.
%d / %DDevice number, in decimal / in hexadecimal.
%t / %TFor a device file, the major / minor device type, in hexadecimal.
%mMount point of the file system the file is on.
%fRaw mode value, in hexadecimal.
%x / %XLast access time — human-readable / seconds since the epoch.
%y / %YLast modification time — human-readable / seconds since the epoch.
%z / %ZLast status-change time — human-readable / seconds since the epoch.
%w / %WFile 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:

DirectiveSubstitutes
%nFile name.
%iFile-system ID, in hexadecimal.
%t / %TFile-system type — in hexadecimal / in words.
%lMaximum length of a file name.
%sBlock size, for fast transfers.
%SFundamental block size, used for the block counts.
%bTotal data blocks in the file system.
%fFree blocks.
%aFree blocks available to an ordinary principal.
%cTotal inodes (file nodes).
%dFree inodes.

Options #

OptionEffect
-f, --file-systemReport on the file system containing each file, instead of the file.
-L, --dereferenceFollow symbolic links — report on the link's target rather than the link itself.
-t, --tersePrint the information on a single line, as bare values with no labels. Useful for scripts.

Exit status #

CodeMeaning
0Every file was stated successfully.
1A 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.

OptionPath printed
-P, --physicalThe physical path: every symbolic link resolved to its target.
-L, --logicalThe 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 #

CodeMeaning
0The working directory was printed.
1The 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:

OptionResolvesExistence requirement
-f, --canonicalizeEvery link in the path.Every component except the last must exist.
-e, --canonicalize-existingEvery link in the path.Every component must exist.
-m, --canonicalize-missingEvery 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 #

OptionEffect
-n, --no-newlineDo not print the trailing newline. Ignored, with a warning, when more than one file is given.
-z, --zeroEnd each output line with a NUL character instead of a newline.
-q, --quiet / -s, --silentSuppress most error messages. This is the default.
-v, --verboseReport 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 #

CodeMeaning
0Every named file was resolved and printed.
1In 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:

OptionExistence requirement
-E, --canonicalize (default)Every component except the last must exist.
-e, --canonicalize-existingEvery component must exist — including the final one.
-m, --canonicalize-missingNo component need exist. The path is canonicalised purely as text where it cannot be walked.

How symbolic links are handled #

OptionEffect
-P, --physicalResolve each symbolic link as it is encountered. This is the default.
-L, --logicalResolve .. components before resolving the symbolic links they follow.
-s, --strip, --no-symlinksDo 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 #

OptionEffect
--relative-to=DIRPrint the result as a path relative to DIR instead of as an absolute path.
--relative-base=DIRPrint an absolute path, unless the result lies inside DIR, in which case print it relative to DIR.
-z, --zeroEnd each output line with a NUL character instead of a newline.
-q, --quietDo 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 #

CodeMeaning
0Every path was resolved and printed.
1A 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.

OptionEffect
-a, --multipleTreat 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=SUFFIXRemove SUFFIX from each name. Implies -a, so it applies to every name given.
-z, --zeroEnd 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 #

CodeMeaning
0Success.
1A 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, so dirname prints . — 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 #

OptionEffect
-z, --zeroEnd each output line with a NUL character instead of a newline.

Exit status #

CodeMeaning
0Success.
1A 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.

OptionAdds
-pCheck against a fixed, conservative set of length limits instead of the local file system's, and flag characters that are not broadly portable.
-PReject an empty name, and reject any component that begins with -.
--portabilityApply 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 #

CodeMeaning
0Every name passed every requested check.
1At 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.

OptionOutput syntax
-b, --sh, --bourne-shellBourne-shell-family syntax.
-c, --csh, --c-shellC-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.

OptionEffect
-p, --print-databasePrint the built-in colour database in its editable source form, instead of shell commands.
--print-ls-colorsPrint 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 #

CodeMeaning
0Success.
1A 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

CommandPurpose
catPrint one or more files straight through — and join several into one.
tacPrint a file with its lines in reverse — last line first.
moreShow a file one screen at a time, pausing between screens.
nlPrint a file with its lines numbered.
odDump a file's raw bytes in octal, hex, decimal, or character form.

Viewing part of a file

CommandPurpose
headPrint the first lines (or bytes) of a file.
tailPrint the last lines of a file — and, with -f, keep printing as the file grows.

Cutting and combining

CommandPurpose
cutPull selected columns — byte ranges or delimited fields — out of each line.
pasteJoin files side by side, line for line.
joinJoin two files on a shared field, like a database join.
commCompare two sorted files and report which lines they share.

Splitting a file into files

CommandPurpose
splitBreak a file into equal-sized pieces.
csplitBreak 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 #

OptionEffect
-n, --numberNumber every output line.
-b, --number-nonblankNumber 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.

OptionEffect
-E, --show-endsPrint a $ at the end of each line, so trailing spaces become visible.
-T, --show-tabsPrint tab characters as ^I instead of as whitespace.
-v, --show-nonprintingPrint control and other non-printing characters using ^ and M- notation (leaving newline and tab as they are).
-eShorthand for -vE.
-tShorthand for -vT.
-A, --show-allShorthand for -vET — show ends, tabs, and all non-printing characters at once.

Adjusting spacing #

OptionEffect
-s, --squeeze-blankCollapse runs of blank lines into a single blank line.

Exit status #

CodeMeaning
0Every file was printed.
1A 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.

OptionEffect
-s, --separator=STRINGUse STRING as the separator between records, instead of a newline.
-r, --regexTreat the separator as a regular expression rather than a literal string.
-b, --beforeExpect 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 #

CodeMeaning
0Every file was printed.
1A 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 #

OptionEffect
-n, --lines=NUMPrint the first NUM lines instead of 10.
-c, --bytes=NUMPrint 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:

OptionEffect
-q, --quietNever print the file-name headers.
-v, --verboseAlways 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 #

OptionEffect
-z, --zero-terminatedTreat the NUL character as the line delimiter instead of newline.

Exit status #

CodeMeaning
0Every file was printed.
1A 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 #

OptionEffect
-n, --lines=NUMPrint the last NUM lines instead of 10.
-c, --bytes=NUMPrint 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.

OptionEffect
-f, --followKeep the file open and print new data as it is added.
-FFollow 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.
--retryKeep trying to open a file that is not yet accessible.
--pid=PIDWhile following, stop once process PID exits.
-s, --sleep-interval=NWait 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.

OptionEffect
-q, --quietNever print the file-name headers.
-v, --verboseAlways print the header, even for a single file.

Other options #

OptionEffect
-z, --zero-terminatedTreat the NUL character as the line delimiter instead of newline.

Exit status #

CodeMeaning
0Success.
1A 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:

KeyAction
SpaceShow the next screenful.
ReturnShow the next line.
qQuit.
/Search forward for a string.
hShow the built-in help.

more moves forward through a file. It is the simple, always-available pager — enough for reading something through once.

Options #

OptionEffect
-n, --lines=NUMShow NUM lines per screenful instead of filling the terminal. --number=NUM is the same.
-F, --from-line=NUMBegin displaying at line NUM.
-P, --pattern=STRINGSearch for STRING and begin displaying at the first match.
-e, --exit-on-eofExit automatically at the end of the file, rather than waiting.
-s, --squeezeCollapse runs of blank lines into one.
-u, --plainSuppress underlining in the displayed text.
-p, --print-overClear the screen and print the next page, instead of scrolling.
-c, --clean-printRedraw each page in place, cleaning line ends, instead of scrolling.
-d, --silentWhen an unrecognised key is pressed, show a short hint instead of ringing the terminal bell.
-l, --logicalDo not pause when a line contains a form-feed character.
-f, --no-pauseCount logical lines rather than screen lines when deciding where to pause.

Exit status #

CodeMeaning
0The file was displayed.
1A 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 STYLENumbers…
-b aevery line.
-b tonly non-empty lines. This is the default.
-b nno lines.
-b pBREonly lines that match the basic regular expression BRE.
OptionEffect
-b, --body-numbering=STYLEThe numbering rule, as above.
-l, --join-blank-lines=NCount a run of N blank lines as one numbered line.

How the number looks #

OptionEffect
-n, --number-format=FORMATln = left-justified; rn = right-justified; rz = right-justified with leading zeros.
-w, --number-width=NUse N columns for the number.
-s, --number-separator=STRINGPut STRING between the number and the line text.
-v, --starting-line-number=NStart counting from N.
-i, --line-increment=NIncrease 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.

OptionEffect
-h, --header-numbering=STYLENumbering style for header sections.
-f, --footer-numbering=STYLENumbering style for footer sections.
-d, --section-delimiter=CCThe characters that mark a section boundary in the input.
-p, --no-renumberDo 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 #

CodeMeaning
0Success.
1A 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:

OptionA column is…
-b, --bytesa single byte.
-c, --charactersa single character.
-f, --fieldsa 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.

SequenceSelects
3column 3 only.
2,5,9columns 2, 5, and 9.
5-7columns 5 through 7.
3-column 3 to the end of the line.
-4the start of the line through column 4.
1,4-6,9any mixture of the above.
OptionEffect
--complementInvert the selection — keep every column except those named.

Field mode: the delimiter #

In field mode, cut needs to know what separates the fields.

OptionEffect
-d, --delimiter=CHARThe character that separates fields. The default is a tab.
-wSeparate fields on runs of whitespace (spaces and tabs) instead of a single character. Cannot be combined with -d.
-s, --only-delimitedPrint only lines that actually contain the delimiter; drop lines that have no fields to cut.
--output-delimiter=STRINGPut STRING between the kept fields in the output, instead of repeating the input delimiter — handy for converting one delimited format to another.

Other options #

OptionEffect
-z, --zero-terminatedTreat the NUL character as the line delimiter, so a "line" may itself contain newlines.

Exit status #

CodeMeaning
0Success.
1A 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 #

OptionEffect
-s, --serialPaste 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 #

OptionEffect
-d, --delimiters=LISTUse the characters in LIST as separators instead of a tab. When LIST has more than one character, paste cycles through them.
-z, --zero-terminatedTreat 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 #

CodeMeaning
0Success.
1A 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.

OptionEffect
-1 FIELDJoin on FIELD of file 1.
-2 FIELDJoin on FIELD of file 2.
-j FIELDJoin on FIELD of both files — shorthand for -1 FIELD -2 FIELD.
-t CHARUse 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:

OptionEffect
-a FILENUMAlso print unpaired lines from file FILENUM (1 or 2).
-v FILENUMPrint only the unpaired lines from file FILENUM, and suppress the joined output.
-e EMPTYFill in any missing field with the string EMPTY.

Shaping the output #

OptionEffect
-o FORMATBuild each output line from the field list FORMAT, rather than the default layout.
-i, --ignore-caseIgnore letter case when comparing join fields.
--headerTreat the first line of each file as column headers — print them, paired, without trying to match them.
-z, --zero-terminatedTreat the NUL character as the line delimiter.

Exit status #

CodeMeaning
0Success.
1A 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.

OptionEffect
-1Suppress column 1 — hide lines unique to file1.
-2Suppress column 2 — hide lines unique to file2.
-3Suppress 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 #

OptionEffect
--output-delimiter=STRSeparate the columns with STR instead of spaces.
--totalPrint a final summary line with the count in each column.
-z, --zero-terminatedTreat the NUL character as the line delimiter.

Exit status #

CodeMeaning
0Success.
1A 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:

OptionEach piece holds…
-l, --lines=NN lines. This is the default behaviour, with N = 1000.
-b, --bytes=SIZESIZE bytes.
-C, --line-bytes=SIZEup to SIZE bytes, but only whole lines — never a line split across two pieces.
-n, --number=CHUNKSthe 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:

FormEffect
NSplit into N pieces by size.
l/NSplit into N pieces without splitting any line across pieces.
r/NLike l/N, but distribute lines round-robin across the pieces.
K/NWrite only the Kth of N pieces, to standard output.

Naming the pieces #

OptionEffect
prefix (operand)The leading part of each output name. Default x.
-a, --suffix-length=NUse 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=SUFFIXAppend 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 #

OptionEffect
-e, --elide-empty-filesWith -n, do not write out pieces that would be empty.
-t, --separator=SEPUse SEP as the line separator instead of newline.
--verbosePrint a line as each output file is opened.

Exit status #

CodeMeaning
0The file was split.
1A 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.

PatternCut 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/-Nan 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 #

OptionEffect
-f, --prefix=PREFIXUse PREFIX instead of xx at the start of each output name.
-n, --digits=NUse N digits in the numeric part of the name instead of 2.
-b, --suffix-format=FORMATUse a custom printf-style FORMAT for the numeric suffix.

Other options #

OptionEffect
-k, --keep-filesDo not delete the output files if csplit fails partway.
-z, --elide-empty-filesDo not write out pieces that would be empty.
--suppress-matchedDrop the lines that the patterns matched, rather than keeping them in a piece.
-s, --quietDo not print the byte size of each piece.

Exit status #

CodeMeaning
0The file was split.
1A 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 -A to 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).

OptionRenders each unit as
-aNamed characters, ignoring the high-order bit (control characters shown by name, e.g. nul, sp, del).
-bOctal, one byte at a time.
-cPrintable ASCII / UTF-8 characters, with backslash escapes (\n, \t, …) for the rest.
-dUnsigned decimal, 2-byte units.
-DUnsigned decimal, 4-byte units.
-oOctal, 2-byte units.
-OOctal, 4-byte units.
-sSigned decimal, 2-byte units.
-iSigned decimal, 4-byte units.
-lSigned decimal, 8-byte units.
-I, -LSigned decimal, 8-byte units.
-x, -hHexadecimal, 2-byte units.
-X, -HHexadecimal, 4-byte units.
-fFloating point, single precision (32-bit).
-e, -FFloating 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 letterMeaning
aPrintable 7-bit ASCII, named (like -a).
cUTF-8 characters, with octal escapes for undefined bytes (like -c).
dSigned decimal.
uUnsigned decimal.
oOctal.
xHexadecimal.
fFloating 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:

SizeApplies toBytes per unit
1 / Cinteger types1
2 / Sinteger types2
4 / Iinteger types4
8 / Linteger types8
4 / Ffloating point4 (single precision)
8 / Dfloating point8 (double precision)
16 / Lfloating point16 (extended / long double)
2 / Hfloating point2 (IEEE half precision)
2 / Bfloating point2 (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 #

OptionEffect
-A RADIX, --address-radix=RADIXPrint 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:

OptionEffect
-j BYTES, --skip-bytes=BYTESSkip BYTES bytes of input before starting to dump. When several files are given, the skip runs across the concatenated stream.
-N BYTES, --read-bytes=BYTESDump 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.

OptionEffect
-v, --output-duplicatesTurn the collapsing off — print every line, including repeats, with no * markers.

Output width #

OptionEffect
-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 #

OptionEffect
-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 #

CodeMeaning
0The input was dumped successfully.
1A 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

CommandPurpose
sortSort lines — alphabetically, numerically, and many other ways.
shufThe opposite of sorting: put lines into a random order.
tsortTopological sort — order items so that each comes after the things it depends on.

Filtering lines

CommandPurpose
uniqCollapse or report adjacent repeated lines.

Substituting characters

CommandPurpose
trTranslate, squeeze, or delete individual characters.

Whitespace

CommandPurpose
expandConvert tabs into spaces.
unexpandConvert runs of spaces back into tabs.

Reflowing and paginating

CommandPurpose
fmtReflow paragraphs to a target width.
foldHard-wrap long lines at a fixed width.
prPaginate and columnate text for printing.

Indexing and counting

CommandPurpose
ptxProduce a permuted index of the words in a file.
wcCount 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:

OptionCompares lines as…
-n, --numeric-sortnumbers. 9 sorts before 10.
-g, --general-numeric-sortnumbers, including scientific notation. Slower than -n; use it only when -n is not enough.
-h, --human-numeric-sorthuman-readable sizes — 2K before 1M before 3G.
-M, --month-sortmonth names — JAN before FEB.
-V, --version-sortversion numbers — 1.2.10 after 1.2.9.
-R, --random-sorta random order (a shuffle that groups equal lines together).

And these adjust what is compared:

OptionEffect
-f, --ignore-caseTreat lower and upper case as the same.
-d, --dictionary-orderConsider only letters, digits, and blanks.
-i, --ignore-nonprintingIgnore non-printing characters.
-b, --ignore-leading-blanksIgnore blanks at the start of a line (or field).

Sort order options #

OptionEffect
-r, --reverseReverse the result.
-u, --uniqueOutput only the first line of each run of equal lines — sort and de-duplicate in one step.
-s, --stableKeep 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
OptionEffect
-k, --key=KEYDEFSort on the key KEYDEF. May be given more than once; keys are tried in order.
-t, --field-separator=SEPUse SEP to split fields, instead of the whitespace default.

Checking and merging #

OptionEffect
-c, --checkDo not sort — just check whether the input is already sorted, and report the first line that is out of order.
-C, --check=silentCheck, but report only through the exit status.
-m, --mergeTreat the inputs as already-sorted files and merge them, which is faster than a full sort.

Output and resources #

OptionEffect
-o, --output=FILEWrite to FILE instead of standard output. FILE may be one of the inputs.
-z, --zero-terminatedTreat the NUL character as the line delimiter.
--parallel=NUse N threads.
-S, --buffer-size=SIZESet the size of the in-memory sort buffer.
-T, --temporary-directory=DIRPut temporary files in DIR rather than the default location.
--files0-from=FTake the list of input files from F, NUL-separated.
--debugUnderline the part of each line that was actually used as the sort key — invaluable when a -k key is not behaving.

Exit status #

CodeMeaning
0The lines were sorted — or, with -c/-C, the input was already sorted.
1With -c/-C, the input was not sorted.
2An 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:

OptionEffect
-c, --countPrefix each line with the number of times it occurred.
-d, --repeatedPrint only lines that were repeated — one copy of each.
-DPrint all copies of every repeated line, not just one.
-u, --uniquePrint 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" #

OptionEffect
-i, --ignore-caseTreat lines differing only in letter case as the same.
-f, --skip-fields=NIgnore the first N fields when comparing.
-s, --skip-chars=NIgnore the first N characters when comparing.
-w, --check-chars=NCompare at most N characters of each line.

Other options #

OptionEffect
-z, --zero-terminatedTreat the NUL character as the line delimiter.

Exit status #

CodeMeaning
0Success.
1A 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 set1 is replaced by the character at the same position in set2. tr 'abc' 'xyz' turns every a into x, b into y, c into z.
  • Delete (-d) — give one set. Every character in set1 is 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:

NotationMeans
a-zA 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, \\, \NNNEscapes — newline, tab, backslash, an octal byte.

Options #

OptionEffect
-d, --deleteDelete the characters in set1 instead of translating.
-s, --squeeze-repeatsCollapse each run of a repeated character listed in the last set into one.
-c, -C, --complementOperate on the complement of set1 — every character it does not list.
-t, --truncate-set1Before translating, shorten set1 to the length of set2, rather than reusing set2's last character to cover the surplus.

Exit status #

CodeMeaning
0Success.
1A 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 #

OptionEffect
-t, --tabs=NSet tab stops every N columns instead of the default 8.
-t, --tabs=LISTGiven a comma-separated list, place tab stops at those explicit column positions.
-i, --initialConvert 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 #

CodeMeaning
0Success.
1A 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 #

OptionEffect
-t, --tabs=NSet 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, --allConvert all runs of spaces that reach a tab stop, not only the leading indentation.
--first-onlyConvert only the leading run of blanks on each line. Overrides -a.

Exit status #

CodeMeaning
0Success.
1A 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 #

OptionEffect
-w, --width=WIDTHFill lines up to at most WIDTH columns. The default is 75.
-g, --goal=WIDTHAim for WIDTH columns — the width fmt targets, while -w is the hard maximum. Defaults to about 93% of the maximum.

Controlling the reflow #

OptionEffect
-s, --split-onlyOnly split lines that are too long; never join short lines together.
-u, --uniform-spacingPut exactly one space between words and two between sentences.
-c, --crown-marginPreserve the indentation of a paragraph's first two lines; indent the rest to match the second.
-t, --tagged-paragraphLike -c, but the first line must be indented differently from the second, or the two are treated as separate paragraphs.
-q, --quickBreak lines faster, accepting a more ragged right edge.

Selecting which lines to format #

OptionEffect
-p, --prefix=PREFIXReformat only lines that begin with PREFIX; reattach PREFIX afterwards. Useful for reflowing comment blocks in source code.
-m, --preserve-headersDetect and preserve mail-style header lines rather than reflowing them.
--tab-width=NTreat a tab as N columns when measuring line length. Tabs are kept in the output; this affects measurement only.

Exit status #

CodeMeaning
0Success.
1A 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 #

OptionEffect
-w, --width=WIDTHWrap at WIDTH columns instead of 80.
-s, --spacesBreak at a space within the width where one exists, rather than mid-word — a gentler wrap.
-b, --bytesCount bytes rather than display columns. Control characters such as tab and newline then count as ordinary bytes.
-c, --charactersCount character positions rather than display columns.

Exit status #

CodeMeaning
0Success.
1A 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.

OptionEffect
-COLUMNProduce COLUMN columns per page (e.g. -3 for three). Text fills down the first column, then the next.
-a, --acrossFill the columns across — line 1 in column 1, line 2 in column 2, and so on — rather than down.
-m, --mergePrint several files side by side, one per column.
-w, --width=WIDTHSet the page width, for multi-column output.

The header and the page #

OptionEffect
-h, --header=TEXTUse TEXT in the header instead of the file name.
-t, --omit-headerPrint neither the header nor the trailer.
-T, --omit-paginationDrop the header, the trailer, and all pagination — just the text.
-l, --length=LINESSet the page length to LINES instead of 66.
-o, --indent=NIndent every line by N spaces.
-D, --date-format=FORMATFormat the header's date with FORMAT.
-F, -f, --form-feedSeparate pages with a form-feed character rather than blank lines.

Selecting and numbering #

OptionEffect
--pages=FIRST[:LAST]Print only pages FIRST through LAST.
-n, --number-lines[=SEP[WIDTH]]Number every line.
-N, --first-line-number=NBegin line numbering at N.
-d, --double-spaceDouble-space the output.

Other options #

OptionEffect
-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-linesMerge full lines, turning off width truncation.
-r, --no-file-warningsDo not warn when a file cannot be opened.

Exit status #

CodeMeaning
0Success.
1A 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 #

OptionEffect
-W, --word-regexp=REGEXPTreat anything matching REGEXP as a keyword.
-b, --break-file=FILETake the set of word-break characters from FILE.
-i, --ignore-file=FILERead a list of words to exclude from FILE.
-o, --only-file=FILEIndex only the words listed in FILE.
-f, --ignore-caseFold case together when sorting.

Output format #

OptionEffect
-w, --width=NSet the output width, in columns.
-g, --gap-size=NSet the gap, in columns, between the output fields.
-A, --auto-referenceGenerate a reference (file name and line number) for each entry automatically.
-r, --referencesTreat the first field of each input line as a reference for that line.
-R, --right-side-refsPut references on the right, and exclude them from the width count.
-F, --flag-truncation=STRINGUse STRING to mark where a line was truncated.
-O, --format=roffEmit the index as roff typesetting directives.
-T, --format=texEmit the index as TeX typesetting directives.
-M, --macro-name=NAMEUse NAME as the macro name in roff/TeX output.
-G, --traditionalBehave like the older System V ptx, without the extended features.

Exit status #

CodeMeaning
0Success.
1A 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 #

CodeMeaning
0A valid order was produced.
1The 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:

OptionCounts
-l, --linesThe number of lines.
-w, --wordsThe number of words — runs of non-whitespace.
-c, --bytesThe number of bytes.
-m, --charsThe number of characters. This differs from -c for multi-byte text, where one character is several bytes.
-L, --max-line-lengthThe length of the longest line.
$ wc -l report.txt        # just the line count
214 report.txt

Other options #

OptionEffect
--files0-from=FTake the list of files to count from F, as NUL-terminated names. - reads the list from standard input.
--total=WHENControl 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 #

CodeMeaning
0Every file was counted.
1A 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:

OptionInput is…
(a file, or stdin)the lines of the file.
-e, --echothe command-line arguments, each treated as one line.
-i, --input-range=LO-HIthe 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 #

OptionEffect
-n, --head-count=COUNTOutput at most COUNT lines — a random sample rather than the whole shuffle.
-r, --repeatAllow lines to be repeated, so output can be longer than the input. Pair with -n to set how many.
-o, --output=FILEWrite to FILE instead of standard output.
-z, --zero-terminatedTreat 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:

OptionEffect
--random-seed=STRINGSeed the shuffle with STRING. The same seed gives the same shuffle.
--random-source=FILETake the random bytes from FILE. The same file gives the same shuffle. Cannot be combined with --random-seed.

Exit status #

CodeMeaning
0Success.
1A 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

CommandPurpose
echoPrint its arguments as a line of text.
printfPrint text from a format string, with precise control over layout.
yesPrint a line over and over, without stopping.
seqPrint a sequence of numbers.
teeCopy standard input to standard output and to each named file.

The environment

CommandPurpose
envRun a command with a modified environment — or print the current one.
printenvPrint environment variables.

Numbers

CommandPurpose
numfmtConvert numbers to and from human-readable forms (1.5G, 1000000).
factorPrint the prime factors of a number.

Evaluating

CommandPurpose
exprEvaluate an arithmetic, string, or comparison expression.
testEvaluate a condition — a file check or a comparison — as a true/false result.

Exit status

CommandPurpose
true and falseDo 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 #

OptionEffect
-nDo not print the trailing newline.
-eInterpret backslash escape sequences in the arguments (see below).
-EDo not interpret escape sequences. This is the default.

Escape sequences #

With -e, echo recognises these backslash sequences and prints the character they stand for:

SequenceCharacter
\\A literal backslash.
\nNewline.
\tHorizontal tab.
\rCarriage return.
\bBackspace.
\fForm feed.
\vVertical tab.
\aAlert (the terminal bell).
\eEscape.
\0NNNThe byte with octal value NNN.
\xHHThe byte with hexadecimal value HH.
\cStop — 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 #

CodeMeaning
0The output was written.
1The 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:

SequenceCharacter
\\Backslash.
\n \t \rNewline, tab, carriage return.
\b \f \v \aBackspace, form feed, vertical tab, alert.
\eEscape.
\NNNThe byte with octal value NNN.
\xHHThe byte with hexadecimal value HH.
\uHHHH, \UHHHHHHHHA Unicode character by hexadecimal code point.
%%A literal %.

Substitution fields #

A field begins with % and is replaced by the next argument, formatted accordingly.

FieldFormats the argument as…
%sa string.
%ba string, with backslash escapes in it interpreted.
%qa string, quoted so it is safe to reuse as shell input.
%ca single character.
%d, %ia signed integer.
%uan unsigned integer.
%x, %Xan unsigned integer in hexadecimal (lower- or upper-case).
%oan unsigned integer in octal.
%f, %Fa decimal floating-point number.
%e, %Ea number in scientific notation.
%g, %Gwhichever 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 #

CodeMeaning
0Success.
1A 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 #

CodeMeaning
0The output was closed normally.
1A 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 to last.
  • seq first last — count from first up to last.
  • seq first increment last — count from first to last, stepping by increment.

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 #

OptionEffect
-s, --separator=STRINGSeparate the numbers with STRING instead of a newline.
-t, --terminator=STRINGEnd the output with STRING instead of a newline.
-w, --equal-widthPad the numbers with leading zeros so they all have the same width.
-f, --format=FORMATFormat 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 #

CodeMeaning
0The sequence was printed.
1An 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 #

OptionEffect
-i, --ignore-environmentStart from an empty environment, not the inherited one — so the command sees only what you assign. A bare - argument means the same.
-u, --unset=NAMERemove NAME from the environment.
--file=FILERead 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 #

OptionEffect
-C, --chdir=DIRChange to DIR before running the command.
--argv0=NAMERun the command but present NAME as its zeroth argument.
-S, --split-string=SSplit 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:

OptionEffect
--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-handlingList the signal-handling changes the other options requested.

Other options #

OptionEffect
-0, --nullWhen printing the environment, end each line with a NUL character instead of a newline. Not valid together with a command.
-v, --debugPrint 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:

CodeMeaning
(command's own)The command ran; this is its exit status.
125env itself failed — for example, a bad option.
126The command was found but could not be run.
127The 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 #

OptionEffect
-0, --nullEnd 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 #

CodeMeaning
0Every named variable was found and printed.
1A named variable was not set.
2A 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 #

OptionEffect
--from=UNITParse input numbers that carry a UNIT suffix, scaling them up to plain digits.
--to=UNITScale output numbers down and add a UNIT suffix.

A UNIT is one of:

UnitSuffixes mean
noneNo suffixes — a suffix is an error. The default.
siPowers of 1000: 1K = 1000, 1M = 1000000.
iecPowers of 1024: 1K = 1024, 1M = 1048576.
iec-iPowers of 1024 with a two-letter suffix: 1Ki = 1024, 1Mi = 1048576.
autoAccept 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:

OptionEffect
--field=FIELDSConvert only the numbers in these fields. FIELDS uses the same range syntax as cut — 3, 2-5, 2-.
-d, --delimiter=XUse X to separate fields instead of whitespace.
--header[=N]Pass the first N lines through unconverted, as a header.

Shaping the output #

OptionEffect
--format=FORMATFormat the number with a printf-style floating-point FORMAT, controlling width, padding, and precision.
--padding=NPad the output to N columns — positive right-aligns, negative left-aligns.
--groupingGroup digits according to the locale (for example, 1,000,000).
--round=METHODChoose the rounding method used when scaling.
--suffix=SUFFIXAppend SUFFIX to each result, and accept it on input.
--from-unit=N / --to-unit=NSet the unit size the input or output is counted in.

Other options #

OptionEffect
--invalid=MODEChoose what to do with input that is not a valid number: abort, fail, warn, or ignore.
-z, --zero-terminatedTreat the NUL character as the line delimiter.
--debugPrint warnings about questionable input.

Exit status #

CodeMeaning
0Every number was converted.
2An 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 #

OperatorResult
ARG1 + ARG2Sum.
ARG1 - ARG2Difference.
ARG1 * ARG2Product.
ARG1 / ARG2Integer quotient.
ARG1 % ARG2Remainder.

Comparison #

A comparison yields 1 for true and 0 for false. It is arithmetic when both sides are numbers, and lexical otherwise.

OperatorTrue when…
ARG1 = ARG2the two are equal.
ARG1 != ARG2they are unequal.
ARG1 < ARG2ARG1 is less.
ARG1 <= ARG2ARG1 is less or equal.
ARG1 > ARG2ARG1 is greater.
ARG1 >= ARG2ARG1 is greater or equal.

Logic #

OperatorResult
ARG1 | ARG2ARG1 if it is neither null nor 0; otherwise ARG2.
ARG1 & ARG2ARG1 if neither argument is null or 0; otherwise 0.

String operations #

FormResult
length STRINGThe number of characters in STRING.
substr STRING POS LENGTHLENGTH characters of STRING, counting POS from 1.
index STRING CHARSThe position of the first of any of CHARS in STRING, or 0.
STRING : REGEXPMatch REGEXP anchored at the start of STRING. Returns the matched length, or the text captured by \(…\).
match STRING REGEXPThe 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:

CodeMeaning
0The result was neither null nor 0.
1The result was null or 0.
2The expression was syntactically invalid.
3An 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 #

OptionEffect
-h, --exponentsWrite a repeated factor in exponent form — p^e — instead of repeating it.
$ factor -h 360
360: 2^3 3^2 5

Exit status #

CodeMeaning
0Every number was factored.
1An 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 #

TestTrue when the file…
-e FILEexists.
-f FILEexists and is a regular file.
-d FILEexists and is a directory.
-L FILE, -h FILEexists and is a symbolic link.
-p FILEexists and is a named pipe (FIFO).
-S FILEexists and is a socket.
-b FILEexists and is a block device.
-c FILEexists and is a character device.
-s FILEexists and is not empty.

Access #

These three tests ask whether you may actually use the file:

TestTrue when…
-r FILEyou may read the file.
-w FILEyou may write the file.
-x FILEyou 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 #

TestTrue when…
FILE1 -nt FILE2FILE1 is newer than FILE2.
FILE1 -ot FILE2FILE1 is older than FILE2.
FILE1 -ef FILE2both name the same file (same device and inode).

Other file tests #

TestTrue when the file…
-t FDfile descriptor FD is open on a terminal.
-N FILEhas been modified since it was last read.
-O FILE, -G FILEcarries an owner-id / group-id field matching the caller's.
-g FILE, -u FILE, -k FILEhas 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 #

TestTrue when…
-n STRINGSTRING is not empty. A bare STRING means the same.
-z STRINGSTRING is empty.
STRING1 = STRING2the strings are equal.
STRING1 != STRING2the strings are unequal.
STRING1 < STRING2STRING1 sorts before STRING2.
STRING1 > STRING2STRING1 sorts after STRING2.

Integer comparisons #

TestTrue when…
A -eq BA equals B.
A -ne BA does not equal B.
A -lt BA is less than B.
A -le BA is less than or equal to B.
A -gt BA is greater than B.
A -ge BA is greater than or equal to B.

Combining conditions #

FormResult
! EXPRESSIONThe negation.
( EXPRESSION )Grouping — escape the parentheses so the shell does not take them.
EXPR1 -a EXPR2True when both are true.
EXPR1 -o EXPR2True 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 #

CodeMeaning
0The expression was true.
1The expression was false.
2The 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 true is the standard way to write a loop that runs until something inside it breaks out.
  • A deliberate placeholder. Setting a configurable command to true makes that step do nothing and "succeed"; false makes 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, true and false provide it.

They are the simplest commands there are — and the overview's note on exit status is the whole idea behind them.

Exit status #

CodeMeaning
0Returned by true, always.
1Returned 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:

ModeBehaviour
warnWarn on a write error to any output, including a broken pipe, and continue with the remaining outputs.
warn-nopipeWarn 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.
exitExit on a write error to any output, including a broken pipe.
exit-nopipeExit on a write error that is not a pipe error. Broken-pipe errors are ignored.

Options #

OptionEffect
-a, --appendAppend to each named file rather than truncating it.
-i, --ignore-interruptsIgnore the interrupt signal.
-pIgnore 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 #

CodeMeaning
0The input was copied to standard output and every named file without error.
1A 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

CommandPurpose
checksum commandsmd5sum, sha1sum, sha256sum, and the rest — one command per hash algorithm.
cksumThe general checksum tool — any of the algorithms, selected by an option.
sumA small, legacy block-checksum.

Encoding

CommandPurpose
base32 and base64Encode binary data as text using the base32 or base64 alphabet, and decode it back.
basencThe 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:

CommandAlgorithmDigest size
md5sumMD5128-bit
sha1sumSHA-1160-bit
sha224sumSHA-224224-bit
sha256sumSHA-256256-bit
sha384sumSHA-384384-bit
sha512sumSHA-512512-bit
b2sumBLAKE2bup 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.

OptionEffect
-c, --checkRead checksums from the given files and verify them.
--ignore-missingIn check mode, skip unavailable listed files when intentionally checking a subset; see the caveat below.
--quietIn check mode, print nothing for files that pass — only failures.
--statusIn check mode, suppress comparison results and summary warnings; determine success from the exit status. File-access or argument errors may still print diagnostics.
-w, --warnWarn about improperly formatted lines in the checksum file.
--strictIn 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.

OptionEffect
--tagProduce a tagged line — SHA256 (file) = hash — that records which algorithm was used.
-z, --zeroEnd each output line with a NUL character instead of a newline.
-b, --binaryNote the file as read in binary mode.
-t, --textNote 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 #

CodeMeaning
0Success under the selected options.
1Verification 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 #

OptionEffect
-a, --algorithm=NAMEUse the named algorithm.

NAME may be:

NameAlgorithm
crcThe default CRC.
crc32bA CRC-32 variant.
md5MD5 — as md5sum.
sha1SHA-1 — as sha1sum.
sha224, sha256, sha384, sha512The SHA-2 family — as the matching sha*sum.
sha3SHA-3.
blake2bBLAKE2b — as b2sum.
sm3The SM3 hash.
sysv, bsdThe 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.

OptionEffect
-c, --checkRead checksums and verify the files against them.
--ignore-missingSkip unavailable listed files when intentionally checking a subset; see the caveat below.
--quietPrint nothing for files that pass.
--statusSuppress comparison results and summary warnings; check the exit status. File-access or argument errors may still print diagnostics.
-w, --warnWarn about malformed checksum lines.
--strictFail 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 #

OptionEffect
--tagProduce a tagged ALGORITHM (file) = hash line. The default for most algorithms.
--untaggedProduce a plain hash file line.
--rawOutput the raw binary digest, with nothing else.
--base64Print the digest in base64 rather than hexadecimal.
-l, --length=BITSFor an algorithm that supports it, produce a shorter digest.
-z, --zeroEnd each output line with a NUL character.

Exit status #

CodeMeaning
0Success under the selected options.
1Verification 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:

OptionAlgorithm
-rThe BSD checksum, counted in 1 KiB blocks. This is the default.
-s, --sysvThe 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 #

CodeMeaning
0The checksum was computed.
1A 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.

OptionEffect
-d, --decodeDecode instead of encode.
-i, --ignore-garbageWhen decoding, skip any characters that are not part of the alphabet, rather than failing on them.
-w, --wrap=COLSWhen 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 base64 --decode payload.b64 > payload.decoded; then
    printf '%s\n' 'Decoding succeeded; payload.decoded is ready to inspect.'
else
    printf '%s\n' 'Decoding failed; do not use payload.decoded.' >&2
    false
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 #

CodeMeaning
0Success.
1A 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 #

OptionEncoding
--base64Standard base64 — the same as the base64 command.
--base64urlBase64 with a file- and URL-safe alphabet.
--base32Standard base32 — the same as the base32 command.
--base32hexBase32 with the extended-hex alphabet.
--base16Hexadecimal.
--base2msbfA bit string, most-significant bit first.
--base2lsbfA bit string, least-significant bit first.
--z85A compact, ASCII85-style encoding. When encoding, the input length must be a multiple of 4; when decoding, a multiple of 5.
--base58Base58 — an alphabet chosen so its characters are not visually confusable.

Options #

By default basenc encodes. These options apply to whichever encoding you chose:

OptionEffect
-d, --decodeDecode instead of encode.
-i, --ignore-garbageWhen decoding, skip characters that are not part of the alphabet.
-w, --wrap=COLSWhen 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 #

CodeMeaning
0Success.
1No 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

CommandPurpose
datePrint — or set — the system date and time.
sleepPause for a given length of time.
uptimeShow how long the system has been running.

Running a command under a constraint

CommandPurpose
timeoutRun a command, and kill it if it runs too long.
nohupRun a command so it survives the terminal closing.
niceRun a command at an adjusted scheduling priority.
stdbufRun a command with altered stream buffering.
chrootRun a command with a different directory as its root.

Signalling processes

CommandPurpose
killSend a signal to a process.

System identity and capacity

CommandPurpose
unamePrint system information — the OS, the kernel, the machine.
archPrint the machine's hardware architecture.
hostnameDisplay — or set — the system's host name.
hostidPrint the host's numeric identifier.
nprocPrint how many processor cores are available.

Who you are

CommandPurpose
whoamiPrint the name of the user a process is running as.
idPrint the user and group identity of a process, or of a named user.
groupsPrint the groups a user belongs to.
lognamePrint 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

CommandPurpose
tokenInspect and manage a process's token.
logonseInspect and manage logon sessions and their processes.

Storage and terminal

CommandPurpose
syncFlush cached writes out to persistent storage.
ttyPrint the name of the terminal on standard input.
sttyDisplay 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:

SequenceIs replaced by
%Y %m %dYear, month, day.
%H %M %SHour, minute, second.
%FThe full date — same as %Y-%m-%d.
%TThe time — same as %H:%M:%S.
%A %aWeekday name, full and abbreviated.
%B %bMonth name, full and abbreviated.
%jDay of the year.
%sSeconds since 1970-01-01 UTC.
%z %ZNumeric and named time zone.
%%A literal %.

The full set is long; date --help lists every sequence with an example.

Displaying a different moment #

OptionEffect
-d, --date=STRINGShow the time described by STRING — "yesterday", "next Friday", "@1615432800" — instead of now.
-r, --reference=FILEShow FILE's last modification time.
-f, --file=DATEFILELike -d, but once for each line of DATEFILE.
-u, --universalWork in Coordinated Universal Time (UTC) rather than the local zone.

Standard formats #

OptionOutput
-I, --iso-8601[=FMT]ISO 8601 — FMT is date, hours, minutes, seconds, or ns.
-R, --rfc-emailThe format used in email headers.
--rfc-3339=FMTRFC 3339 — FMT is date, seconds, or ns.

Setting the clock #

OptionEffect
-s, --set=STRINGSet 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 #

CodeMeaning
0The date was printed or set.
1An 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:

SuffixUnit
sSeconds. This is the default if no suffix is given.
mMinutes.
hHours.
dDays.

Given more than one argument, sleep waits for the sum of them — sleep 1m 30s pauses for ninety seconds.

Exit status #

CodeMeaning
0The full time elapsed.
1A 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.

OptionEffect
-s, --signal=SIGNALSend SIGNAL instead of TERM when the limit is reached.
-k, --kill-after=DURATIONIf the command is still running DURATION after the first signal, send it a KILL signal — which cannot be ignored.
--preserve-statusExit with the command's own status even when it timed out.
--foregroundAllow the command to keep reading from the terminal; do not time out the command's own children.
-v, --verboseReport 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:

CodeMeaning
(command's own)The command finished within the limit.
124The command timed out.
125timeout itself failed.
126The command was found but could not be run.
127The command was not found.
137The 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.out in the current directory. If that file cannot be opened there, nohup falls back to nohup.out in 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.

OptionEffect
--sddl=SDDLCreate 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:

CodeMeaning
(command's own)The command ran; this is its exit status.
125nohup 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.

OptionEffect
-n, --adjustment=NAdd 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:

CodeMeaning
(command's own)The command ran; this is its exit status.
0No command was given; the niceness was printed.
125nice itself failed — for example, a bad adjustment value.
126The command was found but could not be run.
127The 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:

SignalMeaning
TERMTerminate. The default — a request to stop cleanly.
KILLStop immediately. Cannot be caught or ignored, so it cannot be refused — but the process gets no chance to clean up. The last resort.
HUPHang up. Conventionally tells a long-running service to reload its configuration.
INTInterrupt — the signal a terminal sends on Ctrl-C.
STOP / CONTSuspend the process / resume a suspended one.
OptionEffect
-s, --signal=SIGNALSend SIGNAL instead of TERM.
-SIGNALA shorthand — -KILL or -9 both send KILL.
-l, --listList the signal names.
-L, --tableList 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 #

CodeMeaning
0Every signal was sent.
1A 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 #

OptionPrints
-sThe kernel name.
-oThe operating-system name — Peios.
-nThe node name — the name this system is known by on the network.
-rThe kernel release.
-vThe kernel version.
-mThe machine's hardware name (its architecture).
-pThe processor type.
-iThe hardware platform.
-aEverything — 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 #

CodeMeaning
0The requested information was printed.
1The 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 #

CodeMeaning
0The architecture was printed.
1The 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:

OptionPrints
-f, --fqdnThe fully qualified domain name — the full host.domain form. This is the default.
-s, --shortThe short host name only — the part before the first dot.
-d, --domainThe DNS domain part only.
-i, --ip-addressThe 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 #

CodeMeaning
0The host name was displayed or set.
1A 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 #

CodeMeaning
0The 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 #

OptionEffect
--allPrint the number of cores the whole system has, ignoring any restriction on the current process.
--ignore=NSubtract 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 #

CodeMeaning
0The count was printed.
1An 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 #

OptionEffect
-p, --prettyShow just the uptime, in a readable phrase — up 6 days, 3 hours, 21 minutes.
-s, --sinceShow the date and time the system started, rather than how long ago that was.

Exit status #

CodeMeaning
0The uptime was printed.
1The 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:

OptionEffect
file...Flush only the given files, rather than everything.
-d, --dataFor the given files, flush only the file data, not metadata that does not need it.
-f, --file-systemFlush the entire file system that contains each given file.

Exit status #

CodeMeaning
0The flush completed.
1A 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 #

OptionEffect
-s, --silentPrint nothing; report the answer only through the exit status.

Exit status #

CodeMeaning
0Standard input is a terminal.
1Standard input is not a terminal.
2A 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 #

OptionEffect
-a, --allPrint every setting, in a readable layout.
-g, --savePrint 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:

SettingEffect
echo / -echoShow / hide typed characters — -echo is how a password prompt hides input.
rows N / cols NTell the terminal its size.
A numberSet the connection speed (baud rate).

stty has a large catalog of settings; stty -a shows them all with their current values.

Choosing the terminal #

OptionEffect
-F, --file=DEVICEOperate on DEVICE instead of the terminal on standard input.

Exit status #

CodeMeaning
0The settings were printed or changed.
1A 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 #

OptionStream
-i, --input=MODEStandard input.
-o, --output=MODEStandard output.
-e, --error=MODEStandard error.

MODE is one of:

MODEBuffering
0Unbuffered — every write goes out immediately.
LLine-buffered — output is flushed at the end of each line. Not valid for input.
a sizeFully 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:

CodeMeaning
(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.
125stdbuf itself failed, for example an unusable TMPDIR.
126 / 127The 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 #

OptionEffect
--skip-chdirDo 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:

CodeMeaning
125chroot itself failed — for example, newroot is not a directory.
126The command was found but could not be run.
127The 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 #

CodeMeaning
0The name was printed.
1The 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 #

OptionEffect
-u, --userPrint only the effective user ID.
-g, --groupPrint only the effective group ID.
-G, --groupsPrint only the group IDs, space-separated.
-n, --nameWith -u, -g or -G, print names instead of numbers.
-r, --realWith -u, -g or -G, print the real ID rather than the effective one.
-z, --zeroSeparate entries with NUL rather than whitespace. Not allowed in the default format.
-Z, --contextPrint only the security context.
-pA readable, multi-line form.
-PPrint as a password-file entry.
-aIgnored; accepted for compatibility.
-ANot 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 -G reads 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: Everyone and Local, which are on every token.
  • id -G jack looks 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 #

CodeMeaning
0The identity was printed.
1A 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, Batch and Service are 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 in Interactive at 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 #

CodeMeaning
0The groups were printed.
1A 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 #

CodeMeaning
0The login name was printed.
1No 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.

FlagTarget
--selfYour own token. The default.
--realYour primary token specifically, rather than the effective one — relevant when your thread is impersonating.
--pid PIDThe primary token of process PID.
--tid TIDThe effective token of thread TID: its impersonation token, or the process primary token when not impersonating (used with --pid).
--peer SOCK_FDThe 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.

FlagEffect
--shortA one-line summary.
--allEvery query class — the fullest dump.

Field accessors #

Each of these prints one part of a token, for when you want just that piece:

SubcommandPrints
userThe user SID — who the token is.
ownerThe default owner SID.
groupThe primary group SID.
groupsThe group list.
privsThe privileges, with their enabled state.
capsThe capabilities.
claimsThe user and device claims.
integrityThe integrity level.
logonThe logon type and logon SID.
sourceWhat minted the token.
originThe originating session for a derived token.
statsToken statistics — IDs, timestamps, the modification counter.
default-daclThe 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:

token compare 1482 1500
token compare 1482 --pid 1482 --tid 1509
token compare self --real
token compare self --peer 7 --json

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:

token threads 1482
token threads 1482 --json

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:

FormChanges
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 SDDLReplace the token's default DACL. Also --owner-idx / --group-idx.
adjust interactivity-scope SCOPEReplace 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.

FlagEffect
--drop-privs MASK|NAMESPrivileges 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 #

SubcommandEffect
impersonateBegin impersonating the target token on the calling thread. With a trailing -- command …, run that command under the impersonating token.
revertDrop any active impersonation on the calling thread.

See Impersonation for the model these drive.

Creating tokens #

SubcommandEffect
create SPECCreate a token from a binary token-spec (SPEC is a file, or - for standard input).
install SPECCreate 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 #

FlagEffect
--rawRender SIDs in raw S-1-… form only.
--labelRender SIDs as their labels where known, falling back to raw.
--jsonEmit 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 #

CodeMeaning
0The operation succeeded.
1A usage error.
non-zeroThe 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
FlagMeaning
--logon-type TYPEThe kind of logon: interactive, network, batch, service, network-cleartext, new-credentials, or remote-interactive.
--auth-package STRThe name of the authentication package that vouched for the logon.
--user-sid SIDThe 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.

Note

The tool builds the kernel's binary session spec from these fields for you. The underlying wire format is unchanged; only the command-line surface is typed.

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.

FlagMeaning
--checkOnly 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.

FlagMeaning
--pid PIDThe process to act on.
--mitigations MASKThe 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 #

FlagEffect
--jsonEmit JSON instead of human-readable output. Accepted by every subcommand.

Exit status #

CodeMeaning
0The operation succeeded.
1A 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 TARGET pair. Supply both, or use --source / --target.
  • A lone target is valid only for -o remount and for a standalone propagation change (--make-*), which act on an existing mount point.

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

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

Canonicalisation #

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

VerbHow to request itWhat it does
New mountmount [-t T] SRC TGTAttach a fresh instance of the filesystem at SRC onto TGT.
Bind-B / --bind, or -o bindMake an existing subtree visible at a second location.
Recursive bind-R / --rbind, or -o rbindBind a subtree together with every mount underneath it.
Move-M / --move, or -o moveRelocate an existing mount to a new mount point.
Move beneath--beneath SRC TGTAttach SRC beneath the mount currently at TGT (the kernel enforces several constraints; violations surface as exit 32).
Remount-o remount[,...] TGTChange the options of an existing mount (see Remounting).
Propagation--make-* / --make-r*, or the -o tokens belowChange how mount/unmount events propagate across a subtree.
Listmount / mount -lPrint 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 tokenRecursive flagRecursive -o tokenMeaning
--make-sharedshared--make-rsharedrsharedEvents propagate to and from peer mounts.
--make-slaveslave--make-rslaverslaveEvents propagate in from the master but not back out.
--make-privateprivate--make-rprivaterprivateNo propagation either way.
--make-unbindableunbindable--make-runbindablerunbindablePrivate, 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 #

FormResolution
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 fileUsed 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 fileAttached 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).

OptionEffect
ro / rwRead-only / read-write.
suid / nosuidHonour / ignore set-user-ID bits.
dev / nodevAllow / disallow device nodes.
exec / noexecAllow / disallow execution.
atime / noatimeUpdate / never update access times.
relatime / norelatimeRelative-atime updates on or off.
strictatime / nostrictatimeStrict-atime updates on or off.
diratime / nodiratimeDirectory access-time updates on or off.
nosymfollowDo 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 #

OptionEffect
sync / asyncSynchronous / asynchronous writes.
dirsyncSynchronous directory updates.
lazytime / nolazytimeLazy on-disk timestamp updates on or off.
iversion / noiversionInode version counting on or off.
silent / loudSuppress or emit certain kernel messages.
mand / nomandObsolete (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=:

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

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 #

OptionEffect
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=DIRAttach 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.noloopSuppress the implicit loop device for a regular-file source.
X-mount.auto-fstypes=LISTConstrain 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:

OptionPolicy class
-o policy=deny-missingfacs_deny_missing — a file with no SD is unreachable.
-o policy=synth-ephemeralfacs_synthesize_ephemeral — a missing SD is synthesised in memory only.
-o policy=synth-persistfacs_synthesize_persistent — a missing SD is synthesised and written back.
--synth-sddl SDDLProvide 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.

Warning

The peiosutils 0.8.18 diagnostic still says the policy operation requires SeTcbPrivilege. That is not the full gate in the pinned kernel. See Check policy access before interpreting this as a need to grant TCB; administrator membership alone is not enough, and automatic escalation or a global CAP_SYS_ADMIN remap is not a remedy.

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 #

FlagEffect
-t, --types TYPEFilesystem type, or auto.
-o, --options LISTMount options (above); repeatable.
-r, --ro, --read-onlyMount read-only (-o ro).
-w, --rw, --read-writeMount read-write and forbid the automatic read-only fallback on a write-protected device (-o rw).
--source SRC / --target DIRName the operands explicitly.
--target-prefix DIRPrepend DIR/ to the target.
-B, --bind / -R, --rbind / -M, --move / --beneathThe structural verbs.
--make-*, --make-r*Propagation changes.
--exclusiveForce 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 UUIDSelect the source by filesystem label / UUID.
-c, --no-canonicalizeDo not canonicalise paths.
-f, --fakeSkip mount and policy syscalls; target preparation still runs. With --mkdir or X-mount.mkdir, directories may be created. See execution limits.
-v, --verboseNarrate resolved values and each syscall; drain the kernel fs_context log. Repeatable but -vv is the same as -v.
-l, --show-labelsIn list mode, append each filesystem's label.
--onlyonceAccepted 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 NSOperate 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-onlyDo not invoke a mount.<type> helper.
-n, --no-mtabAccepted and ignored (Peios has no mtab).
--synth-sddl SDDLKACS synth-policy template SD (above).
-h, --help / -V, --versionStandard.

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.
  • --fake can 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=0700 for 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 #

CodeMeaning
0Success.
1Incorrect 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".
2System error (out of memory, no free loop device, cannot fork).
4Internal error (an invariant failure).
8Interrupted (SIGINT), after signal-safe cleanup.
32Mount failure — a syscall in the flow failed, a label/UUID had no match, a probe was undetermined, or a --beneath/move/--exclusive kernel refusal.
126An 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, umount refuses with an error naming the candidates — resolve it by naming a specific mount point, or use -A to unmount all of them.

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

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

Recursive and all-targets unmounts #

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

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

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

Flags #

FlagEffect
-l, --lazyDetach the filesystem now and clean up references later (MNT_DETACH).
-f, --forceForce the unmount (MNT_FORCE), e.g. for an unreachable server.
-R, --recursiveUnmount the target and everything under it (see above).
-A, --all-targetsUnmount every mount point of the given source (see above).
-d, --detach-loopAfter 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-onlyIf the unmount fails, remount the filesystem read-only instead (via mount_setattr). Mutually exclusive with -R.
-g, --gracefulExit 0 when the target is absent or not mounted (rather than failing). Applied unconditionally.
-q, --quietSuppress "not mounted" messages.
-c, --no-canonicalizeDo not canonicalise paths; selects UMOUNT_NOFOLLOW. Applied unconditionally.
-v, --verboseSay what is being unmounted. Repeatable.
-N, --namespace NSEnter 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-mtabAccepted and ignored (no mtab on Peios).
-i, --internal-onlyDo not invoke a umount.<type> helper (none ship).
--fakeDry run: resolve operands and report, but skip the unmount syscalls.
-h, --help / -V, --versionStandard.

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 #

CodeMeaning
0Success (or a -g no-op on an absent target).
1Incorrect invocation or a permission denial — including -R with -r, a missing operand, an ambiguous source, an embedded NUL, or an authorisation failure.
2System error (e.g. cannot read the mount table).
8Interrupted (SIGINT).
32Unmount failed, or the target is not mounted (unless -g).
64Several source/target arguments were given and at least one succeeded while another failed.
126An 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/block unpatched, 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 — OWNER and MODE, read the same way ls -l reads them.
  • The /dev/disk/by-* symlink farm — ID-LINK. Populated by the device manager once it has run; there is deliberately no /run/udev/data parser, so the column reads the links themselves and is empty in the initramfs, where no device manager runs.

Output modes #

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

FlagMode
(default)Indented tree.
-l, --listFlat list — the same columns, no tree glyphs.
-J, --jsonJSON. Flag columns render as bare booleans and MOUNTPOINTS as an array; children nest under a children key.
-P, --pairsKEY="value" pairs, one device per line.
-r, --rawRaw, 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):

FlagColumn set
-o, --output LISTExactly the comma-separated columns named (case-insensitive; an unknown name is a usage error).
-O, --output-allEvery available column.
-f, --fsFilesystem view: NAME, FSTYPE, FSVER, LABEL, UUID, MOUNTPOINTS.
-m, --permsPermissions 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 #

FlagEffect
-a, --allInclude empty (zero-size) devices, which are hidden by default.
-d, --nodepsDo not print a device's holders or slaves (drop the children).
-I, --include LISTShow only devices with these major numbers (comma-separated).
-e, --exclude LISTExclude devices by major number. The default is 1 (RAM disks); an explicit -e replaces that default, and -a clears exclusions entirely.
-s, --inversePrint dependencies in inverse order (holders above the devices they depend on).
-M, --mergeCollapse a subtree shared by several parents (a multipath device) to a single occurrence.
-E, --dedup COLUMNDrop rows whose COLUMN value duplicates an earlier one.
-x, --sort COLUMNSort siblings by COLUMN (numeric columns sort by value, not text).

Formatting #

FlagEffect
-b, --bytesPrint SIZE as an exact byte count instead of a human-readable value.
-p, --pathsPrint full /dev paths in the NAME column.
-n, --noheadingsOmit the header row.
-i, --asciiDraw the tree with ASCII characters instead of box-drawing glyphs.
-y, --shellRender column keys shell-safe (MAJ:MIN becomes MAJ_MIN).
-w, --width NUMTruncate each table row to NUM columns wide.
--sysroot DIRRead sysfs, the mount table and /dev from DIR instead of / (chiefly for testing against a fixture).
-h, --help / -V, --versionStandard.

Exit status #

CodeMeaning
0Success.
1Incorrect 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:

  1. Validate the layout. <src-dir> must be a directory, must carry an executable init (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.seq or system/prelude/hooks.seq.<n> — which mkirf generates. A missing or non-executable init, or a stray sequence file, stops the build.

  2. 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 in LC_ALL=C byte order, which places every directory before its descendants — the order the kernel's unpacker requires.

  3. 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. mkirf parses each hook's # /// hook metadata 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 mkirf writes every version it can express faithfully — currently hooks.seq.1 (the flat resolved order) and hooks.seq.2 (that order plus each hook's declarations). mkirf ships in peiosutils and 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, alongside system/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.

  4. 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 success mkirf prints 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, symlinks 0777, FIFOs and device nodes 0644, regular files 0755 if executable and 0644 otherwise.
  • 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 -n equivalent) 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; no init, or init is 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 # /// hook metadata block (unclosed, a non-comment line inside it, a duplicate or unknown key, a badly-formed array), a dependency cycle, a requires that 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). An after naming a capability nothing supplies is not an error — that is what distinguishes it from requires. (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, and mkirf prints 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, --watch with <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 #

OptionEffect
--watchStay resident and rebuild on every change to <src-dir>, after an initial build. Runs until killed.
--debounce SECSWith --watch, the settle time before a rebuild. Default 5. A non-numeric value is a usage error.
--exclude GLOBExclude 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:

PositionalMeaning
<src-dir>The source tree; its contents map onto / in the initramfs.
<out-file>The output cpio.gz archive.

Exit status #

CodeMeaning
0The image was built (or --help/--version was printed).
1An operational failure — an invalid layout, an unresolvable hook set, or an I/O/compression error.
2A 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 # /// hook metadata 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:

SectionContentSource
.cmdlineThe kernel command line, NUL-terminated.--cmdline or --cmdline-file
.linuxThe kernel image.--kernel, or the unique kernel resolved by --kernel-dir
.initrdThe 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.

Note

mkuki does not sign the image. Producing the unified binary and signing it for Secure Boot are separate steps; mkuki has no signing options. Because a UKI bundles the kernel, initramfs, and command line into one PE image, that image is what a later Secure Boot milestone signs and the firmware verifies as a whole — but the signing itself is out of mkuki's scope.

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 #

OptionEffect
--kernel PATHA concrete kernel image; becomes the .linux section. Mutually exclusive with --kernel-dir.
--kernel-dir PATHResolve 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 PATHThe initramfs cpio; becomes the .initrd section. Required (except with --stub-info).
--cmdline TEXTThe kernel command line, given literally; becomes the .cmdline section. Mutually exclusive with --cmdline-file.
--cmdline-file PATHRead the kernel command line from PATH instead. Mutually exclusive with --cmdline.
--out PATHThe output UKI path. Required (except with --stub-info).
--stub PATHThe PE/COFF EFI stub to append sections to. Defaults to the bundled systemd stub.
--stub-infoPrint the bundled stub's provenance and exit.
--watchStay resident and rebuild the UKI whenever the selected kernel source, --initramfs, or --cmdline-file changes. Runs until killed.
--debounce SECSWith --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 #

CodeMeaning
0The UKI was built (or --stub-info, --help, or --version was printed).
1A build or watch failure — a missing or unreadable input, an invalid stub, an empty payload, or an I/O error.
2A usage error — a bad or missing option, or an invalid --cmdline/--cmdline-file combination.

See also #

  • mkirf — builds the .initrd payload mkuki wraps.
  • 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 nowreg
Look up what a value means — its type, default, valid range, when it appliesregman

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:

PathMeaning
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 .reg convention: 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:

PrefixRegistry typeData syntax
sz:REG_SZUTF-8 string (rest of token, verbatim).
expand:REG_EXPAND_SZUTF-8 string, %VAR% left unexpanded on disk.
dword:REG_DWORD42 or 0x2A; must fit u32.
dword-be:REG_DWORD_BIG_ENDIAN42 or 0x2A; must fit u32.
qword:REG_QWORD42 or 0x2A; must fit u64.
multi:REG_MULTI_SZComma-separated; \, escapes a literal comma.
hex: / bin:REG_BINARYHex bytes; :, -, and space separators are ignored.
link:REG_LINKAn absolute key path (a symlink target).
none:REG_NONENo 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 shapeInferred type
all decimal digits, fits u32REG_DWORD
all decimal digits, fits u64 but not u32REG_QWORD
0x… hex, ≤ 8 significant hex digitsREG_DWORD
0x… hex, ≤ 16 significant hex digitsREG_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.

OptionEnv varEffect
--jsonREG_JSON=1Emit structured JSON instead of human text. watch emits JSON-lines.
-v, --verboseREG_VERBOSE=1More detailed output.
-q, --quiet—Suppress non-essential output. (A surprising type coercion is still reported.)
--sep=CHARREG_SEPPath display separator: \ (default) or /.
--help—Print help for the command and exit 0.
--version—Print the version and exit 0.

Mutating subcommands additionally accept:

OptionEnv varEffect
--layer NAMEREG_LAYERTarget layer for the write (default: the base layer).
-y, --yesREG_ASSUME_YES=1Skip 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.

OptionEffect
-L, --layersAnnotate the value with the layer it resolved from and its sequence number.
--rawWrite the value's raw bytes to standard output verbatim — for piping REG_BINARY data.
--no-followOperate 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.

OptionEffect
-l, --longLong form: for subkeys, child and value counts; for values, type and byte size.
--keys-onlyList subkeys only.
--values-onlyList 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.

OptionEffect
--depth NLimit recursion to N levels.
--valuesInclude 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.

OptionEffect
--no-followInspect 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.

OptionEffect
--layer NAMEWrite into layer NAME instead of the base layer.
-p, --parentsCreate any missing ancestor keys.
--expected-seq NCompare-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.

OptionEffect
--layer NAMECreate the key in layer NAME.
-p, --parentsCreate any missing ancestor keys.
--volatileCreate 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.

OptionEffect
--layer NAMEDelete from layer NAME.
-r, --recursiveDelete 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, --yesSkip 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.

OptionEffect
--layer NAMEThe layer that carries the tombstone.
--allBlanket 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.

OptionEffect
--layer NAMEThe 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.

OptionEffect
-l, --longLong 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.

OptionEffect
--precedence NPrecedence (higher wins). A precedence above 0 requires SeTcbPrivilege; the tool attempts it and reports any denial.
--owner SIDRecord an informational owner SID, stored as a binary SID. Without it, the owner is whoever creates the layer.
--disabledCreate 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.

OptionEffect
--precedence NChange the precedence.
--enable / --disableEnable or disable the layer.
--owner SIDChange 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).

OptionEffect
--set SDDLApply the given SDDL. Without it, sd prints the current descriptor.
--ownerScope to the owner.
--groupScope to the group.
--daclScope to the DACL.
--saclScope 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.

OptionEffect
--layer NAMECreate 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.

OptionEffect
--subtreeWatch descendant keys too, not just this key.
--filter LISTComma-separated subset of value, subkey, sd (default: all).
--count NExit 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.

OptionEffect
--dir DIRApply every batch file in DIR (sorted), each as its own transaction. A missing or empty directory applies nothing and succeeds.
--once-deleteDelete each batch file after it applies successfully (a drain).
-y, --yesSkip 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.

{"keys": [
  {"path": "Machine\\Generic"},
  {"path": "Machine\\Generic\\Events", "descriptor": "S:(AL;CI;0x10002;;;WD)"}
]}

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.

OptionEffect
--layer NAMEExport 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 typeHuman formJSON form
REG_SZ / REG_EXPAND_SZthe string{"type":"sz","data":"…"}
REG_DWORD / REG_QWORDdecimal{"type":"dword","data":42}
REG_MULTI_SZone element per line{"type":"multi","data":["a","b"]}
REG_BINARYhex{"type":"binary","data":"deadbeef"}
REG_LINKthe 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 #

CodeMeaningTypical cause
0Success.—
1Usage error.Bad arguments; a required option missing.
2Key or value not found.ENOENT.
3Access denied.EACCES, EPERM — the kernel refused the operation.
4Invalid specification.Bad path, type, or literal; a cross-hive batch; a name too long (EINVAL, EXDEV, ENAMETOOLONG).
5Syscall or source failure.EIO, ETIMEDOUT, ENOSPC, ENOMEM.
6Compare-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 you reg set.
  • Keys, values, and types — the data model reg addresses.
  • Layers — what "effective" resolves against, and what --layer, mask, and hide operate on.
  • Access control on keys — the check every reg operation 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:

FieldTells you
TypeThe value's registry type (REG_QWORD, REG_SZ, …).
DefaultThe value used when nothing is set.
ValidThe range, set, or constraint a sensible value must satisfy.
AppliesWhen 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 validregman — the manual
What the value is set toreg — a live query against the store
What the subsystem is actually running onthe 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.
  ...
FactTells you
TierHow 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.
GatingWhat 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.
CardinalityHow 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 meansevman — the manual
Which events have happenedthe Event Viewer, or a query against eventd
Whether an event type is recorded at allits 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.

TaskCommandsPackage
Search textgrep, including grep -E, grep -F and grep -Porg.gnu.grep
Transform linessedorg.gnu.sed
Process fields and recordsawk, gawkorg.gnu.gawk
Select files and pass argumentsfind, xargsorg.gnu.findutils
Compare filesdiff, cmp, diff3, sdifforg.gnu.diffutils
Read long files interactivelylesscom.greenwoodsoftware.less
Identify file contentsfilecom.darwinsys.file
Create and extract tar archivestarorg.gnu.tar
Gzip compressiongzip, gunzip, zcatorg.gnu.gzip
XZ compressionxz, unxz, xzcatorg.tukaani.xz
Zstandard compressionzstd, unzstd, zstdcatcom.facebook.zstd
Bzip2 compressionbzip2, bunzip2, bzcatorg.sourceware.bzip2
Create ZIP archiveszipnet.sourceforge.infozip.zip
Inspect and extract ZIP archivesunzip, zipinfonet.sourceforge.infozip.unzip

Peiosutils supplies the other basic text commands, including cat, head, tail, sort, uniq, cut, paste, join, tr and wc.

Inspecting text #

grep -n 'error' output.txt
awk '{ print $1 }' records.txt
diff before.txt after.txt
less output.txt

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.

tar -caf backup.tar.xz documents/
tar -tf backup.tar.xz
mkdir restored
tar -xf backup.tar.xz -C restored

zip -r documents.zip documents/
unzip -l documents.zip
unzip documents.zip -d restored-zip

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:

loadkeys --bkeymap uk > uk.bmap

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:

loadkeys -C /dev/tty1 uk
setfont -C /dev/tty1 Lat2-Terminus16

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:

reg set Machine/System/Console Keymap sz:uk
svctl restart console-keymap

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, kmes and ntfe (§3 to §7), then peinit, peipkg, eventd, authd, lpsd, timed, netd and trustd (§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 python3 tools/gen-events-book.py --check

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.

FieldHeader fieldMeaning
event.typetypeThe event type, as the emitter named it.
event.timetimestampWall clock at the moment KMES accepted the event, nanoseconds since the Unix epoch.
event.sequencesequenceThe emitting CPU's per-boot counter. The first event on each CPU gets 1, and the numbers on one ring are contiguous.
event.cpucpu_idThe 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.classorigin_classThe emission path that wrote the record: 0 userspace, 1 kmes, 2 kacs, 3 lcs, 4 ntfe.
emitter.token.guideffective_token_guidThe effective token of the task that caused the event.
emitter.true-token.guidtrue_token_guidThat task's own token, before any impersonation.
emitter.process.guidprocess_guidThe 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.

TypeWire form
bin.sidbin holding the binary SID.
bin.guidbin, exactly 16 bytes.
bin.acebin holding the binary ACE, copied from the descriptor.
binbin: a digest or other opaque bytes. Shown as hexadecimal.
uint.luiduint.
uint.timeuint, nanoseconds since the Unix epoch.
uint.durationuint, nanoseconds.
uint.bytesuint, a size in bytes.
uint.maskuint, an access mask, decoded against the table for the record's object.kind.
uint.flagsuint, a set of bits the field's definition names.
uint.integrityuint, the integrity RID.
uint.enumuint, where the numbers are fixed by another specification. Rendered by name.
int.errnoint, a negative error number.
boolbool.
str.enumstr, a kebab-case name.
str.pathstr, a path.
str.ipstr, an address in the textual form for its family.
strstr, UTF-8 text.
uint, intuint 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 #

FieldTypeMeaning
subject.token.sidbin.sidThe user SID of the effective token the operation ran under.
subject.token.integrityuint.integrityThe integrity RID of the effective token.
subject.token.iduint.luidThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumThe impersonation level of the effective token.

2.1.2 Carried by #

  • lcs.audit.backup.ended
  • lcs.audit.backup.started
  • lcs.audit.key.created
  • lcs.audit.key.deleted
  • lcs.audit.key.descriptor.changed
  • lcs.audit.key.hidden
  • lcs.audit.key.opened
  • lcs.audit.key.tombstoned
  • lcs.audit.restore.ended
  • lcs.audit.restore.started
  • lcs.audit.transaction.committed
  • lcs.audit.value.deleted
  • lcs.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 #

FieldTypeMeaning
subject.token.sidbin.sidThe user SID of the effective token the operation ran under.
subject.token.groupsbin.sid[]The group SIDs carried by the effective token.
subject.token.group-attributesuint.flags[]Per-group attribute bitmasks, positionally parallel to subject.token.groups.
subject.token.integrityuint.integrityThe integrity RID of the effective token.
subject.token.iduint.luidThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumThe impersonation level of the effective token.
subject.token.uiduintThe Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data.
emitter.process.piduintThe process ID the record was written from.
emitter.process.namestrThe kernel's name for the emitting process, typically the executable's basename.
emitter.process.executablestr.pathThe path the emitting process was executed from, resolved at exec with symbolic links already followed.
subject.pip.typeuint.enumThe Protected Isolated Process type in force for the access check.
subject.pip.trustuintThe PIP trust level in force for the access check.

2.2.2 Carried by #

  • kacs.audit.access.checked
  • kacs.audit.descriptor.changed
  • kacs.audit.handle.used
  • kacs.audit.privilege.used
  • kacs.caap.policy.changed
  • kacs.caap.sacl.skipped
  • kacs.caap.staging.diverged
  • kacs.descriptor.rejected
  • kacs.impersonation.reverted
  • kacs.impersonation.started
  • kacs.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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia subjectThe user SID of the effective token the operation ran under.
subject.token.groupsbin.sid[]via subjectThe group SIDs carried by the effective token.
subject.token.group-attributesuint.flags[]via subjectPer-group attribute bitmasks, positionally parallel to subject.token.groups.
subject.token.integrityuint.integrityvia subjectThe integrity RID of the effective token.
subject.token.iduint.luidvia subjectThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia subjectThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia subjectWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia subjectThe impersonation level of the effective token.
subject.token.uiduintvia subjectThe Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data.
emitter.process.piduintvia subjectThe process ID the record was written from.
emitter.process.namestrvia subjectThe kernel's name for the emitting process, typically the executable's basename.
emitter.process.executablestr.pathvia subjectThe path the emitting process was executed from, resolved at exec with symbolic links already followed.
subject.pip.typeuint.enumvia subjectThe Protected Isolated Process type in force for the access check.
subject.pip.trustuintvia subjectThe PIP trust level in force for the access check.
object.kindstr.enumrequiredWhat sort of thing the operation acted on.
object.file.pathstr.pathwhen object.kind == fileThe absolute path of the file the operation acted on, resolved at the enforcement point.
object.process.piduintwhen object.kind == processThe process ID of the process the operation acted on.
object.process.guidbin.guidwhen object.kind == processThe durable GUID of the process the operation acted on.
object.token.iduint.luidwhen object.kind == tokenThe LUID of the token the operation acted on, for token-open and token-adjust checks.
object.token.guidbin.guidwhen object.kind == tokenThe durable GUID of the token the operation acted on.
access.requesteduint.maskrequiredThe access mask the caller asked for, after generic bits have been mapped to type-specific ones.
access.granteduint.maskrequiredThe mask this check granted.
access.denied-integrityuint.maskoptionalThe 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-trustuint.maskoptionalThe requested bits the object's process-trust label withheld, read the same way.
outcome.successboolrequiredTrue 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.kindstr.enumrequiredWhy an audit record exists at all.
trigger.acebin.acewhen trigger.kind == saclThe exact ACE that caused this record, so a consumer can identify which rule fired.
fields.attestation.userspacebooloptionalSet 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia subjectThe user SID of the effective token the operation ran under.
subject.token.groupsbin.sid[]via subjectThe group SIDs carried by the effective token.
subject.token.group-attributesuint.flags[]via subjectPer-group attribute bitmasks, positionally parallel to subject.token.groups.
subject.token.integrityuint.integrityvia subjectThe integrity RID of the effective token.
subject.token.iduint.luidvia subjectThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia subjectThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia subjectWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia subjectThe impersonation level of the effective token.
subject.token.uiduintvia subjectThe Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data.
emitter.process.piduintvia subjectThe process ID the record was written from.
emitter.process.namestrvia subjectThe kernel's name for the emitting process, typically the executable's basename.
emitter.process.executablestr.pathvia subjectThe path the emitting process was executed from, resolved at exec with symbolic links already followed.
subject.pip.typeuint.enumvia subjectThe Protected Isolated Process type in force for the access check.
subject.pip.trustuintvia subjectThe PIP trust level in force for the access check.
object.kindstr.enumrequiredfile, token, process or ipc.
object.file.pathstr.pathoptionalOn a file, its absolute path, when the handle's path could be resolved.
object.token.iduint.luidwhen object.kind == tokenThe LUID of the token the operation acted on, for token-open and token-adjust checks.
object.token.guidbin.guidwhen object.kind == tokenThe durable GUID of the token the operation acted on.
object.process.guidbin.guidwhen object.kind == processThe durable GUID of the process the operation acted on.
object.ipc.typestr.enumwhen object.kind == ipcWhich kind of System V IPC object the operation acted on: a semaphore set, a shared-memory segment or a message queue.
object.ipc.idintwhen object.kind == ipcThe System V IPC identifier of the object, as semget, shmget or msgget returned it.
object.sd.componentsuint.flagsrequiredThe parts the caller set. A SACL bit means the record was not discretionary: such a change is always recorded.
object.sd.lengthuint.byteswhen outcome.success == trueThe byte length of the object's security descriptor.
object.sd.digestbinwhen outcome.success == trueSHA-256 of the descriptor as written.
object.sd.ownerbin.sidoptionalThe owner of the descriptor written, when it has one.
object.sd.length-previousuint.bytesoptionalThe byte length of the descriptor a change replaced.
object.sd.digest-previousbinoptionalSHA-256 of the descriptor replaced. Absent, with its length and owner, where the object had no stored descriptor to replace.
object.sd.owner-previousbin.sidoptionalThe owner SID a change replaced.
access.requesteduint.maskrequiredThe rights the change needed: WRITE_OWNER, WRITE_DAC, ACCESS_SYSTEM_SECURITY, as the parts set ask.
access.granteduint.maskoptionalWhat the handle the change was made through was opened with. Absent on a change made without one.
access.audit-maskuint.maskoptionalThe alarm mask on that handle.
access.matcheduint.maskoptionalThe 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.successboolrequiredWhether 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.errnoint.errnowhen outcome.success == falseThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia subjectThe user SID of the effective token the operation ran under.
subject.token.groupsbin.sid[]via subjectThe group SIDs carried by the effective token.
subject.token.group-attributesuint.flags[]via subjectPer-group attribute bitmasks, positionally parallel to subject.token.groups.
subject.token.integrityuint.integrityvia subjectThe integrity RID of the effective token.
subject.token.iduint.luidvia subjectThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia subjectThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia subjectWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia subjectThe impersonation level of the effective token.
subject.token.uiduintvia subjectThe Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data.
emitter.process.piduintvia subjectThe process ID the record was written from.
emitter.process.namestrvia subjectThe kernel's name for the emitting process, typically the executable's basename.
emitter.process.executablestr.pathvia subjectThe path the emitting process was executed from, resolved at exec with symbolic links already followed.
subject.pip.typeuint.enumvia subjectThe Protected Isolated Process type in force for the access check.
subject.pip.trustuintvia subjectThe PIP trust level in force for the access check.
object.kindstr.enumrequiredAlways file today, because FACS is the only enforcement point.
object.file.pathstr.pathrequiredThe absolute path of the file the operation acted on, resolved at the enforcement point.
operation.namestr.enumrequiredWhich 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.requesteduint.maskrequiredWhat this operation needs — a read needs FILE_READ_DATA, an mmap with PROT_EXEC needs FILE_EXECUTE.
access.matcheduint.maskrequiredThe subset of the requested access that overlapped an audit mask and therefore caused the record to exist.
access.granteduint.maskrequiredWhat 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-maskuint.maskrequiredThe full continuous-audit mask cached on a handle, of which access.matched is the part this operation touched.
outcome.successboolrequiredWhether the operation itself was allowed.
outcome.reasonstr.enumwhen outcome.success == falseFrom 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia subjectThe user SID of the effective token the operation ran under.
subject.token.groupsbin.sid[]via subjectThe group SIDs carried by the effective token.
subject.token.group-attributesuint.flags[]via subjectPer-group attribute bitmasks, positionally parallel to subject.token.groups.
subject.token.integrityuint.integrityvia subjectThe integrity RID of the effective token.
subject.token.iduint.luidvia subjectThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia subjectThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia subjectWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia subjectThe impersonation level of the effective token.
subject.token.uiduintvia subjectThe Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data.
emitter.process.piduintvia subjectThe process ID the record was written from.
emitter.process.namestrvia subjectThe kernel's name for the emitting process, typically the executable's basename.
emitter.process.executablestr.pathvia subjectThe path the emitting process was executed from, resolved at exec with symbolic links already followed.
subject.pip.typeuint.enumvia subjectThe Protected Isolated Process type in force for the access check.
subject.pip.trustuintvia subjectThe PIP trust level in force for the access check.
operation.namestr.enumrequiredWhich operation was attempted.

Values here (open set): access-check · linux-cap · volume-mount.
object.kindstr.enumoptionalOn 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.pathstr.pathwhen object.kind == fileThe absolute path of the file the operation acted on, resolved at the enforcement point.
object.process.piduintwhen object.kind == processThe process ID of the process the operation acted on.
object.process.guidbin.guidwhen object.kind == processThe durable GUID of the process the operation acted on.
object.token.iduint.luidwhen object.kind == tokenThe LUID of the token the operation acted on, for token-open and token-adjust checks.
object.token.guidbin.guidwhen object.kind == tokenThe durable GUID of the token the operation acted on.
privilege.namestr.enumrequiredThe privilege a record is about, by its Windows name.
privilege.contributeduint.maskwhen operation.name == access-checkThe access bits this privilege supplied to the check, intersected with what the caller actually asked for.
privilege.survivinguint.maskwhen operation.name == access-checkThe subset of privilege.contributed that reached the final granted mask.
linux.capstr.enumwhen operation.name == linux-capThe 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.requesteduint.maskwhen operation.name == access-checkThe whole check's requested mask, not this privilege's share of it.
access.granteduint.maskwhen operation.name == access-checkThe whole check's final granted mask.
outcome.successboolrequiredOn 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.userspacebooloptionalSet 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia subjectThe user SID of the effective token the operation ran under.
subject.token.groupsbin.sid[]via subjectThe group SIDs carried by the effective token.
subject.token.group-attributesuint.flags[]via subjectPer-group attribute bitmasks, positionally parallel to subject.token.groups.
subject.token.integrityuint.integrityvia subjectThe integrity RID of the effective token.
subject.token.iduint.luidvia subjectThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia subjectThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia subjectWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia subjectThe impersonation level of the effective token.
subject.token.uiduintvia subjectThe Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data.
emitter.process.piduintvia subjectThe process ID the record was written from.
emitter.process.namestrvia subjectThe kernel's name for the emitting process, typically the executable's basename.
emitter.process.executablestr.pathvia subjectThe path the emitting process was executed from, resolved at exec with symbolic links already followed.
subject.pip.typeuint.enumvia subjectThe Protected Isolated Process type in force for the access check.
subject.pip.trustuintvia subjectThe PIP trust level in force for the access check.
caap.policy.sidbin.sidrequiredThe SID identifying a central access policy.
operation.namestr.enumrequiredset 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.successboolrequiredWhether the operation succeeded.
outcome.errnoint.errnowhen outcome.success == falseThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia subjectThe user SID of the effective token the operation ran under.
subject.token.groupsbin.sid[]via subjectThe group SIDs carried by the effective token.
subject.token.group-attributesuint.flags[]via subjectPer-group attribute bitmasks, positionally parallel to subject.token.groups.
subject.token.integrityuint.integrityvia subjectThe integrity RID of the effective token.
subject.token.iduint.luidvia subjectThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia subjectThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia subjectWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia subjectThe impersonation level of the effective token.
subject.token.uiduintvia subjectThe Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data.
emitter.process.piduintvia subjectThe process ID the record was written from.
emitter.process.namestrvia subjectThe kernel's name for the emitting process, typically the executable's basename.
emitter.process.executablestr.pathvia subjectThe path the emitting process was executed from, resolved at exec with symbolic links already followed.
subject.pip.typeuint.enumvia subjectThe Protected Isolated Process type in force for the access check.
subject.pip.trustuintvia subjectThe PIP trust level in force for the access check.
object.kindstr.enumrequiredWhat sort of thing the operation acted on.
object.file.pathstr.pathwhen object.kind == fileThe absolute path of the file the operation acted on, resolved at the enforcement point.
object.process.piduintwhen object.kind == processThe process ID of the process the operation acted on.
object.process.guidbin.guidwhen object.kind == processThe durable GUID of the process the operation acted on.
object.token.iduint.luidwhen object.kind == tokenThe LUID of the token the operation acted on, for token-open and token-adjust checks.
object.token.guidbin.guidwhen object.kind == tokenThe durable GUID of the token the operation acted on.
caap.policy.sidbin.sidrequiredThe SID identifying a central access policy.
caap.rule.indexuintrequiredThe zero-based position of a rule within the central access policy named by caap.policy.sid.
caap.phasestr.enumrequiredWhich phase of a central access policy's audit evaluation a CAAP diagnostic describes.
outcome.reasonstr.enumrequiredinvalid-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.requesteduint.maskrequiredThe access mask the caller asked for, after generic bits have been mapped to type-specific ones.
access.granteduint.maskrequiredThe effective grant of the check the rule was evaluated in.
access.granted-stageduint.maskrequiredWhat a staged central access policy, not yet in force, would have granted.
fields.attestation.userspacebooloptionalSet 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia subjectThe user SID of the effective token the operation ran under.
subject.token.groupsbin.sid[]via subjectThe group SIDs carried by the effective token.
subject.token.group-attributesuint.flags[]via subjectPer-group attribute bitmasks, positionally parallel to subject.token.groups.
subject.token.integrityuint.integrityvia subjectThe integrity RID of the effective token.
subject.token.iduint.luidvia subjectThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia subjectThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia subjectWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia subjectThe impersonation level of the effective token.
subject.token.uiduintvia subjectThe Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data.
emitter.process.piduintvia subjectThe process ID the record was written from.
emitter.process.namestrvia subjectThe kernel's name for the emitting process, typically the executable's basename.
emitter.process.executablestr.pathvia subjectThe path the emitting process was executed from, resolved at exec with symbolic links already followed.
subject.pip.typeuint.enumvia subjectThe Protected Isolated Process type in force for the access check.
subject.pip.trustuintvia subjectThe PIP trust level in force for the access check.
object.kindstr.enumrequiredWhat sort of thing the operation acted on.
object.file.pathstr.pathwhen object.kind == fileThe absolute path of the file the operation acted on, resolved at the enforcement point.
object.process.piduintwhen object.kind == processThe process ID of the process the operation acted on.
object.process.guidbin.guidwhen object.kind == processThe durable GUID of the process the operation acted on.
object.token.iduint.luidwhen object.kind == tokenThe LUID of the token the operation acted on, for token-open and token-adjust checks.
object.token.guidbin.guidwhen object.kind == tokenThe durable GUID of the token the operation acted on.
access.requesteduint.maskrequiredThe access mask the caller asked for, after generic bits have been mapped to type-specific ones.
access.granteduint.maskrequiredWhat the effective policy granted: the access the caller actually got.
access.granted-stageduint.maskrequiredWhat the staged policy would have granted instead.
fields.attestation.userspacebooloptionalSet 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 #

FieldTypePresenceMeaning
config.key.pathstrrequiredThe registry key the reported value lives under, in canonical form.
config.namestroptionalThe 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.reasonstr.enumrequiredempty: 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.errnoint.errnorequiredThe error the operation failed with, as a negative errno.
policy.previous-retainedboolrequiredAlways true: a rejected table leaves the one in force in place.
policy.fallbackboolrequiredWhether 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia subjectThe user SID of the effective token the operation ran under.
subject.token.groupsbin.sid[]via subjectThe group SIDs carried by the effective token.
subject.token.group-attributesuint.flags[]via subjectPer-group attribute bitmasks, positionally parallel to subject.token.groups.
subject.token.integrityuint.integrityvia subjectThe integrity RID of the effective token.
subject.token.iduint.luidvia subjectThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia subjectThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia subjectWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia subjectThe impersonation level of the effective token.
subject.token.uiduintvia subjectThe Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data.
emitter.process.piduintvia subjectThe process ID the record was written from.
emitter.process.namestrvia subjectThe kernel's name for the emitting process, typically the executable's basename.
emitter.process.executablestr.pathvia subjectThe path the emitting process was executed from, resolved at exec with symbolic links already followed.
subject.pip.typeuint.enumvia subjectThe Protected Isolated Process type in force for the access check.
subject.pip.trustuintvia subjectThe PIP trust level in force for the access check.
object.kindstr.enumrequiredAlways file today: file descriptors are the only stored descriptors KACS reads this way.
object.file.pathstr.pathoptionalNot 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.inodeuintrequiredThe inode number the object presents.
object.file.deviceuintrequiredWith 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.lengthuint.bytesrequiredThe 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.reasonstr.enumrequiredWhy 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia subjectThe user SID of the effective token the operation ran under.
subject.token.groupsbin.sid[]via subjectThe group SIDs carried by the effective token.
subject.token.group-attributesuint.flags[]via subjectPer-group attribute bitmasks, positionally parallel to subject.token.groups.
subject.token.integrityuint.integrityvia subjectThe integrity RID of the effective token.
subject.token.iduint.luidvia subjectThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia subjectThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia subjectWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia subjectThe impersonation level of the effective token.
subject.token.uiduintvia subjectThe Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data.
emitter.process.piduintvia subjectThe process ID the record was written from.
emitter.process.namestrvia subjectThe kernel's name for the emitting process, typically the executable's basename.
emitter.process.executablestr.pathvia subjectThe path the emitting process was executed from, resolved at exec with symbolic links already followed.
subject.pip.typeuint.enumvia subjectThe Protected Isolated Process type in force for the access check.
subject.pip.trustuintvia subjectThe PIP trust level in force for the access check.
emitter.thread.tiduintrequiredThe thread that wrote the record, where the operation is scoped to a thread rather than to its process.
object.kindstr.enumrequiredAlways token.
object.token.guidbin.guidrequiredThe impersonation token given up.
object.token.sidbin.sidrequiredThe user SID of the token the operation acted on.
operation.namestr.enumrequiredrevert 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.successboolrequiredWhether the operation succeeded.
outcome.errnoint.errnowhen outcome.success == falseThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia subjectThe user SID of the effective token the operation ran under.
subject.token.groupsbin.sid[]via subjectThe group SIDs carried by the effective token.
subject.token.group-attributesuint.flags[]via subjectPer-group attribute bitmasks, positionally parallel to subject.token.groups.
subject.token.integrityuint.integrityvia subjectThe integrity RID of the effective token.
subject.token.iduint.luidvia subjectThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia subjectThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia subjectWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia subjectThe impersonation level of the effective token.
subject.token.uiduintvia subjectThe Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data.
emitter.process.piduintvia subjectThe process ID the record was written from.
emitter.process.namestrvia subjectThe kernel's name for the emitting process, typically the executable's basename.
emitter.process.executablestr.pathvia subjectThe path the emitting process was executed from, resolved at exec with symbolic links already followed.
subject.pip.typeuint.enumvia subjectThe Protected Isolated Process type in force for the access check.
subject.pip.trustuintvia subjectThe PIP trust level in force for the access check.
emitter.thread.tiduintrequiredThe thread that wrote the record, where the operation is scoped to a thread rather than to its process.
object.kindstr.enumrequiredAlways token.
object.token.guidbin.guidrequiredThe durable GUID of the token the operation acted on.
object.token.iduint.luidrequiredThe LUID of the token the operation acted on, for token-open and token-adjust checks.
object.token.sidbin.sidrequiredThe client: whose identity the server took on or asked for.
object.token.typestr.enumrequiredprimary only on a refusal: a primary token cannot be impersonated.
object.token.integrityuint.integrityrequiredThe integrity level of the token the operation acted on, as an integrity RID.
object.token.auth-iduint.luidoptionalAbsent only for a token with no logon session.
object.token.restrictedboolrequiredWhether the token is restricted: whether it carries restricting SIDs that every access check must also satisfy.
object.token.impersonationuint.enumrequiredThe level the client token offered: the level asked for.
object.token.impersonation-permitteduint.enumoptionalThe level the gate allowed. Absent when the attempt was refused before the gate answered.
privilege.namestr.enumoptionalSeImpersonatePrivilege, 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.heldboolwhen privilege.name == SeImpersonatePrivilegeAlways true where present: a server without the privilege is not refused but lowered to Identification, which impersonation-permitted shows.
outcome.successboolrequiredWhether the operation succeeded.
outcome.errnoint.errnowhen outcome.success == falseThe error the operation failed with, as a negative errno.
outcome.reasonstr.enumoptionalOn 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia subjectThe user SID of the effective token the operation ran under.
subject.token.groupsbin.sid[]via subjectThe group SIDs carried by the effective token.
subject.token.group-attributesuint.flags[]via subjectPer-group attribute bitmasks, positionally parallel to subject.token.groups.
subject.token.integrityuint.integrityvia subjectThe integrity RID of the effective token.
subject.token.iduint.luidvia subjectThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia subjectThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia subjectWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia subjectThe impersonation level of the effective token.
subject.token.uiduintvia subjectThe Linux UID this token projects onto, for correlating a Peios event with Linux-side audit data.
emitter.process.piduintvia subjectThe process ID the record was written from.
emitter.process.namestrvia subjectThe kernel's name for the emitting process, typically the executable's basename.
emitter.process.executablestr.pathvia subjectThe path the emitting process was executed from, resolved at exec with symbolic links already followed.
subject.pip.typeuint.enumvia subjectThe Protected Isolated Process type in force for the access check.
subject.pip.trustuintvia subjectThe PIP trust level in force for the access check.
object.mount.fs-typestroptionalThe filesystem's type, when the kernel knows it.
object.mount.policystr.enumrequiredThe KACS mount policy in force, which decides what happens to an object with no stored descriptor.
object.mount.policy-previousstr.enumrequiredThe mount policy a change replaced, beside object.mount.policy, the one it put in force.
object.mount.policy-generationuintrequiredThe generation the change began. Every descriptor cached under an older one is discarded on its next use.
outcome.successboolrequiredAlways 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 #

FieldTypePresenceMeaning
object.session.iduint.luidrequiredThe LUID of the logon session the operation acted on.
object.session.user.sidbin.sidrequiredThe user SID of the principal the logon session belongs to: whose session it is.
object.session.logon-typestr.enumrequiredWhat kind of logon the session represents, from the KACS_LOGON_TYPE_* values in uapi/pkm/token.h.
object.session.auth-packagestrrequiredThe name of the authentication package that established the session, as the creator supplied it.
object.session.logon-timeuint.timerequiredWhen 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 #

FieldTypePresenceMeaning
signature.crypto-stagestr.enumrequiredAlways boot-probe today.
outcome.errnoint.errnorequiredThe 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 #

FieldTypePresenceMeaning
object.file.path-relativestr.pathoptionalAbsent 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.indexuintrequiredWhich stratum an object was read from, by index within the mount's stack.
source.stratum.pathstr.pathoptionalAbsent only from a reduced record, with the other paths.
destination.stratum.indexuintrequiredWhich stratum an object was written into, by index within the mount's stack.
destination.stratum.pathstr.pathoptionalAbsent only from a reduced record, with the other paths.
outcome.successboolrequiredWhether the operation succeeded.
outcome.errnoint.errnowhen outcome.success == falseThe 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 #

FieldTypePresenceMeaning
object.file.path-relativestr.pathoptionalAbsent 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.namestr.enumrequiredStrataFS 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.indexuintoptionalAbsent when the refusal was raised before any provider was determined.
source.stratum.pathstr.pathoptionalPresent exactly when source.stratum.index is, except in a reduced record, which carries no paths.
outcome.errnoint.errnorequiredThe error the operation failed with, as a negative errno.
outcome.deferredboolrequiredWhether 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia callerThe user SID of the effective token the operation ran under.
subject.token.integrityuint.integrityvia callerThe integrity RID of the effective token.
subject.token.iduint.luidvia callerThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia callerThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia callerWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia callerThe impersonation level of the effective token.
object.key.guidbin.guidrequiredThe registry key an operation acted on.
outcome.successboolrequiredWhether the operation succeeded.
outcome.errnoint.errnowhen outcome.success == falseThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia callerThe user SID of the effective token the operation ran under.
subject.token.integrityuint.integrityvia callerThe integrity RID of the effective token.
subject.token.iduint.luidvia callerThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia callerThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia callerWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia callerThe impersonation level of the effective token.
object.key.guidbin.guidrequiredThe registry key an operation acted on.
operation.fduintrequiredThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia callerThe user SID of the effective token the operation ran under.
subject.token.integrityuint.integrityvia callerThe integrity RID of the effective token.
subject.token.iduint.luidvia callerThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia callerThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia callerWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia callerThe impersonation level of the effective token.
object.kindstr.enumrequiredAlways key.
object.key.guidbin.guidrequiredThe new key.
object.key.pathstrrequiredThe new key's full path.
object.key.layer.namestroptionalThe layer the key was created in.
object.key.createdboolrequiredAlways true.
object.key.volatileboolrequiredWhether a registry key is volatile, meaning it lives only in memory and does not survive a reboot.
object.key.volatile-requestedboolrequiredEqual to object.key.volatile, since only a key that was made is recorded.
object.key.symlinkboolrequiredWhether a registry key is a symbolic link to another key.
object.sd.lengthuint.bytesrequiredThe new key's descriptor, as built from the parent's inheritable ACEs and the caller's token.
object.sd.ownerbin.sidoptionalThe owner SID on the object's security descriptor.
access.requesteduint.maskrequiredAlways KEY_CREATE_SUB_KEY, the right checked on the parent.
access.granteduint.maskrequiredWhat the parent check granted.
transaction.iduintoptionalThe transaction the operation belonged to, registry or package.
outcome.successboolrequiredAlways 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia callerThe user SID of the effective token the operation ran under.
subject.token.integrityuint.integrityvia callerThe integrity RID of the effective token.
subject.token.iduint.luidvia callerThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia callerThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia callerWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia callerThe impersonation level of the effective token.
object.kindstr.enumrequiredAlways key.
object.key.guidbin.guidrequiredThe registry key an operation acted on.
object.key.pathstrrequiredThe resolved absolute path of the registry key, in canonical form, such as Machine\System\KMES.
object.key.layer.namestroptionalThe layer whose entry was deleted.
object.layer.namestroptionalPresent when the delete removed a layer, naming it.
access.requesteduint.maskrequiredAlways DELETE.
access.granteduint.maskrequiredThe access mask in force.
access.matcheduint.maskrequiredThe subset of the requested access that overlapped an audit mask and therefore caused the record to exist.
access.audit-maskuint.maskrequiredThe full continuous-audit mask cached on a handle, of which access.matched is the part this operation touched.
transaction.iduintoptionalThe transaction the operation belonged to, registry or package.
request.timed-outboolwhen outcome.success == falseWhether a request's wait for the source's reply ran out before the reply arrived.
outcome.successboolrequiredWhether the operation succeeded.
outcome.errnoint.errnowhen outcome.success == falseThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia callerThe user SID of the effective token the operation ran under.
subject.token.integrityuint.integrityvia callerThe integrity RID of the effective token.
subject.token.iduint.luidvia callerThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia callerThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia callerWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia callerThe impersonation level of the effective token.
object.kindstr.enumrequiredAlways key.
object.key.guidbin.guidrequiredThe registry key an operation acted on.
object.key.pathstrrequiredThe resolved absolute path of the registry key, in canonical form, such as Machine\System\KMES.
object.sd.componentsuint.flagsrequiredThe parts the caller asked to change, its security_info.
object.sd.lengthuint.bytesoptionalThe 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-previousuint.bytesoptionalAbsent when the request failed before the old descriptor was read.
object.sd.digestbinoptionalPresent with object.sd.length.
object.sd.digest-previousbinoptionalPresent with object.sd.length-previous.
object.sd.ownerbin.sidoptionalPresent with object.sd.length.
object.sd.owner-previousbin.sidoptionalPresent with object.sd.length-previous.
access.requesteduint.maskrequiredThe rights the change needs: WRITE_OWNER for the owner or group, WRITE_DAC for the DACL, ACCESS_SYSTEM_SECURITY for the SACL.
access.granteduint.maskrequiredThe access mask in force.
access.matcheduint.maskrequiredZero when the record exists only because the change included the SACL.
access.audit-maskuint.maskrequiredThe full continuous-audit mask cached on a handle, of which access.matched is the part this operation touched.
transaction.iduintoptionalThe transaction the operation belonged to, registry or package.
request.timed-outboolwhen outcome.success == falseTrue when the source did not answer in time, so the change may have been applied after all.
outcome.successboolrequiredWhether the operation succeeded.
outcome.errnoint.errnowhen outcome.success == falseThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia callerThe user SID of the effective token the operation ran under.
subject.token.integrityuint.integrityvia callerThe integrity RID of the effective token.
subject.token.iduint.luidvia callerThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia callerThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia callerWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia callerThe impersonation level of the effective token.
object.kindstr.enumrequiredAlways key.
object.key.guidbin.guidrequiredThe registry key an operation acted on.
object.key.pathstrrequiredThe resolved absolute path of the registry key, in canonical form, such as Machine\System\KMES.
object.key.layer.namestroptionalThe layer the marker was written in.
access.requesteduint.maskrequiredAlways DELETE.
access.granteduint.maskrequiredThe access mask in force.
access.matcheduint.maskrequiredThe subset of the requested access that overlapped an audit mask and therefore caused the record to exist.
access.audit-maskuint.maskrequiredThe full continuous-audit mask cached on a handle, of which access.matched is the part this operation touched.
mutation.sequenceuintoptionalThe sequence number LCS stamped on a registry mutation.
transaction.iduintoptionalThe transaction the operation belonged to, registry or package.
request.timed-outboolwhen outcome.success == falseWhether a request's wait for the source's reply ran out before the reply arrived.
outcome.successboolrequiredWhether the operation succeeded.
outcome.errnoint.errnowhen outcome.success == falseThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia callerThe user SID of the effective token the operation ran under.
subject.token.integrityuint.integrityvia callerThe integrity RID of the effective token.
subject.token.iduint.luidvia callerThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia callerThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia callerWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia callerThe impersonation level of the effective token.
object.kindstr.enumrequiredAlways key. Names the table the access masks decode against.
object.key.guidbin.guidrequiredThe registry key an operation acted on.
access.requesteduint.maskrequiredAfter registry generic mapping, with MAXIMUM_ALLOWED re-added when the caller asked for it.
access.granteduint.maskrequiredForced 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.successboolrequiredWhether the open was allowed. Replaces the former decision string, which carried the same two states as words.
trigger.sacl-matchuint.flagsrequiredWhich 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia callerThe user SID of the effective token the operation ran under.
subject.token.integrityuint.integrityvia callerThe integrity RID of the effective token.
subject.token.iduint.luidvia callerThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia callerThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia callerWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia callerThe impersonation level of the effective token.
object.kindstr.enumrequiredAlways key.
object.key.guidbin.guidrequiredThe registry key an operation acted on.
object.key.pathstrrequiredThe resolved absolute path of the registry key, in canonical form, such as Machine\System\KMES.
object.key.layer.namestroptionalThe layer the blanket belongs to.
operation.namestr.enumoptionalWhether the blanket was set or cleared. Absent only when the request failed before it was read.

Values here (closed set): set · clear.
access.requesteduint.maskrequiredAlways KEY_SET_VALUE.
access.granteduint.maskrequiredThe access mask in force.
access.matcheduint.maskrequiredThe subset of the requested access that overlapped an audit mask and therefore caused the record to exist.
access.audit-maskuint.maskrequiredThe full continuous-audit mask cached on a handle, of which access.matched is the part this operation touched.
mutation.sequenceuintoptionalPresent when a blanket was set: clearing one stamps no sequence.
transaction.iduintoptionalThe transaction the operation belonged to, registry or package.
request.timed-outboolwhen outcome.success == falseWhether a request's wait for the source's reply ran out before the reply arrived.
outcome.successboolrequiredWhether the operation succeeded.
outcome.errnoint.errnowhen outcome.success == falseThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia callerThe user SID of the effective token the operation ran under.
subject.token.integrityuint.integrityvia callerThe integrity RID of the effective token.
subject.token.iduint.luidvia callerThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia callerThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia callerWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia callerThe impersonation level of the effective token.
object.key.guidbin.guidrequiredThe registry key an operation acted on.
outcome.successboolrequiredWhether the operation succeeded.
outcome.errnoint.errnowhen outcome.success == falseThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia callerThe user SID of the effective token the operation ran under.
subject.token.integrityuint.integrityvia callerThe integrity RID of the effective token.
subject.token.iduint.luidvia callerThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia callerThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia callerWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia callerThe impersonation level of the effective token.
object.key.guidbin.guidrequiredThe registry key an operation acted on.
operation.fduintrequiredThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia callerThe user SID of the effective token the operation ran under.
subject.token.integrityuint.integrityvia callerThe integrity RID of the effective token.
subject.token.iduint.luidvia callerThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia callerThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia callerWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia callerThe impersonation level of the effective token.
transaction.iduintrequiredThe transaction the operation belonged to, registry or package.
transaction.statestr.enumrequiredThe state it ended in: committed on success.
transaction.commit-outstandingboolwhen outcome.success == falseWhether a transaction's commit had been sent to its source and not yet answered.
outcome.successboolrequiredTrue only when the transaction committed.
outcome.errnoint.errnooptionalPresent when a REG_IOC_COMMIT call failed; absent for a transaction that ended without one.
outcome.reasonstr.enumwhen outcome.success == falseaborted 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia callerThe user SID of the effective token the operation ran under.
subject.token.integrityuint.integrityvia callerThe integrity RID of the effective token.
subject.token.iduint.luidvia callerThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia callerThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia callerWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia callerThe impersonation level of the effective token.
object.kindstr.enumrequiredAlways key.
object.key.guidbin.guidrequiredThe registry key an operation acted on.
object.key.pathstrrequiredThe resolved absolute path of the registry key, in canonical form, such as Machine\System\KMES.
object.key.layer.namestroptionalThe layer whose entry was deleted. Absent only when the request failed before its layer was read.
object.key.value.namestroptionalAbsent only when the request failed before its name was read.
object.key.value.type-previousuint.enumoptionalAbsent when no value was found to delete.
object.key.value.length-previousuint.bytesoptionalPresent with object.key.value.type-previous.
object.key.value.digest-previousbinoptionalPresent with object.key.value.type-previous.
access.requesteduint.maskrequiredAlways KEY_SET_VALUE.
access.granteduint.maskrequiredThe access mask in force.
access.matcheduint.maskrequiredThe subset of the requested access that overlapped an audit mask and therefore caused the record to exist.
access.audit-maskuint.maskrequiredThe full continuous-audit mask cached on a handle, of which access.matched is the part this operation touched.
transaction.iduintoptionalThe transaction the operation belonged to, registry or package.
request.timed-outboolwhen outcome.success == falseWhether a request's wait for the source's reply ran out before the reply arrived.
outcome.successboolrequiredWhether the operation succeeded.
outcome.errnoint.errnowhen outcome.success == falseThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidvia callerThe user SID of the effective token the operation ran under.
subject.token.integrityuint.integrityvia callerThe integrity RID of the effective token.
subject.token.iduint.luidvia callerThe token's own LUID, identifying this specific token rather than the logon session it belongs to.
subject.token.auth-iduint.luidvia callerThe LUID of the logon session the effective token belongs to.
subject.token.typestr.enumvia callerWhether the effective token is a primary token or an impersonation token.
subject.token.impersonationuint.enumvia callerThe impersonation level of the effective token.
object.kindstr.enumrequiredAlways key.
object.key.guidbin.guidrequiredThe registry key an operation acted on.
object.key.pathstrrequiredThe resolved absolute path of the registry key, in canonical form, such as Machine\System\KMES.
object.key.layer.namestroptionalThe layer written, base when the caller named none. Absent only when the request failed before its layer was read.
object.key.value.namestroptionalAbsent only when the request failed before its name was read.
object.key.value.typeuint.enumoptionalThe type written. Present once the data was read from the caller.
object.key.value.lengthuint.bytesoptionalPresent with object.key.value.type.
object.key.value.digestbinoptionalPresent with object.key.value.type.
object.key.value.type-previousuint.enumoptionalThe 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-previousuint.bytesoptionalPresent with object.key.value.type-previous.
object.key.value.digest-previousbinoptionalPresent with object.key.value.type-previous.
access.requesteduint.maskrequiredAlways KEY_SET_VALUE.
access.granteduint.maskrequiredWhat the handle was opened with.
access.matcheduint.maskrequiredThe subset of the requested access that overlapped an audit mask and therefore caused the record to exist.
access.audit-maskuint.maskrequiredThe full continuous-audit mask cached on a handle, of which access.matched is the part this operation touched.
mutation.sequenceuintoptionalThe sequence stamped on the write. Absent only when the request failed before one was allocated.
mutation.sequence-expecteduintoptionalPresent on a compare-and-swap write.
transaction.iduintoptionalPresent when the write was staged in a transaction.
request.timed-outboolwhen outcome.success == falseTrue when the source did not answer in time, so the write may have been applied after all.
outcome.successboolrequiredWhether the operation succeeded.
outcome.errnoint.errnowhen outcome.success == falseThe 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 #

FieldTypePresenceMeaning
config.key.pathstrrequiredThe registry key the reported value lives under, in canonical form.
config.namestrrequiredThe name of the configuration value being reported on, within config.key.path.
config.expected.typeuint.enumrequiredThe registry type the value is required to have.
config.expected.minuintrequiredThe smallest value accepted for this setting.
config.expected.maxuintrequiredThe largest value accepted for this setting.
config.received.kindstr.enumrequiredHow the stored value failed validation.
config.received.typeuint.enumwhen config.received.kind == wrong-typeThe registry type the value actually had.
config.received.valueuintwhen config.received.kind == out-of-rangeThe value that was stored and rejected.
config.valueuintrequiredThe 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 #

FieldTypePresenceMeaning
source.rsi.slotuintrequiredWhich registry source slot supplied the data, by index.
source.rsi.hivestroptionalThe hive the offending source data belonged to, where the failure could be attributed to one.
request.iduintoptionalThe identifier of the source request this record concerns, for joining a record to the exchange that produced it.
request.op-codeuintoptionalThe source-protocol operation code of the request this record concerns.
object.key.guidbin.guidoptionalPresent when the rejected data could be attributed to a specific key.
outcome.reasonstr.enumrequiredThe 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 #

FieldTypePresenceMeaning
buffer.capacity-requesteduint.bytesrequiredThe per-CPU ring buffer capacity that was asked for.
buffer.capacityuint.bytesrequiredThe capacity kept.
outcome.errnoint.errnorequired-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 #

FieldTypePresenceMeaning
config.key.pathstrrequiredAlways Machine\System\KMES.
config.counts.applieduintrequiredHow many settings in a configuration refresh were applied.
config.counts.retained-missinguintrequiredHow many settings kept their previous value because they were absent from the registry.
config.counts.retained-invaliduintrequiredHow many settings kept their previous value because the stored value was rejected, of the wrong type or out of range.
config.counts.ignored-unknownuintrequiredHow many registry values were ignored because their names match no setting.
buffer.capacityuint.bytesrequiredThe per-CPU ring capacity in force after the commit.
emission.rate-limituintrequiredThe 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 #

FieldTypePresenceMeaning
config.key.pathstrrequiredMachine\System\KMES or Machine\Generic\Events.
outcome.errnoint.errnorequiredThe 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 #

FieldTypePresenceMeaning
config.key.pathstrrequiredThe registry key the reported value lives under, in canonical form.
config.namestrrequiredThe name of the configuration value being reported on, within config.key.path.
config.expected.typeuint.enumrequiredThe registry type the value is required to have.
config.expected.minuintrequiredThe smallest value accepted for this setting.
config.expected.maxuintrequiredThe largest value accepted for this setting.
config.received.kindstr.enumrequiredHow the stored value failed validation.
config.received.typeuint.enumwhen config.received.kind == wrong-typeThe registry type the value actually had.
config.received.valueuintwhen config.received.kind == out-of-rangeThe value that was stored and rejected.
config.valueuintrequiredThe 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 #

FieldTypePresenceMeaning
policy.generationuintrequiredThe generation this policy now runs as. Verdict records carry the same number.
policy.generation-previousuintrequiredThe 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.layersstr.enum[]requiredThe rules layers the active generation loaded a forest for.
policy.report-thresholduintrequiredThe machine's reporting threshold, CurrentReportingLevel: a rule's report fires only when its level is at least this.
policy.report-threshold-previousuintrequired1, 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 #

FieldTypePresenceMeaning
policy.generationuintrequiredThe generation still in force, which the refused policy did not replace.
policy.previous-retainedboolrequiredAlways true: NTFE never leaves part of a refused policy in force.
outcome.errnoint.errnowhen outcome.reason != no-rules-keyThe error the operation failed with, as a negative errno.
outcome.reasonstr.enumrequiredWhy 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.namestroptionalThe 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-truncatedbooloptionalWhether rule.name was cut short because the rule's path did not fit in the record.
rule.layerstr.enumoptionalThe 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-errorstr.enumwhen outcome.reason == bad-actionWhy 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 #

FieldTypePresenceMeaning
rule.namestrrequiredThe policy rule that produced this record, by name.
rule.hashuintrequiredThe FNV-1a-64 hash of the rule's whole registry path.
rule.name-truncatedbooloptionalWhether rule.name was cut short because the rule's path did not fit in the record.
rule.report-leveluintrequiredThe reporting level the matching rule asked for.
rule.layerstr.enumrequiredWhich evaluation layer the rule sat in.
rule.seatstr.enumrequiredWhere in the path evaluation happened.
outcome.verdictstr.enumrequiredThe decision a policy engine reached, where the decision has more than two states and outcome.success cannot express it.
outcome.reasonstr.enumwhen outcome.verdict == rejectWhich 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.directionstr.enumrequiredWhich way the traffic was moving relative to this machine.
network.interface.namestroptionalAbsent when the traffic was evaluated with no device attached, in which case network.interface.index is 0.
network.interface.indexuintrequiredThe kernel's index for that interface.
network.ether-typeuintrequiredThe link-layer protocol number of the frame.
network.familyuint.enumrequiredThe address family of the addresses in this record.
network.protocoluint.enumwhen network.family != 0The transport protocol number, from the IANA protocol numbers registry.
source.addressstr.ipwhen network.family != 0The address the traffic came from, rendered in the textual form for its family.
destination.addressstr.ipwhen network.family != 0The address the traffic was going to, rendered in the textual form for its family.
source.portuintoptionalPresent when the protocol has ports.
destination.portuintoptionalPresent when the protocol has ports.
flow.statestr.enumoptionalPresent when flow tracking applied.
network.lengthuint.bytesrequiredThe length of the traffic unit evaluated.
policy.generationuintrequiredThe 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 #

FieldTypePresenceMeaning
outcome.reasonstr.enumrequiredThe finding that forced the downgrade.

Values here (open set): critical-cycle · critical-boot-conflict.
graph.servicesstr[]when outcome.reason == critical-cycleThe services a graph finding involves.
object.service.namestrwhen outcome.reason == critical-boot-conflictThe name of the service the event is about, as its definition names it.
object.service.conflict.namestrwhen outcome.reason == critical-boot-conflictThe 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 #

FieldTypePresenceMeaning
object.service.namestroptionalThe service whose cgroup leaked. Absent for a submitted job's cgroup, which belongs to no service.
object.job.guidbin.guidoptionalThe submitted job whose cgroup leaked. Exactly one of object.service.name and this field is present.
object.cgroup.pathstr.pathrequiredThe cgroup the event is about, as its full path under /sys/fs/cgroup/peinit.
object.cgroup.typestr.enumrequiredservice-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 #

FieldTypePresenceMeaning
outcome.successboolrequiredWhether the operation succeeded.
outcome.detailstroptionalWhy the reload failed, for a person. Present only when outcome.success is false.
graph.servicesstr[]optionalThe 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.addeduintwhen outcome.success == trueHow many service definitions a configuration reload added: names that had no definition before.
graph.counts.updateduintwhen outcome.success == trueHow many existing service definitions a configuration reload changed.
graph.counts.restoreduintwhen outcome.success == trueHow many definitions a configuration reload brought back after an earlier reload had removed them while their services were still running.
graph.counts.marked-removeduintwhen outcome.success == trueHow many definitions a configuration reload withdrew from services that were still running.
graph.counts.discardeduintwhen outcome.success == trueHow many definitions a configuration reload withdrew and dropped at once, because their services were not running.
graph.counts.undecodableuintwhen outcome.success == trueHow 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 #

FieldTypePresenceMeaning
graph.servicesstr[]requiredThe 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 #

FieldTypePresenceMeaning
object.service.namestrrequiredThe name of the service the event is about, as its definition names it.
outcome.reasonstr.enumrequiredHow the Critical service failed.

Values here (open set): service-main-terminal · health-check-failure · watchdog-timeout · restart-budget-exhausted.
shutdown.finalization-statestr.enumrequiredHow far the system shutdown had got when the event was written.
outcome.detailstrwhen shutdown.finalization-state == failedWhy 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 #

FieldTypePresenceMeaning
emission.typestrrequiredThe event type an emitter was trying to emit, as it supplied it.
emission.payload-lengthuint.bytesrequiredThe byte length of an event's MessagePack payload, without the header or the event type.
object.service.namestroptionalThe service the refused event was about, or whose job sent it.
object.job.guidbin.guidoptionalThe job the refused event was about, or whose process sent it.
outcome.errnoint.errnooptionalThe 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 #

FieldTypePresenceMeaning
object.service.namestrrequiredThe name of the service the event is about, as its definition names it.
object.fd-store.namestrrequiredThe name a service gave a file descriptor it asked peinit to keep, with FDNAME=.
outcome.reasonstr.enumrequireddisabled 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 #

FieldTypePresenceMeaning
graph.contextuintrequiredThe 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.guidbin.guidrequiredThe 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.namestrrequiredThe name of the service the event is about, as its definition names it.
outcome.successboolrequiredTrue 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 #

FieldTypePresenceMeaning
graph.phasestr.enumrequiredWhen the service graph was validated.
outcome.reasonstr.enumrequiredThe 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.namestroptionalThe service the finding is about. Absent on cycle, which names several.
graph.servicesstr[]when outcome.reason == cycleThe services a graph finding involves.
object.service.dependency.namestroptionalOn missing-hard-dependency and hard-dependency-blocked.
object.service.dependency.kindstr.enumoptionalPresent exactly when object.service.dependency.name is.
object.service.conflict.namestrwhen outcome.reason == conflicting-boot-servicesThe 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.intervaluint.durationwhen outcome.reason == invalid-health-check-restart-windowThe configured interval between health checks, in nanoseconds.
object.service.health-check.retriesuintwhen outcome.reason == invalid-health-check-restart-windowThe configured number of consecutive health-check failures at which peinit stops reporting the service unhealthy and acts on it.
object.service.health-check.restart-windowuint.durationwhen outcome.reason == invalid-health-check-restart-windowThe configured restart window of a health check, in nanoseconds.
object.service.typestr.enumwhen outcome.reason == unschedulable-health-checkHow the service's main process is supervised.
object.service.timer.schedulestrwhen outcome.reason == invalid-timer-scheduleThe service's timer schedule, exactly as its definition wrote it.
outcome.detailstroptionalOn 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 #

FieldTypePresenceMeaning
graph.phasestr.enumrequiredWhen the service graph was validated.
outcome.reasonstr.enumrequiredThe warning.

Values here (open set): alive-readiness-with-hard-dependents · unfilled-role.
object.service.namestrwhen outcome.reason == alive-readiness-with-hard-dependentsThe name of the service the event is about, as its definition names it.
object.service.dependentsstr[]when outcome.reason == alive-readiness-with-hard-dependentsThe services that hard-depend on this one, by name.
graph.rolestrwhen outcome.reason == unfilled-roleThe role no service fills.
graph.servicesstr[]when outcome.reason == unfilled-roleThe 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 #

FieldTypePresenceMeaning
operation.stagestr.enumrequiredThe step peinit could not carry out.

Values here (open set): job-terminal · process-setup · lifecycle-deadline · notify.
object.service.namestroptionalThe name of the service the event is about, as its definition names it.
object.job.guidbin.guidoptionalThe job the event is about.
object.service.failedbooloptionalPresent 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 #

FieldTypePresenceMeaning
object.job.guidbin.guidrequiredThe job the event is about.
object.job.typestr.enumrequiredWhat the job is for.
object.job.statestr.enumrequiredAlways created.
object.service.namestrwhen object.job.type != submittedThe name of the service the event is about, as its definition names it.
object.job.activation-generationuintwhen object.job.type != submittedThe activation generation of the service the job belongs to, at the time the job was created.
object.operation.guidbin.guidoptionalThe operation the job serves. Absent when none asked for it, as for a submitted job.
object.job.executablestr.pathrequiredThe path of the executable the job runs.
object.job.token.sidbin.sidoptionalA 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.groupsbin.sid[]optionalPresent once peinit holds the job's token: a submitted job's at once, a service job's from peinit.job.started.
object.job.token.privilegesuint.flagsoptionalPresent exactly when object.job.token.groups is.
object.job.token.privileges-enableduint.flagsoptionalPresent 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 #

FieldTypePresenceMeaning
object.job.guidbin.guidrequiredThe job the event is about.
object.job.typestr.enumrequiredWhat the job is for.
object.job.statestr.enumrequiredThe terminal state: completed, failed or abandoned.
outcome.successboolrequiredTrue exactly when object.job.state is completed.
outcome.detailstroptionalWhy 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.namestrwhen object.job.type != submittedThe name of the service the event is about, as its definition names it.
object.job.activation-generationuintwhen object.job.type != submittedThe activation generation of the service the job belongs to, at the time the job was created.
object.operation.guidbin.guidoptionalThe 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.piduintoptionalAbsent when the process never started.
object.process.exit-codeintoptionalThe exit code of the job's process, as it passed it to exit.
object.process.exit-signalintoptionalThe number of the signal that ended the job's process.
object.job.executablestr.pathrequiredThe path of the executable the job runs.
object.job.argumentsstr[]requiredThe job's argument vector, including argv[0].
object.job.arguments-truncatedboolrequiredWhether object.job.arguments was cut to fit the record.
object.job.arguments-countuintrequiredHow many arguments the job was given, before any were cut.
object.job.created-timeuint.timerequiredWhen peinit created the job, in nanoseconds since the Unix epoch.
object.job.started-timeuint.timeoptionalWhen the job's process started, in nanoseconds since the Unix epoch.
object.job.durationuint.durationrequiredHow long the job lasted, in nanoseconds, from its creation to its end.
object.cgroup.pathstr.pathrequiredThe cgroup the event is about, as its full path under /sys/fs/cgroup/peinit.
object.cgroup.generationuintrequiredThe cgroup generation of the service the cgroup belongs to.
object.job.token.sidbin.sidoptionalAs 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.groupsbin.sid[]optionalPresent exactly when the job's process started, or it is a submitted job.
object.job.token.privilegesuint.flagsoptionalPresent exactly when object.job.token.groups is.
object.job.token.privileges-enableduint.flagsoptionalPresent 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 #

FieldTypePresenceMeaning
object.job.guidbin.guidrequiredThe 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 #

FieldTypePresenceMeaning
object.job.guidbin.guidrequiredThe job the event is about.
object.job.typestr.enumrequiredWhat the job is for.
object.job.statestr.enumrequiredAlways running.
object.service.namestrwhen object.job.type != submittedThe name of the service the event is about, as its definition names it.
object.job.activation-generationuintwhen object.job.type != submittedThe activation generation of the service the job belongs to, at the time the job was created.
object.operation.guidbin.guidoptionalThe 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.piduintrequiredThe job's process.
object.cgroup.pathstr.pathrequiredThe cgroup the job's process runs in.
object.job.token.sidbin.sidrequiredThe user SID of the token the job's process runs as.
object.job.token.groupsbin.sid[]requiredThe group SIDs of the token the job's process runs as, in the token's order.
object.job.token.privilegesuint.flagsrequiredThe 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-enableduint.flagsrequiredWhich 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 #

FieldTypePresenceMeaning
object.job.guidbin.guidrequiredThe job the event is about.
object.job.submitter.sidbin.sidrequiredThe user SID of the client that submitted the job.
notify.statusstroptionalThe text a service sent as STATUS=: free text describing what it is doing.
notify.progress.currentuintoptionalHow far a service or submitted job has got, as it reported with PROGRESS=: the count before any /.
notify.progress.totaluintoptionalThe end notify.progress.current is counting towards, as reported after the / of PROGRESS=.
notify.progress.boundedbooloptionalWhether 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.unitstr.enumoptionalWhat 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 #

FieldTypePresenceMeaning
subject.service.namestrrequiredThe service whose job sent a notification message.
subject.job.guidbin.guidrequiredThe job whose process sent a notification message: the service's main process, or a submitted job.
subject.job.activation-generationuintrequiredThe activation generation of the service whose job sent a notification message, as object.job.activation-generation describes it.
subject.operation.guidbin.guidoptionalThe peinit operation the sending job was serving when it sent a notification message, such as the start that a readiness message completes.
notify.errnoint.errnooptionalAbsent 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 #

FieldTypePresenceMeaning
subject.service.namestrrequiredThe service whose job sent a notification message.
subject.job.guidbin.guidrequiredThe job whose process sent a notification message: the service's main process, or a submitted job.
subject.job.activation-generationuintrequiredThe activation generation of the service whose job sent a notification message, as object.job.activation-generation describes it.
subject.operation.guidbin.guidoptionalThe peinit operation the sending job was serving when it sent a notification message, such as the start that a readiness message completes.
notify.exit-statusintoptionalAbsent 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 #

FieldTypePresenceMeaning
subject.service.namestrrequiredThe service whose job sent a notification message.
subject.job.guidbin.guidrequiredThe job whose process sent a notification message: the service's main process, or a submitted job.
subject.job.activation-generationuintrequiredThe activation generation of the service whose job sent a notification message, as object.job.activation-generation describes it.
subject.operation.guidbin.guidoptionalThe peinit operation the sending job was serving when it sent a notification message, such as the start that a readiness message completes.
notify.progress.currentuintoptionalHow far a service or submitted job has got, as it reported with PROGRESS=: the count before any /.
notify.progress.totaluintoptionalThe end notify.progress.current is counting towards, as reported after the / of PROGRESS=.
notify.progress.boundedbooloptionalWhether 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.unitstr.enumoptionalWhat 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 #

FieldTypePresenceMeaning
outcome.reasonstr.enumrequiredWhy 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.piduintoptionalThe sending process, when the kernel gave its credentials.
subject.service.namestroptionalThe sender's service, when peinit could attribute it.
subject.job.guidbin.guidoptionalThe job whose process sent a notification message: the service's main process, or a submitted job.
subject.job.activation-generationuintoptionalThe activation generation of the service whose job sent a notification message, as object.job.activation-generation describes it.
subject.operation.guidbin.guidoptionalThe 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 #

FieldTypePresenceMeaning
subject.service.namestrrequiredThe service whose job sent a notification message.
subject.job.guidbin.guidrequiredThe job whose process sent a notification message: the service's main process, or a submitted job.
subject.job.activation-generationuintrequiredThe activation generation of the service whose job sent a notification message, as object.job.activation-generation describes it.
subject.operation.guidbin.guidoptionalThe peinit operation the sending job was serving when it sent a notification message, such as the start that a readiness message completes.
notify.statusstrrequiredThe 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=1 is 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 #

FieldTypePresenceMeaning
subject.service.namestrrequiredThe service whose job sent a notification message.
subject.job.guidbin.guidrequiredThe job whose process sent a notification message: the service's main process, or a submitted job.
subject.job.activation-generationuintrequiredThe activation generation of the service whose job sent a notification message, as object.job.activation-generation describes it.
subject.operation.guidbin.guidoptionalThe 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 #

FieldTypePresenceMeaning
object.service.namestrrequiredThe service whose failure the chain was handling.
object.service.on-failure.namestrrequiredThe OnFailure handler peinit declined to start, because starting it would have looped or gone too deep.
object.service.on-failure-chainstr[]requiredThe OnFailure handlers already started in the chain that peinit cut short, in the order they were started.
outcome.reasonstr.enumrequiredWhy 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 #

FieldTypePresenceMeaning
object.operation.guidbin.guidrequiredThe 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.typestr.enumrequiredWhat the operation does to its service.
object.operation.sourcestr.enumrequiredWho or what asked for the operation.
object.operation.statestr.enumrequiredcompleted, failed, cancelled or aborted.
outcome.successboolrequiredTrue exactly when object.operation.state is completed.
outcome.detailstroptionalThe 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.durationuint.durationoptionalFrom the request, not from the start. Absent on a cancelled operation, which never ran.
object.service.namestrrequiredThe name of the service the event is about, as its definition names it.
subject.token.sidbin.sidoptionalThe user SID of the effective token the operation ran under.
subject.token.groupsbin.sid[]optionalThe group SIDs carried by the effective token.
subject.token.privilegesuint.flagsoptionalThe 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-enableduint.flagsoptionalWhich 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 #

FieldTypePresenceMeaning
object.operation.guidbin.guidrequiredThe 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.typestr.enumrequiredWhat the operation does to its service.
object.operation.sourcestr.enumrequiredWho or what asked for the operation.
object.operation.statestr.enumrequiredAlways merged.
object.operation.merged-into.guidbin.guidrequiredThe operation that absorbed this one.
object.service.namestrrequiredThe name of the service the event is about, as its definition names it.
subject.token.sidbin.sidoptionalThe user SID of the effective token the operation ran under.
subject.token.groupsbin.sid[]optionalThe group SIDs carried by the effective token.
subject.token.privilegesuint.flagsoptionalThe 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-enableduint.flagsoptionalWhich 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 #

FieldTypePresenceMeaning
object.operation.guidbin.guidrequiredThe 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.typestr.enumrequiredWhat the operation does to its service.
object.operation.sourcestr.enumrequiredWho or what asked for the operation.
object.operation.statestr.enumrequiredAlways pending.
object.service.namestrrequiredThe name of the service the event is about, as its definition names it.
subject.token.sidbin.sidoptionalThe client whose command created the operation. Absent for an operation peinit created itself.
subject.token.groupsbin.sid[]optionalWritten 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.privilegesuint.flagsoptionalPresent exactly when subject.token.groups is.
subject.token.privileges-enableduint.flagsoptionalPresent 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 #

FieldTypePresenceMeaning
object.operation.guidbin.guidrequiredThe 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.typestr.enumrequiredWhat the operation does to its service.
object.operation.sourcestr.enumrequiredWho or what asked for the operation.
object.operation.statestr.enumrequiredAlways running.
object.service.namestrrequiredThe name of the service the event is about, as its definition names it.
subject.token.sidbin.sidoptionalThe user SID of the effective token the operation ran under.
subject.token.groupsbin.sid[]optionalThe group SIDs carried by the effective token.
subject.token.privilegesuint.flagsoptionalThe 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-enableduint.flagsoptionalWhich 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 #

FieldTypePresenceMeaning
outcome.reasonstr.enumrequiredWhy 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.detailstroptionalWhat 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 #

FieldTypePresenceMeaning
object.service.namestrrequiredThe name of the service the event is about, as its definition names it.
object.cgroup.pathstr.pathrequiredThe cgroup the event is about, as its full path under /sys/fs/cgroup/peinit.
object.service.state-previousstr.enumrequiredThe service's state before the event, with the same values as object.service.state.
object.service.statestr.enumrequiredAlways abandoned.
object.service.transition-causestr.enumrequiredWhy the service's state changed.
object.service.generationuintrequiredThe 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 #

FieldTypePresenceMeaning
object.service.namestrrequiredThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe user SID of the effective token the operation ran under.
operation.namestr.enumrequiredWhich 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.namestroptionalThe 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.pathstr.pathwhen operation.name == remove-modified-fileThe modified file the uninstall would remove, as the package database records it: absolute within the root peipkg operated on.
object.repository.namestroptionalThe repository, for stale-trust-state and stale-index.
outcome.successboolrequiredFalse when the operator declined. For remove-modified-file, keeping the file and aborting the transaction both decline the removal.
outcome.detailstroptionalThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe user SID of the effective token the operation ran under.
transaction.iduintoptionalThe transaction the operation belonged to, registry or package.
object.claim.rolestrrequiredThe role a claim event changed the holder of.
object.claim.holderstroptionalThe package that holds object.claim.role after the event, by name.
outcome.successboolrequiredWhether the operation succeeded.
outcome.reasonstr.enumwhen outcome.success == falseWhy the operation resolved the way it did.

Values here (open set): stale · busy · denied · unowned · alternate-upgrade · unresolvable · untrusted · failed.
outcome.detailstroptionalFree 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe 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.iduintoptionalThe 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.namestroptionalAbsent only on a refused request that named no package.
object.package.versionstroptionalThe version installed. Absent on a refused request that named no version.
object.package.architecturestroptionalThe architecture of the package an event is about: an identifier of PSPU §5.8, such as x86_64, aarch64 or noarch.
source.repository.namestroptionalAbsent for an install from a local file.
outcome.successboolrequiredWhether the operation succeeded.
outcome.reasonstr.enumwhen outcome.success == falsepeipkg'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.detailstroptionalThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe user SID of the effective token the operation ran under.
transaction.iduintoptionalAbsent when the request was refused before a transaction opened.
object.package.namestrrequiredThe name of the package the event is about.
object.package.versionstroptionalThe version removed. Absent on a refused request.
outcome.successboolrequiredWhether the operation succeeded.
outcome.reasonstr.enumwhen outcome.success == falseWhy the operation resolved the way it did.

Values here (open set): stale · busy · denied · unowned · alternate-upgrade · unresolvable · untrusted · failed.
outcome.detailstroptionalFree 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe user SID of the effective token the operation ran under.
transaction.iduintoptionalAbsent when the request was refused before a transaction opened, or when a cross-root transaction failed while preparing its roots.
object.package.namestroptionalAbsent only on a refused request that named no package, such as an upgrade of everything.
object.package.versionstroptionalThe version the package moved to.
object.package.version-previousstroptionalThe version it moved from. Absent on a refused request.
object.package.architecturestroptionalThe architecture of the package an event is about: an identifier of PSPU §5.8, such as x86_64, aarch64 or noarch.
source.repository.namestroptionalAbsent for an upgrade from a local file.
outcome.successboolrequiredWhether the operation succeeded.
outcome.reasonstr.enumwhen outcome.success == falseWhy the operation resolved the way it did.

Values here (open set): stale · busy · denied · unowned · alternate-upgrade · unresolvable · untrusted · failed.
outcome.detailstroptionalFree 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe user SID of the effective token the operation ran under.
object.repository.namestrrequiredThe repository the event is about, by the name it is configured under.
object.repository.urlstrrequiredThe base URL of the repository the event is about, which is what identifies it (PSPU §5.36).
outcome.successboolrequiredWhether the operation succeeded.
outcome.reasonstr.enumwhen outcome.success == falseWhy the operation resolved the way it did.

Values here (open set): stale · busy · denied · unowned · alternate-upgrade · unresolvable · untrusted · failed.
outcome.detailstroptionalThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe user SID of the effective token the operation ran under.
object.repository.namestrrequiredThe repository the event is about, by the name it is configured under.
config.namestrrequiredThe setting's name as written in the repository file.
config.textstrwhen config.name == signature_policyA 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-previousstrwhen config.name == signature_policyThe text value a change replaced, beside config.text.
config.valueuintoptionalThe 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-previousuintoptionalThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe user SID of the effective token the operation ran under.
operation.succeeded-countuintrequiredRepositories refreshed.
operation.failed-countuintrequiredRepositories whose refresh failed.
outcome.successboolrequiredFalse 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe user SID of the effective token the operation ran under.
object.repository.namestrrequiredThe repository the event is about, by the name it is configured under.
outcome.successboolrequiredWhether the operation succeeded.
outcome.detailstroptionalThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe user SID of the effective token the operation ran under.
operation.succeeded-countuintrequiredInterrupted transactions rolled back, before the failure on a run that failed. Zero when there were none.
outcome.successboolrequiredWhether the operation succeeded.
outcome.detailstroptionalThe 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 #

FieldTypePresenceMeaning
config.key.pathstrrequiredAlways Machine\System\eventd.
config.namestrrequiredThe name of the configuration value being reported on, within config.key.path.
config.typeuint.enumoptionalAbsent when the change removed the value. Present exactly when config.value is.
config.type-previousuint.enumoptionalAbsent when the value was not set before. Present exactly when config.value-previous is.
config.valueuintoptionalThe value in force after the event.
config.value-previousuintoptionalThe 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 #

FieldTypePresenceMeaning
store.restartedboolrequiredWhether 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-countuintrequiredHow many active event-store shards eventd is running with, after resolving its configured shard count.
store.resume.cpusuint[]requiredThe CPUs whose KMES rings eventd resumed reading at startup, one entry per logical CPU in ascending order.
store.resume.sequencesuint[]requiredFor 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 #

FieldTypePresenceMeaning
store.committed.cpusuint[]optionalAbsent when eventd could not read its record of what it had committed (PEI-1394), rather than written as zeroes.
store.committed.sequencesuint[]optionalPresent 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 #

FieldTypePresenceMeaning
buffer.cpuuintrequiredThe logical CPU whose ring buffer a record concerns.
loss.sequenceuintrequiredThe first sequence number a loss covers.
loss.sequence-lastuintrequiredThe last sequence number of a gap, where loss.sequence is the first.
loss.countuintrequiredThe number of events lost in one occurrence.
loss.preceding-timeuint.timeoptionalThe 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 #

FieldTypePresenceMeaning
store.kindstr.enumrequiredWhich of eventd's stores a record is about: the sharded event store, the log store or the metric store.
store.sharduintwhen store.kind == eventAbsent for the log and metric stores, which are not sharded (PEI-1394).
outcome.detailstrrequiredThe 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 LogonStart authd 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 #

FieldTypePresenceMeaning
source.token.sidbin.sidrequiredThe originator: the principal of the process on the other end of /run/logon.sock that asked for the logon.
source.addressstr.ipoptionalThe 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-typestr.enumrequiredThe logon type the originator asked for, whether or not it was granted.
object.session.auth-packagestroptionalThe principal source authd asked to verify the credential. Absent when the logon was refused before a source was chosen.
subject.token.sidbin.sidwhen outcome.success == trueThe principal signed in: the user SID of the token minted.
object.session.iduint.luidoptionalThe 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.iduint.luidoptionalThe token minted for the originator, present exactly when object.session.id is.
outcome.successboolrequiredTrue only when the token reached the originator.
outcome.reasonstr.enumwhen outcome.success == falseThe 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 ServiceAttest authd 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe principal that asked: the user SID of the peer's token, SYSTEM for the service manager.
object.service.namestroptionalThe service the token is for, as the service manager named it. Absent only when the request named no service.
object.token.sidbin.sidoptionalThe 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.iduint.luidoptionalThe logon session created for the service, present exactly when object.token.sid is.
object.token.iduint.luidoptionalThe token minted, present exactly when object.token.sid is.
object.token.privilegesuint.flagsoptionalThe privileges local policy granted the token, all of them enabled. Present exactly when object.token.sid is.
outcome.successboolrequiredTrue only when the token reached the service manager.
outcome.reasonstr.enumwhen outcome.success == falseThe 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 SessionEnd authd 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe principal that asked: themselves, signing out, or someone the descriptor permits to end other principals' sessions.
object.session.iduint.luidrequiredThe LUID of the logon session the operation acted on.
object.session.user.sidbin.sidrequiredWhose session it was.
object.session.logon-typestr.enumrequiredWhat kind of logon the session represents, from the KACS_LOGON_TYPE_* values in uapi/pkm/token.h.
outcome.successboolrequiredTrue when no process was left holding the session.
outcome.reasonstr.enumwhen outcome.success == falseSome 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe administrator: the user SID of the token that asked.
object.account.sidbin.sidwhen outcome.success == trueThe new account.
outcome.successboolrequiredTrue once the account is durable.
outcome.reasonstr.enumwhen outcome.success == falseThe 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe administrator.
object.account.sidbin.sidoptionalThe account. Absent when no account had the name asked for.
outcome.successboolrequiredWhether the operation succeeded.
outcome.reasonstr.enumwhen outcome.success == falseinvalid 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe principal that asked: an administrator, or the account itself for set-display-name.
object.account.sidbin.sidoptionalThe account changed. Absent when no account had the name asked for.
object.group.sidbin.sidoptionalFor set-primary-group, the group made the account's primary group.
operation.namestr.enumrequiredThe 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.successboolrequiredWhether the operation succeeded.
outcome.reasonstr.enumwhen outcome.success == falsedenied 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe 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.sidbin.sidrequiredThe 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.namestr.enumrequiredWhich operation was attempted.

Values here (open set): password · ssh-key-added · ssh-key-removed.
outcome.successboolrequiredTrue once the change is durable.
outcome.reasonstr.enumwhen outcome.success == falseno-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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredlpsd itself, the principal that checked the credential: the user SID of lpsd's own token, which is its service account.
object.account.sidbin.sidoptionalThe account the credential was checked for. Absent when lpsd holds no account by the name it was asked about.
operation.namestr.enumrequiredWhat 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.successboolrequiredTrue when lpsd told authd who the principal is.
outcome.reasonstr.enumwhen outcome.success == falsenot-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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe administrator.
object.group.sidbin.sidwhen outcome.success == trueThe new group.
outcome.successboolrequiredWhether the operation succeeded.
outcome.reasonstr.enumwhen outcome.success == falseWhy 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe administrator.
object.group.sidbin.sidoptionalThe group. Absent when no group had the name asked for.
outcome.successboolrequiredWhether the operation succeeded.
outcome.reasonstr.enumwhen outcome.success == falseWhy 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe administrator.
object.account.sidbin.sidoptionalThe account. Absent when no account had the name asked for.
object.group.sidbin.sidoptionalThe group. Absent when no group had the name asked for.
outcome.successboolrequiredWhether the operation succeeded.
outcome.reasonstr.enumwhen outcome.success == falseWhy 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 #

FieldTypePresenceMeaning
subject.token.sidbin.sidrequiredThe administrator.
object.account.sidbin.sidoptionalThe account. Absent when no account had the name asked for.
object.group.sidbin.sidoptionalThe group. Absent when no group had the name asked for.
outcome.successboolrequiredWhether the operation succeeded.
outcome.reasonstr.enumwhen outcome.success == falseinvalid 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 #

FieldTypePresenceMeaning
operation.namestr.enumrequiredWhy 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.timeuint.timerequiredNever before the build's timestamp: timed refuses a step that would land there.
clock.time-previousuint.timeoptionalThe wall clock's reading just before timed moved it, the counterpart of clock.time.
subject.token.sidbin.sidrequiredThe 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 #

FieldTypePresenceMeaning
config.namestrrequiredAlways Hostname, the setting the name is held in, whether the value in force came from the registry or from a lease.
config.textstrrequiredThe name now in force, as netd passed it to the kernel.
config.text-previousstroptionalThe 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.sidbin.sidrequiredThe 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 #

FieldTypePresenceMeaning
object.certificate.digestbinrequiredThe 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.namestrrequiredThe 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.purposesstr.enum[]requiredThe 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 #

FieldTypePresenceMeaning
object.certificate.digestbinrequiredThe 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.namestrrequiredThe 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.purposesstr.enum[]requiredThe 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 #

FieldTypePresenceMeaning
object.certificate.digestbinrequiredThe 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.namestrrequiredThe 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.purposesstr.enum[]requiredThe 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 #

FieldTypePresenceMeaning
object.certificate.digestbinrequiredThe new certificate's SHA-256 digest.
object.certificate.digest-previousbinrequiredThe 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.namestrrequiredThe 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.

OffsetSizeField
04total_len
42event_type
62name_len
8name_lenname, 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.

SizeField
2path_depth
2 + n eachpath_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 typeTierFragmentSummary
kacs.audit.access.checkedessentialkacs.evmanThe record that an access check completed, and what it decided.
kacs.audit.descriptor.changedessentialkacs.evmanThe 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.usedessentialkacs.evmanThe record of what was done with a handle after it was opened.
kacs.audit.privilege.usedessentialkacs.evmanThe record that a privilege was spent.
kacs.caap.policy.changedessentialkacs.evmanThe record that a central access policy was installed, replaced or removed.
kacs.caap.sacl.skippedstandardkacs.evmanThe 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.divergedstandardkacs.evmanThe record that a staged central access policy would have decided an access differently from the policy in force.
kacs.config.value.rejectedessentialkacs.evmanThe record that KACS read one of its registry-held tables, rejected it, and carried on with what it had.
kacs.descriptor.rejectedstandardkacs.evmanThe record that KACS read an object's stored security descriptor, found it corrupt, and refused to use it.
kacs.impersonation.revertedverbosekacs.evmanThe record that a thread stopped impersonating and acts as its own token again.
kacs.impersonation.startedstandardkacs.evmanThe record that a thread took on, or tried to take on, a client's identity by impersonating its token.
kacs.mount.policy.changedessentialkacs.evmanThe record that a filesystem's KACS mount policy was set: what happens to an object on it that has no stored descriptor.
kacs.session.destroyedessentialkacs.evmanThe record that a logon session ended because its last token went away.
kacs.signature.crypto.failedessentialkacs.evmanThe record that the machinery for verifying signed executables cannot run.

A.2 Filesystem Events #

Event typeTierFragmentSummary
stratafs.file.copied-upstandardstratafs.evmanThe record that StrataFS materialised an object into a writable stratum, successful or not.
stratafs.mutation.refusedstandardstratafs.evmanThe 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 typeTierFragmentSummary
lcs.audit.backup.endedessentiallcs.evmanThe record that a registry subtree backup finished, successfully or otherwise.
lcs.audit.backup.startedessentiallcs.evmanThe record that a registry subtree backup began, emitted before any data is read.
lcs.audit.key.createdessentiallcs.evmanThe record that a registry key was created.
lcs.audit.key.deletedessentiallcs.evmanThe record that a registry key was deleted from a layer through its own handle, or that an attempt failed.
lcs.audit.key.descriptor.changedessentiallcs.evmanThe record that a registry key's security descriptor was changed through a key handle, or that an attempt failed.
lcs.audit.key.hiddenessentiallcs.evmanThe record that a registry key was hidden in a layer through its own handle, or that an attempt failed.
lcs.audit.key.openedessentiallcs.evmanThe record that a registry key was opened and what access the open received.
lcs.audit.key.tombstonedessentiallcs.evmanThe record that a key's blanket tombstone was set or cleared in a layer, or that an attempt failed.
lcs.audit.restore.endedessentiallcs.evmanThe record that a registry subtree restore finished, successfully or otherwise.
lcs.audit.restore.startedessentiallcs.evmanThe record that a registry subtree restore began, emitted before any state is modified.
lcs.audit.transaction.committedessentiallcs.evmanThe record of how a registry transaction ended, written for a transaction that staged at least one write some other record covers.
lcs.audit.value.deletedessentiallcs.evmanThe record that a registry value was deleted through a key handle, or that an attempt to delete one failed.
lcs.audit.value.setessentiallcs.evmanThe record that a registry value was written through a key handle, or that an attempt to write one failed.
lcs.config.value.rejectedessentiallcs.evmanThe record that LCS read one of its own configuration values, rejected it, and carried on with what it had.
lcs.source.response.rejectedessentiallcs.evmanThe record that LCS rejected data a registry source sent it.

A.4 Event Stream Events #

Event typeTierFragmentSummary
kmes.buffer.swap.failedstandardkmes.evmanThe record that KMES could not resize its per-CPU ring buffers and kept the capacity it had.
kmes.config.appliedstandardkmes.evmanThe record that KMES read its configuration key and committed what it found.
kmes.config.refresh.failedessentialkmes.evmanThe record that KMES could not read one of its configuration keys and kept running on what it had.
kmes.config.value.rejectedessentialkmes.evmanThe record that KMES read a configuration value, rejected it, and carried on with what it had.

A.5 Network Policy Events #

Event typeTierFragmentSummary
ntfe.policy.publishedstandardntfe.evmanThe record that a new generation of traffic rules went into force.
ntfe.policy.rejectedessentialntfe.evmanThe 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.reportedessentialntfe.evmanThe record of a traffic policy decision that a rule explicitly asked to be reported.

A.6 Service Events #

Event typeTierFragmentSummary
peinit.boot.downgradedessentialpeinit.evmanA full boot was downgraded to safe mode: the machine is running a reduced service set.
peinit.cgroup.leakedstandardpeinit.evmanA cgroup could not be reclaimed: its processes do not respond to the kernel, and the cgroup is leaked.
peinit.config.reload.appliedstandardpeinit.evmanA 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.deferredverbosepeinit.evmanA configuration reload during the boot window changed definitions of services the boot plan had not yet launched.
peinit.critical-service.failedessentialpeinit.evmanA service marked Critical failed, and peinit took the machine to its reboot final action.
peinit.event.droppedessentialpeinit.evmanThe ring refused one of peinit's events, and it is gone.
peinit.fd-store.rejectedstandardpeinit.evmanpeinit refused a file descriptor a service asked it to keep.
peinit.graph.operation.endedverbosepeinit.evmanAn 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.failedstandardpeinit.evmanThe service graph failed validation for the reason in outcome.reason.
peinit.graph.validation.warnedstandardpeinit.evmanThe service graph passed validation with a warning: nothing was refused, but something will not behave as its author probably expects.
peinit.internal-error.containedstandardpeinit.evmanpeinit could not carry out a step on a service's behalf, and contained the fault to that service.
peinit.job.createdverbosepeinit.evmanA job exists: peinit has decided to run a process and recorded it, before the process exists.
peinit.job.endedstandardpeinit.evmanA job is over: its process exited or was killed, or the job ended without its process ever starting.
peinit.job.output.droppedstandardpeinit.evmanA submitted job's submitter stopped draining its output, and peinit began dropping the submitter's copy of the job's lines.
peinit.job.startedstandardpeinit.evmanA 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.reportedverbosepeinit.evmanA submitted job reported STATUS= or PROGRESS= on the notification channel, and this is what peinit retained after it.
peinit.notify.errno.reportedstandardpeinit.evmanA service's job sent ERRNO=: its own claim about an error, which peinit neither acts on nor retains.
peinit.notify.exit-status.reportedstandardpeinit.evmanA service's job sent EXIT_STATUS=, informationally; it is not the exit of any process.
peinit.notify.progress.reportedverbosepeinit.evmanA service's job sent PROGRESS= or PROGRESS_UNIT=, and this is the progress peinit retained after it.
peinit.notify.rejectedstandardpeinit.evmanpeinit 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.reportedverbosepeinit.evmanA service's job sent STATUS= on the notification channel.
peinit.notify.stopping.reportedstandardpeinit.evmanA service's job sent STOPPING=1, so peinit will not send it SIGTERM.
peinit.on-failure.suppressedstandardpeinit.evmanpeinit 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.endedstandardpeinit.evmanAn operation reached a terminal state other than merged: it completed, failed, was cancelled while still pending, or was aborted while running.
peinit.operation.mergedverbosepeinit.evmanAn 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.requestedstandardpeinit.evmanAn operation on a service — a start, stop, restart, reload or reset — was created.
peinit.operation.startedverbosepeinit.evmanAn operation left the queue and began to run.
peinit.recovery.enteredessentialpeinit.evmanpeinit dropped the machine to a recovery shell, for the reason in outcome.reason.
peinit.service.abandonedstandardpeinit.evmanA 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-outstandardpeinit.evmanA 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 typeTierFragmentSummary
peipkg.action.authorisedessentialpeipkg.evmanThe record that the operator was asked to authorise an elevated action specifically, apart from the routine proceed prompt, and what they answered.
peipkg.claim.changedstandardpeipkg.evmanThe record that peipkg granted a role to a package or revoked it, or tried to and failed.
peipkg.package.installedessentialpeipkg.evmanThe record that peipkg installed a package, or tried to and failed.
peipkg.package.uninstalledessentialpeipkg.evmanThe record that peipkg removed a package, or tried to and failed.
peipkg.package.upgradedessentialpeipkg.evmanThe record that peipkg moved an installed package to another version, or tried to and failed.
peipkg.repository.addedessentialpeipkg.evmanThe record that peipkg added a repository and established trust in it, or tried to and failed.
peipkg.repository.reconfiguredessentialpeipkg.evmanThe record that a repository add changed one of an existing repository's trust-relevant settings.
peipkg.repository.refreshedstandardpeipkg.evmanThe record that peipkg refreshed the metadata of its configured repositories.
peipkg.repository.removedessentialpeipkg.evmanThe record that peipkg removed a repository from its configuration and database, or tried to and failed.
peipkg.transaction.recoveredstandardpeipkg.evmanThe record that peipkg recover reconciled interrupted transactions, rolling back each one it found pending, or failed to.

A.8 Event Store Events #

Event typeTierFragmentSummary
eventd.config.changedessentialeventd.evmanThe record that eventd applied a change to one of its configuration values at runtime.
eventd.daemon.startedessentialeventd.evmanThe record that eventd started, attached to KMES and decided where to resume reading each CPU's ring.
eventd.daemon.stoppedessentialeventd.evmanThe record that eventd shut down gracefully, written after it stopped reading the rings and committed everything it had read.
eventd.events.lostessentialeventd.evmanThe record that eventd found events missing from a CPU's ring: sequence numbers it never read and never stored.
eventd.store.quarantinedessentialeventd.evmanThe 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 typeTierFragmentSummary
authd.logon.attemptedessentialauthd.evmanThe record that something asked authd to sign a principal in, and whether it did.
authd.service.attestedstandardauthd.evmanThe record that the service manager asked authd for a service's token, with no credential behind it, and whether authd minted one.
authd.session.endedessentialauthd.evmanThe 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 typeTierFragmentSummary
lpsd.account.createdessentiallpsd.evmanThe record that an administrator created a local account with lps, or tried to.
lpsd.account.deletedessentiallpsd.evmanThe record that an administrator deleted a local account with lps, or tried to.
lpsd.account.modifiedessentiallpsd.evmanThe record that an account was changed: by an administrator with lps, or by the principal themselves where the self socket allows it.
lpsd.credential.changedessentiallpsd.evmanThe 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.verifiedstandardlpsd.evmanThe record that lpsd checked a credential for a logon authd relayed to it, and what it found.
lpsd.group.createdessentiallpsd.evmanThe record that an administrator created a local group with lps, or tried to.
lpsd.group.deletedessentiallpsd.evmanThe record that an administrator deleted a local group with lps, or tried to.
lpsd.group.member.addedessentiallpsd.evmanThe record that an administrator added an account to a group with lps, or tried to.
lpsd.group.member.removedessentiallpsd.evmanThe record that an administrator removed an account from a group with lps, or tried to.

A.11 Time Events #

Event typeTierFragmentSummary
timed.clock.steppedessentialtimed.evmantimed moved the system clock at once by an arbitrary amount, rather than adjusting its rate.

A.12 Network Configuration Events #

Event typeTierFragmentSummary
netd.hostname.changedessentialnetd.evmannetd changed the machine's hostname.

A.13 Trust Events #

Event typeTierFragmentSummary
trustd.root.addedstandardtrustd.evmanA root certificate authority entered the set this machine trusts.
trustd.root.distrustedstandardtrustd.evmanA distrust took a root certificate authority out of the set this machine trusts.
trustd.root.removedstandardtrustd.evmanA 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 typeTierFragmentSummary
gxwid.certificate.renewedessentialgxwid.evmangxwid 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.changed
  • kacs.audit.handle.used
  • lcs.audit.key.deleted
  • lcs.audit.key.descriptor.changed
  • lcs.audit.key.hidden
  • lcs.audit.key.tombstoned
  • lcs.audit.value.deleted
  • lcs.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:

  • kacs.audit.access.checked

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:

  • kacs.audit.access.checked

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.checked
  • kacs.audit.descriptor.changed
  • kacs.audit.handle.used
  • kacs.audit.privilege.used
  • kacs.caap.sacl.skipped
  • kacs.caap.staging.diverged
  • lcs.audit.key.created
  • lcs.audit.key.deleted
  • lcs.audit.key.descriptor.changed
  • lcs.audit.key.hidden
  • lcs.audit.key.opened
  • lcs.audit.key.tombstoned
  • lcs.audit.value.deleted
  • lcs.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:

  • kacs.caap.sacl.skipped
  • kacs.caap.staging.diverged

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.changed
  • kacs.audit.handle.used
  • lcs.audit.key.deleted
  • lcs.audit.key.descriptor.changed
  • lcs.audit.key.hidden
  • lcs.audit.key.tombstoned
  • lcs.audit.value.deleted
  • lcs.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.checked
  • kacs.audit.descriptor.changed
  • kacs.audit.handle.used
  • kacs.audit.privilege.used
  • kacs.caap.sacl.skipped
  • kacs.caap.staging.diverged
  • lcs.audit.key.created
  • lcs.audit.key.deleted
  • lcs.audit.key.descriptor.changed
  • lcs.audit.key.hidden
  • lcs.audit.key.opened
  • lcs.audit.key.tombstoned
  • lcs.audit.value.deleted
  • lcs.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:

  • kmes.buffer.swap.failed
  • kmes.config.applied

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:

  • kmes.buffer.swap.failed

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:

  • eventd.events.lost

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:

  • kacs.caap.sacl.skipped

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:

  • kacs.caap.policy.changed
  • kacs.caap.sacl.skipped

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:

  • kacs.caap.sacl.skipped

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:

  • timed.clock.stepped

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:

  • timed.clock.stepped

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:

  • kmes.config.applied

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:

  • kmes.config.applied

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:

  • kmes.config.applied

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:

  • kmes.config.applied

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:

  • kmes.config.value.rejected
  • lcs.config.value.rejected

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:

  • kmes.config.value.rejected
  • lcs.config.value.rejected

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:

  • kmes.config.value.rejected
  • lcs.config.value.rejected

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.changed
  • kacs.config.value.rejected
  • kmes.config.applied
  • kmes.config.refresh.failed
  • kmes.config.value.rejected
  • lcs.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.changed
  • kacs.config.value.rejected
  • kmes.config.value.rejected
  • lcs.config.value.rejected
  • netd.hostname.changed
  • peipkg.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:

  • kmes.config.value.rejected
  • lcs.config.value.rejected

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:

  • kmes.config.value.rejected
  • lcs.config.value.rejected

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:

  • kmes.config.value.rejected
  • lcs.config.value.rejected

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:

  • netd.hostname.changed
  • peipkg.repository.reconfigured

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:

  • netd.hostname.changed
  • peipkg.repository.reconfigured

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:

  • eventd.config.changed

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:

  • eventd.config.changed

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.changed
  • kmes.config.value.rejected
  • lcs.config.value.rejected
  • peipkg.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:

  • eventd.config.changed
  • peipkg.repository.reconfigured

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:

  • ntfe.verdict.reported

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:

  • ntfe.verdict.reported

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:

  • stratafs.file.copied-up

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:

  • stratafs.file.copied-up

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:

  • peinit.event.dropped

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:

  • kmes.config.applied

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:

  • peinit.event.dropped

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 (via subject)
  • kacs.audit.descriptor.changed (via subject)
  • kacs.audit.handle.used (via subject)
  • kacs.audit.privilege.used (via subject)
  • kacs.caap.policy.changed (via subject)
  • kacs.caap.sacl.skipped (via subject)
  • kacs.caap.staging.diverged (via subject)
  • kacs.descriptor.rejected (via subject)
  • kacs.impersonation.reverted (via subject)
  • kacs.impersonation.started (via subject)
  • kacs.mount.policy.changed (via subject)

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 (via subject)
  • kacs.audit.descriptor.changed (via subject)
  • kacs.audit.handle.used (via subject)
  • kacs.audit.privilege.used (via subject)
  • kacs.caap.policy.changed (via subject)
  • kacs.caap.sacl.skipped (via subject)
  • kacs.caap.staging.diverged (via subject)
  • kacs.descriptor.rejected (via subject)
  • kacs.impersonation.reverted (via subject)
  • kacs.impersonation.started (via subject)
  • kacs.mount.policy.changed (via subject)

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 (via subject)
  • kacs.audit.descriptor.changed (via subject)
  • kacs.audit.handle.used (via subject)
  • kacs.audit.privilege.used (via subject)
  • kacs.caap.policy.changed (via subject)
  • kacs.caap.sacl.skipped (via subject)
  • kacs.caap.staging.diverged (via subject)
  • kacs.descriptor.rejected (via subject)
  • kacs.impersonation.reverted (via subject)
  • kacs.impersonation.started (via subject)
  • kacs.mount.policy.changed (via subject)

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:

  • kacs.impersonation.reverted
  • kacs.impersonation.started

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.checked
  • kacs.audit.privilege.used
  • kacs.caap.sacl.skipped
  • kacs.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:

  • ntfe.verdict.reported

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:

  • peinit.graph.operation.ended

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:

  • peinit.config.reload.applied

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:

  • peinit.config.reload.applied

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:

  • peinit.config.reload.applied

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:

  • peinit.config.reload.applied

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:

  • peinit.config.reload.applied

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:

  • peinit.config.reload.applied

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:

  • peinit.graph.validation.failed
  • peinit.graph.validation.warned

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:

  • peinit.graph.validation.warned

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.downgraded
  • peinit.config.reload.applied
  • peinit.config.reload.deferred
  • peinit.graph.validation.failed
  • peinit.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:

  • kacs.audit.privilege.used

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:

  • eventd.events.lost

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:

  • eventd.events.lost

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:

  • eventd.events.lost

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:

  • eventd.events.lost

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:

  • lcs.audit.key.hidden
  • lcs.audit.key.tombstoned
  • lcs.audit.value.set

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:

  • lcs.audit.value.set

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:

  • ntfe.verdict.reported

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:

  • ntfe.verdict.reported

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:

  • ntfe.verdict.reported

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:

  • ntfe.verdict.reported

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:

  • ntfe.verdict.reported

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:

  • ntfe.verdict.reported

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:

  • ntfe.verdict.reported

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:

  • peinit.notify.errno.reported

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:

  • peinit.notify.exit-status.reported

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:

  • peinit.job.status.reported
  • peinit.notify.progress.reported

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:

  • peinit.job.status.reported
  • peinit.notify.progress.reported

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:

  • peinit.job.status.reported
  • peinit.notify.progress.reported

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:

  • peinit.job.status.reported
  • peinit.notify.progress.reported

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:

  • peinit.job.status.reported
  • peinit.notify.status.reported

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.created
  • lpsd.account.deleted
  • lpsd.account.modified
  • lpsd.credential.changed
  • lpsd.credential.verified
  • lpsd.group.member.added
  • lpsd.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:

  • gxwid.certificate.renewed
  • trustd.root.added
  • trustd.root.distrusted
  • trustd.root.removed

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:

  • gxwid.certificate.renewed

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:

  • gxwid.certificate.renewed
  • trustd.root.added
  • trustd.root.distrusted
  • trustd.root.removed

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:

  • trustd.root.added
  • trustd.root.distrusted
  • trustd.root.removed

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:

  • peinit.job.ended

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:

  • peinit.cgroup.leaked
  • peinit.job.ended
  • peinit.job.started
  • peinit.service.abandoned

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:

  • peinit.cgroup.leaked

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:

  • peipkg.claim.changed

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:

  • peipkg.claim.changed

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:

  • peinit.fd-store.rejected

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:

  • kacs.descriptor.rejected

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:

  • kacs.descriptor.rejected

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.checked
  • kacs.audit.descriptor.changed
  • kacs.audit.handle.used
  • kacs.audit.privilege.used
  • kacs.caap.sacl.skipped
  • kacs.caap.staging.diverged
  • kacs.descriptor.rejected
  • peipkg.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:

  • stratafs.file.copied-up
  • stratafs.mutation.refused

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.modified
  • lpsd.group.created
  • lpsd.group.deleted
  • lpsd.group.member.added
  • lpsd.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:

  • kacs.audit.descriptor.changed

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:

  • kacs.audit.descriptor.changed

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:

  • peinit.job.created
  • peinit.job.ended
  • peinit.job.started

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:

  • peinit.job.ended

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:

  • peinit.job.ended

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:

  • peinit.job.ended

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:

  • peinit.job.ended

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:

  • peinit.job.ended

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:

  • peinit.job.created
  • peinit.job.ended

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.leaked
  • peinit.event.dropped
  • peinit.internal-error.contained
  • peinit.job.created
  • peinit.job.ended
  • peinit.job.output.dropped
  • peinit.job.started
  • peinit.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:

  • peinit.job.ended

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:

  • peinit.job.created
  • peinit.job.ended
  • peinit.job.started

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:

  • peinit.job.status.reported

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:

  • peinit.job.created
  • peinit.job.ended
  • peinit.job.started

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:

  • peinit.job.created
  • peinit.job.ended
  • peinit.job.started

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:

  • peinit.job.created
  • peinit.job.ended
  • peinit.job.started

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:

  • peinit.job.created
  • peinit.job.ended
  • peinit.job.started

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:

  • peinit.job.created
  • peinit.job.ended
  • peinit.job.started

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:

  • lcs.audit.key.created

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.ended
  • lcs.audit.backup.started
  • lcs.audit.key.created
  • lcs.audit.key.deleted
  • lcs.audit.key.descriptor.changed
  • lcs.audit.key.hidden
  • lcs.audit.key.opened
  • lcs.audit.key.tombstoned
  • lcs.audit.restore.ended
  • lcs.audit.restore.started
  • lcs.audit.value.deleted
  • lcs.audit.value.set
  • lcs.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.created
  • lcs.audit.key.deleted
  • lcs.audit.key.hidden
  • lcs.audit.key.tombstoned
  • lcs.audit.value.deleted
  • lcs.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.created
  • lcs.audit.key.deleted
  • lcs.audit.key.descriptor.changed
  • lcs.audit.key.hidden
  • lcs.audit.key.tombstoned
  • lcs.audit.value.deleted
  • lcs.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:

  • lcs.audit.key.created

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:

  • lcs.audit.value.set

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:

  • lcs.audit.value.deleted
  • lcs.audit.value.set

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:

  • lcs.audit.value.set

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:

  • lcs.audit.value.deleted
  • lcs.audit.value.set

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:

  • lcs.audit.value.deleted
  • lcs.audit.value.set

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:

  • lcs.audit.value.set

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:

  • lcs.audit.value.deleted
  • lcs.audit.value.set

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:

  • lcs.audit.key.created

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:

  • lcs.audit.key.created

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.checked
  • kacs.audit.descriptor.changed
  • kacs.audit.handle.used
  • kacs.audit.privilege.used
  • kacs.caap.sacl.skipped
  • kacs.caap.staging.diverged
  • kacs.descriptor.rejected
  • kacs.impersonation.reverted
  • kacs.impersonation.started
  • lcs.audit.key.created
  • lcs.audit.key.deleted
  • lcs.audit.key.descriptor.changed
  • lcs.audit.key.hidden
  • lcs.audit.key.opened
  • lcs.audit.key.tombstoned
  • lcs.audit.value.deleted
  • lcs.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:

  • lcs.audit.key.deleted

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:

  • kacs.mount.policy.changed

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:

  • kacs.mount.policy.changed

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:

  • kacs.mount.policy.changed

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:

  • kacs.mount.policy.changed

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:

  • peinit.operation.ended

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.ended
  • peinit.job.created
  • peinit.job.ended
  • peinit.job.started
  • peinit.operation.ended
  • peinit.operation.merged
  • peinit.operation.requested
  • peinit.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:

  • peinit.operation.merged

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:

  • peinit.operation.ended
  • peinit.operation.merged
  • peinit.operation.requested
  • peinit.operation.started

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:

  • peinit.operation.ended
  • peinit.operation.merged
  • peinit.operation.requested
  • peinit.operation.started

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:

  • peinit.operation.ended
  • peinit.operation.merged
  • peinit.operation.requested
  • peinit.operation.started

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:

  • peipkg.package.installed
  • peipkg.package.upgraded

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.authorised
  • peipkg.package.installed
  • peipkg.package.uninstalled
  • peipkg.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:

  • peipkg.package.installed
  • peipkg.package.uninstalled
  • peipkg.package.upgraded

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:

  • peipkg.package.upgraded

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:

  • peinit.job.ended

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:

  • peinit.job.ended

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.checked
  • kacs.audit.descriptor.changed
  • kacs.audit.privilege.used
  • kacs.caap.sacl.skipped
  • kacs.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.checked
  • kacs.audit.privilege.used
  • kacs.caap.sacl.skipped
  • kacs.caap.staging.diverged
  • peinit.job.ended
  • peinit.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.authorised
  • peipkg.repository.added
  • peipkg.repository.reconfigured
  • peipkg.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:

  • peipkg.repository.added

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:

  • kacs.audit.descriptor.changed
  • lcs.audit.key.descriptor.changed

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:

  • kacs.audit.descriptor.changed
  • lcs.audit.key.descriptor.changed

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:

  • kacs.audit.descriptor.changed
  • lcs.audit.key.descriptor.changed

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.changed
  • kacs.descriptor.rejected
  • lcs.audit.key.created
  • lcs.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:

  • kacs.audit.descriptor.changed
  • lcs.audit.key.descriptor.changed

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:

  • kacs.audit.descriptor.changed
  • lcs.audit.key.created
  • lcs.audit.key.descriptor.changed

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:

  • kacs.audit.descriptor.changed
  • lcs.audit.key.descriptor.changed

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:

  • peinit.boot.downgraded
  • peinit.graph.validation.failed

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:

  • peinit.graph.validation.failed

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:

  • peinit.graph.validation.failed

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:

  • peinit.graph.validation.warned

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:

  • peinit.internal-error.contained

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:

  • peinit.service.abandoned

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:

  • peinit.graph.validation.failed

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:

  • peinit.graph.validation.failed

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:

  • peinit.graph.validation.failed

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.attested
  • peinit.boot.downgraded
  • peinit.cgroup.leaked
  • peinit.critical-service.failed
  • peinit.event.dropped
  • peinit.fd-store.rejected
  • peinit.graph.operation.ended
  • peinit.graph.validation.failed
  • peinit.graph.validation.warned
  • peinit.internal-error.contained
  • peinit.job.created
  • peinit.job.ended
  • peinit.job.started
  • peinit.on-failure.suppressed
  • peinit.operation.ended
  • peinit.operation.merged
  • peinit.operation.requested
  • peinit.operation.started
  • peinit.service.abandoned
  • peinit.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:

  • peinit.on-failure.suppressed

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:

  • peinit.on-failure.suppressed

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:

  • peinit.service.abandoned

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:

  • peinit.service.abandoned

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:

  • peinit.graph.validation.failed

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:

  • peinit.service.abandoned

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:

  • peinit.graph.validation.failed

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:

  • authd.logon.attempted
  • kacs.session.destroyed

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:

  • authd.logon.attempted
  • authd.service.attested
  • authd.session.ended
  • kacs.session.destroyed

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:

  • kacs.session.destroyed

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:

  • authd.logon.attempted
  • authd.session.ended
  • kacs.session.destroyed

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:

  • authd.session.ended
  • kacs.session.destroyed

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:

  • kacs.impersonation.started

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.checked
  • kacs.audit.descriptor.changed
  • kacs.audit.privilege.used
  • kacs.caap.sacl.skipped
  • kacs.caap.staging.diverged
  • kacs.impersonation.reverted
  • kacs.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.attempted
  • authd.service.attested
  • kacs.audit.access.checked
  • kacs.audit.descriptor.changed
  • kacs.audit.privilege.used
  • kacs.caap.sacl.skipped
  • kacs.caap.staging.diverged
  • kacs.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:

  • kacs.impersonation.started

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:

  • kacs.impersonation.started

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:

  • kacs.impersonation.started

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:

  • authd.service.attested

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:

  • kacs.impersonation.started

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:

  • authd.service.attested
  • kacs.impersonation.reverted
  • kacs.impersonation.started

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:

  • kacs.impersonation.started

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:

  • peipkg.repository.refreshed

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:

  • lcs.audit.backup.started
  • lcs.audit.restore.started

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.used
  • kacs.audit.privilege.used
  • kacs.caap.policy.changed
  • kacs.impersonation.reverted
  • lcs.audit.key.tombstoned
  • lpsd.account.modified
  • lpsd.credential.changed
  • lpsd.credential.verified
  • peipkg.action.authorised
  • stratafs.mutation.refused
  • timed.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:

  • peinit.internal-error.contained

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:

  • peipkg.repository.refreshed
  • peipkg.transaction.recovered

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:

  • stratafs.mutation.refused

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.quarantined
  • peinit.config.reload.applied
  • peinit.critical-service.failed
  • peinit.graph.validation.failed
  • peinit.job.ended
  • peinit.operation.ended
  • peinit.recovery.entered
  • peipkg.action.authorised
  • peipkg.claim.changed
  • peipkg.package.installed
  • peipkg.package.uninstalled
  • peipkg.package.upgraded
  • peipkg.repository.added
  • peipkg.repository.removed
  • peipkg.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.changed
  • kacs.caap.policy.changed
  • kacs.config.value.rejected
  • kacs.impersonation.reverted
  • kacs.impersonation.started
  • kacs.signature.crypto.failed
  • kmes.buffer.swap.failed
  • kmes.config.refresh.failed
  • lcs.audit.backup.ended
  • lcs.audit.key.deleted
  • lcs.audit.key.descriptor.changed
  • lcs.audit.key.hidden
  • lcs.audit.key.tombstoned
  • lcs.audit.restore.ended
  • lcs.audit.transaction.committed
  • lcs.audit.value.deleted
  • lcs.audit.value.set
  • ntfe.policy.rejected
  • peinit.event.dropped
  • stratafs.file.copied-up
  • stratafs.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.attempted
  • authd.service.attested
  • authd.session.ended
  • kacs.audit.handle.used
  • kacs.caap.sacl.skipped
  • kacs.config.value.rejected
  • kacs.descriptor.rejected
  • kacs.impersonation.started
  • lcs.audit.transaction.committed
  • lcs.source.response.rejected
  • lpsd.account.created
  • lpsd.account.deleted
  • lpsd.account.modified
  • lpsd.credential.changed
  • lpsd.credential.verified
  • lpsd.group.created
  • lpsd.group.deleted
  • lpsd.group.member.added
  • lpsd.group.member.removed
  • ntfe.policy.rejected
  • ntfe.verdict.reported
  • peinit.boot.downgraded
  • peinit.critical-service.failed
  • peinit.fd-store.rejected
  • peinit.graph.validation.failed
  • peinit.graph.validation.warned
  • peinit.notify.rejected
  • peinit.on-failure.suppressed
  • peinit.recovery.entered
  • peipkg.claim.changed
  • peipkg.package.installed
  • peipkg.package.uninstalled
  • peipkg.package.upgraded
  • peipkg.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.attempted
  • authd.service.attested
  • authd.session.ended
  • kacs.audit.access.checked
  • kacs.audit.descriptor.changed
  • kacs.audit.handle.used
  • kacs.audit.privilege.used
  • kacs.caap.policy.changed
  • kacs.impersonation.reverted
  • kacs.impersonation.started
  • kacs.mount.policy.changed
  • lcs.audit.backup.ended
  • lcs.audit.key.created
  • lcs.audit.key.deleted
  • lcs.audit.key.descriptor.changed
  • lcs.audit.key.hidden
  • lcs.audit.key.opened
  • lcs.audit.key.tombstoned
  • lcs.audit.restore.ended
  • lcs.audit.transaction.committed
  • lcs.audit.value.deleted
  • lcs.audit.value.set
  • lpsd.account.created
  • lpsd.account.deleted
  • lpsd.account.modified
  • lpsd.credential.changed
  • lpsd.credential.verified
  • lpsd.group.created
  • lpsd.group.deleted
  • lpsd.group.member.added
  • lpsd.group.member.removed
  • peinit.config.reload.applied
  • peinit.graph.operation.ended
  • peinit.job.ended
  • peinit.operation.ended
  • peipkg.action.authorised
  • peipkg.claim.changed
  • peipkg.package.installed
  • peipkg.package.uninstalled
  • peipkg.package.upgraded
  • peipkg.repository.added
  • peipkg.repository.refreshed
  • peipkg.repository.removed
  • peipkg.transaction.recovered
  • stratafs.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:

  • ntfe.verdict.reported

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:

  • kacs.config.value.rejected

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:

  • ntfe.policy.published
  • ntfe.policy.rejected
  • ntfe.verdict.reported

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:

  • ntfe.policy.published

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:

  • ntfe.policy.published

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:

  • kacs.config.value.rejected
  • ntfe.policy.rejected

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:

  • ntfe.policy.published

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:

  • ntfe.policy.published

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:

  • kacs.audit.privilege.used

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:

  • kacs.impersonation.started

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:

  • kacs.audit.privilege.used
  • kacs.impersonation.started

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:

  • kacs.audit.privilege.used

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:

  • lcs.source.response.rejected

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:

  • lcs.source.response.rejected

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.deleted
  • lcs.audit.key.descriptor.changed
  • lcs.audit.key.hidden
  • lcs.audit.key.tombstoned
  • lcs.audit.value.deleted
  • lcs.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:

  • ntfe.policy.rejected

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:

  • ntfe.verdict.reported

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:

  • ntfe.policy.rejected
  • ntfe.verdict.reported

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:

  • ntfe.policy.rejected
  • ntfe.verdict.reported

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:

  • ntfe.policy.rejected
  • ntfe.verdict.reported

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:

  • ntfe.verdict.reported

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:

  • ntfe.verdict.reported

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:

  • peinit.critical-service.failed

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:

  • kacs.signature.crypto.failed

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:

  • authd.logon.attempted
  • ntfe.verdict.reported

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:

  • ntfe.verdict.reported

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:

  • peipkg.package.installed
  • peipkg.package.upgraded

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:

  • lcs.source.response.rejected

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:

  • lcs.source.response.rejected

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:

  • stratafs.file.copied-up
  • stratafs.mutation.refused

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:

  • stratafs.file.copied-up
  • stratafs.mutation.refused

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:

  • authd.logon.attempted

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:

  • eventd.daemon.stopped

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:

  • eventd.daemon.stopped

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:

  • eventd.store.quarantined

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:

  • eventd.daemon.started

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:

  • eventd.daemon.started

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:

  • eventd.daemon.started

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:

  • eventd.store.quarantined

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:

  • eventd.daemon.started

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.reported
  • peinit.notify.exit-status.reported
  • peinit.notify.progress.reported
  • peinit.notify.rejected
  • peinit.notify.status.reported
  • peinit.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.reported
  • peinit.notify.exit-status.reported
  • peinit.notify.progress.reported
  • peinit.notify.rejected
  • peinit.notify.status.reported
  • peinit.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.reported
  • peinit.notify.exit-status.reported
  • peinit.notify.progress.reported
  • peinit.notify.rejected
  • peinit.notify.status.reported
  • peinit.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 (via subject)
  • kacs.audit.descriptor.changed (via subject)
  • kacs.audit.handle.used (via subject)
  • kacs.audit.privilege.used (via subject)
  • kacs.caap.policy.changed (via subject)
  • kacs.caap.sacl.skipped (via subject)
  • kacs.caap.staging.diverged (via subject)
  • kacs.descriptor.rejected (via subject)
  • kacs.impersonation.reverted (via subject)
  • kacs.impersonation.started (via subject)
  • kacs.mount.policy.changed (via subject)

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 (via subject)
  • kacs.audit.descriptor.changed (via subject)
  • kacs.audit.handle.used (via subject)
  • kacs.audit.privilege.used (via subject)
  • kacs.caap.policy.changed (via subject)
  • kacs.caap.sacl.skipped (via subject)
  • kacs.caap.staging.diverged (via subject)
  • kacs.descriptor.rejected (via subject)
  • kacs.impersonation.reverted (via subject)
  • kacs.impersonation.started (via subject)
  • kacs.mount.policy.changed (via subject)

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:

  • peinit.notify.rejected

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.reported
  • peinit.notify.exit-status.reported
  • peinit.notify.progress.reported
  • peinit.notify.rejected
  • peinit.notify.status.reported
  • peinit.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 (via subject)
  • kacs.audit.descriptor.changed (via subject)
  • kacs.audit.handle.used (via subject)
  • kacs.audit.privilege.used (via subject)
  • kacs.caap.policy.changed (via subject)
  • kacs.caap.sacl.skipped (via subject)
  • kacs.caap.staging.diverged (via subject)
  • kacs.descriptor.rejected (via subject)
  • kacs.impersonation.reverted (via subject)
  • kacs.impersonation.started (via subject)
  • kacs.mount.policy.changed (via subject)
  • lcs.audit.backup.ended (via caller)
  • lcs.audit.backup.started (via caller)
  • lcs.audit.key.created (via caller)
  • lcs.audit.key.deleted (via caller)
  • lcs.audit.key.descriptor.changed (via caller)
  • lcs.audit.key.hidden (via caller)
  • lcs.audit.key.opened (via caller)
  • lcs.audit.key.tombstoned (via caller)
  • lcs.audit.restore.ended (via caller)
  • lcs.audit.restore.started (via caller)
  • lcs.audit.transaction.committed (via caller)
  • lcs.audit.value.deleted (via caller)
  • lcs.audit.value.set (via caller)

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 (via subject)
  • kacs.audit.descriptor.changed (via subject)
  • kacs.audit.handle.used (via subject)
  • kacs.audit.privilege.used (via subject)
  • kacs.caap.policy.changed (via subject)
  • kacs.caap.sacl.skipped (via subject)
  • kacs.caap.staging.diverged (via subject)
  • kacs.descriptor.rejected (via subject)
  • kacs.impersonation.reverted (via subject)
  • kacs.impersonation.started (via subject)
  • kacs.mount.policy.changed (via subject)

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 (via subject)
  • kacs.audit.descriptor.changed (via subject)
  • kacs.audit.handle.used (via subject)
  • kacs.audit.privilege.used (via subject)
  • kacs.caap.policy.changed (via subject)
  • kacs.caap.sacl.skipped (via subject)
  • kacs.caap.staging.diverged (via subject)
  • kacs.descriptor.rejected (via subject)
  • kacs.impersonation.reverted (via subject)
  • kacs.impersonation.started (via subject)
  • kacs.mount.policy.changed (via subject)
  • peinit.operation.ended
  • peinit.operation.merged
  • peinit.operation.requested
  • peinit.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 (via subject)
  • kacs.audit.descriptor.changed (via subject)
  • kacs.audit.handle.used (via subject)
  • kacs.audit.privilege.used (via subject)
  • kacs.caap.policy.changed (via subject)
  • kacs.caap.sacl.skipped (via subject)
  • kacs.caap.staging.diverged (via subject)
  • kacs.descriptor.rejected (via subject)
  • kacs.impersonation.reverted (via subject)
  • kacs.impersonation.started (via subject)
  • kacs.mount.policy.changed (via subject)
  • lcs.audit.backup.ended (via caller)
  • lcs.audit.backup.started (via caller)
  • lcs.audit.key.created (via caller)
  • lcs.audit.key.deleted (via caller)
  • lcs.audit.key.descriptor.changed (via caller)
  • lcs.audit.key.hidden (via caller)
  • lcs.audit.key.opened (via caller)
  • lcs.audit.key.tombstoned (via caller)
  • lcs.audit.restore.ended (via caller)
  • lcs.audit.restore.started (via caller)
  • lcs.audit.transaction.committed (via caller)
  • lcs.audit.value.deleted (via caller)
  • lcs.audit.value.set (via caller)

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 (via subject)
  • kacs.audit.descriptor.changed (via subject)
  • kacs.audit.handle.used (via subject)
  • kacs.audit.privilege.used (via subject)
  • kacs.caap.policy.changed (via subject)
  • kacs.caap.sacl.skipped (via subject)
  • kacs.caap.staging.diverged (via subject)
  • kacs.descriptor.rejected (via subject)
  • kacs.impersonation.reverted (via subject)
  • kacs.impersonation.started (via subject)
  • kacs.mount.policy.changed (via subject)
  • lcs.audit.backup.ended (via caller)
  • lcs.audit.backup.started (via caller)
  • lcs.audit.key.created (via caller)
  • lcs.audit.key.deleted (via caller)
  • lcs.audit.key.descriptor.changed (via caller)
  • lcs.audit.key.hidden (via caller)
  • lcs.audit.key.opened (via caller)
  • lcs.audit.key.tombstoned (via caller)
  • lcs.audit.restore.ended (via caller)
  • lcs.audit.restore.started (via caller)
  • lcs.audit.transaction.committed (via caller)
  • lcs.audit.value.deleted (via caller)
  • lcs.audit.value.set (via caller)

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 (via subject)
  • kacs.audit.descriptor.changed (via subject)
  • kacs.audit.handle.used (via subject)
  • kacs.audit.privilege.used (via subject)
  • kacs.caap.policy.changed (via subject)
  • kacs.caap.sacl.skipped (via subject)
  • kacs.caap.staging.diverged (via subject)
  • kacs.descriptor.rejected (via subject)
  • kacs.impersonation.reverted (via subject)
  • kacs.impersonation.started (via subject)
  • kacs.mount.policy.changed (via subject)
  • lcs.audit.backup.ended (via caller)
  • lcs.audit.backup.started (via caller)
  • lcs.audit.key.created (via caller)
  • lcs.audit.key.deleted (via caller)
  • lcs.audit.key.descriptor.changed (via caller)
  • lcs.audit.key.hidden (via caller)
  • lcs.audit.key.opened (via caller)
  • lcs.audit.key.tombstoned (via caller)
  • lcs.audit.restore.ended (via caller)
  • lcs.audit.restore.started (via caller)
  • lcs.audit.transaction.committed (via caller)
  • lcs.audit.value.deleted (via caller)
  • lcs.audit.value.set (via caller)

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:

  • peinit.operation.ended
  • peinit.operation.merged
  • peinit.operation.requested
  • peinit.operation.started

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:

  • peinit.operation.ended
  • peinit.operation.merged
  • peinit.operation.requested
  • peinit.operation.started

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.attempted
  • authd.service.attested
  • authd.session.ended
  • kacs.audit.access.checked (via subject)
  • kacs.audit.descriptor.changed (via subject)
  • kacs.audit.handle.used (via subject)
  • kacs.audit.privilege.used (via subject)
  • kacs.caap.policy.changed (via subject)
  • kacs.caap.sacl.skipped (via subject)
  • kacs.caap.staging.diverged (via subject)
  • kacs.descriptor.rejected (via subject)
  • kacs.impersonation.reverted (via subject)
  • kacs.impersonation.started (via subject)
  • kacs.mount.policy.changed (via subject)
  • lcs.audit.backup.ended (via caller)
  • lcs.audit.backup.started (via caller)
  • lcs.audit.key.created (via caller)
  • lcs.audit.key.deleted (via caller)
  • lcs.audit.key.descriptor.changed (via caller)
  • lcs.audit.key.hidden (via caller)
  • lcs.audit.key.opened (via caller)
  • lcs.audit.key.tombstoned (via caller)
  • lcs.audit.restore.ended (via caller)
  • lcs.audit.restore.started (via caller)
  • lcs.audit.transaction.committed (via caller)
  • lcs.audit.value.deleted (via caller)
  • lcs.audit.value.set (via caller)
  • lpsd.account.created
  • lpsd.account.deleted
  • lpsd.account.modified
  • lpsd.credential.changed
  • lpsd.credential.verified
  • lpsd.group.created
  • lpsd.group.deleted
  • lpsd.group.member.added
  • lpsd.group.member.removed
  • netd.hostname.changed
  • peinit.operation.ended
  • peinit.operation.merged
  • peinit.operation.requested
  • peinit.operation.started
  • peipkg.action.authorised
  • peipkg.claim.changed
  • peipkg.package.installed
  • peipkg.package.uninstalled
  • peipkg.package.upgraded
  • peipkg.repository.added
  • peipkg.repository.reconfigured
  • peipkg.repository.refreshed
  • peipkg.repository.removed
  • peipkg.transaction.recovered
  • timed.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 (via subject)
  • kacs.audit.descriptor.changed (via subject)
  • kacs.audit.handle.used (via subject)
  • kacs.audit.privilege.used (via subject)
  • kacs.caap.policy.changed (via subject)
  • kacs.caap.sacl.skipped (via subject)
  • kacs.caap.staging.diverged (via subject)
  • kacs.descriptor.rejected (via subject)
  • kacs.impersonation.reverted (via subject)
  • kacs.impersonation.started (via subject)
  • kacs.mount.policy.changed (via subject)
  • lcs.audit.backup.ended (via caller)
  • lcs.audit.backup.started (via caller)
  • lcs.audit.key.created (via caller)
  • lcs.audit.key.deleted (via caller)
  • lcs.audit.key.descriptor.changed (via caller)
  • lcs.audit.key.hidden (via caller)
  • lcs.audit.key.opened (via caller)
  • lcs.audit.key.tombstoned (via caller)
  • lcs.audit.restore.ended (via caller)
  • lcs.audit.restore.started (via caller)
  • lcs.audit.transaction.committed (via caller)
  • lcs.audit.value.deleted (via caller)
  • lcs.audit.value.set (via caller)

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 (via subject)
  • kacs.audit.descriptor.changed (via subject)
  • kacs.audit.handle.used (via subject)
  • kacs.audit.privilege.used (via subject)
  • kacs.caap.policy.changed (via subject)
  • kacs.caap.sacl.skipped (via subject)
  • kacs.caap.staging.diverged (via subject)
  • kacs.descriptor.rejected (via subject)
  • kacs.impersonation.reverted (via subject)
  • kacs.impersonation.started (via subject)
  • kacs.mount.policy.changed (via subject)

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:

  • lcs.audit.transaction.committed

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.created
  • lcs.audit.key.deleted
  • lcs.audit.key.descriptor.changed
  • lcs.audit.key.hidden
  • lcs.audit.key.tombstoned
  • lcs.audit.transaction.committed
  • lcs.audit.value.deleted
  • lcs.audit.value.set
  • peipkg.claim.changed
  • peipkg.package.installed
  • peipkg.package.uninstalled
  • peipkg.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:

  • lcs.audit.transaction.committed

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:

  • kacs.audit.access.checked

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:

  • kacs.audit.access.checked

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:

  • lcs.audit.key.opened

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.

Peios Learn — documentation for the Peios project.

Built with Trail.