Skip to content
pichi-vmPublic

About

The pichi guest agent: mounts the root carapace in the initramfs and pivots.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Corium

Corium is a minimal Linux initramfs PID 1. It establishes a verified Carapace root and optional writable state, then executes /sbin/init. It also retains its static executable in /run/initramfs/shutdown for systemd’s shutdown handoff.

Image-builder contract

  • Supply Linux 7.0 or newer, with device-mapper, devtmpfs, procfs, sysfs, bootconfig support (CONFIG_BOOT_CONFIG=y), and the drivers needed to discover and mount the root. Drivers may be built in or supplied as modules.
  • Install the static Corium binary as /init in an initramfs.
  • Put the required modules and their dependencies under /usr/lib/modules/<running-kernel-release>/, preserving subdirectories. Corium loads regular .ko, .ko.xz, .ko.gz, and .ko.zst files. Compressed files require the matching kernel decompressor. No module index, userspace decompressor, udev, shell, or device nodes are needed.
  • Activate bootconfig with bootconfig on the kernel command line and supply corium.root in the bootconfig document. The value is the Carapace tip hash: nonempty, even-length hexadecimal, in either case, with no 0x prefix. Carapace validates the digest length against the chain.
  • Expose the Carapace's GPT partitions to the guest. The assembled Carapace may contain a filesystem directly or a GPT with one eligible root partition for the guest architecture. Corium detects the layout from the GPT signature.
  • Include an executable /sbin/init, its interpreter/libraries, and the directories and files the real init requires in that filesystem, including real /dev, /proc, /sys and /run mountpoints. Corium does not create runtime mountpoints inside the read-only root.

Boot behavior

  1. Mount devtmpfs and reopen the console. Require Linux 7.0+ before mounting temporary procfs and sysfs.
  2. Read and validate Corium's settings from /proc/bootconfig.
  3. Read the selected plain .ko files' ELF .modinfo hard dependencies and load dependencies first. Arma decompresses modules when building the initramfs; compressed files are rejected with a packaging diagnostic. Already-loaded modules count as available. Missing dependencies, cycles and malformed metadata fail before loading; kernel rejection is fatal. Module metadata reads are limited to 64 MiB per file. An absent or empty module directory is valid.
  4. Discover the root chain, allowing up to five seconds for missing partitions. Validation and other I/O failures are fatal immediately. Assemble it using Carapace's file-based API.
  5. Create a read-only dm-linear view named corium-root over the assembled device or its selected GPT root partition, leaving the shared chain available as another consumer's origin. Recognize ext-family, XFS and Btrfs primary superblock signatures and use the matching registered driver directly. Prefer ext4 for the ext family (it also supports ext2/ext3), avoiding incompatible older alias probes. A missing driver or rejected recognized mount is fatal; conflicting signatures are rejected. Otherwise try registered non-nodev drivers until the view mounts read-only. Unknown formats retain driver discovery rather than being rejected by a fixed filesystem allowlist.
  6. Make mount propagation recursively private, then move the temporary API mounts into the new root. Use pivot_root(".", ".") and detach the old initramfs, relying on Linux 7.0+'s rootfs semantics. There is no recursive deletion, writable old-root directory, or legacy-kernel fallback.
  7. Mount tmpfs at /run and copy only the running static executable to /run/initramfs/shutdown; the boot initramfs and module files are not retained. Detach the temporary API mounts, then execute /sbin/init with every received process argument after argv[0], preserving their bytes, order, and boundaries. Set argv[0] to /sbin/init and inherit the environment. Console descriptors close on successful exec, but remain available for a fatal diagnostic if the handoff fails.

On fatal errors, Corium logs at error level, syncs, and attempts to power off.

Shutdown handoff

Systemd stops services and flushes storage, switches into /run/initramfs, and executes /shutdown with the shutdown action as its first argument. Corium normally unmounts the /oldroot subtree, children first, then syncs and performs the requested poweroff, reboot, halt or kexec. Unmount failures are reported before the requested action is attempted. An unsupported action is reported and halts instead. No dracut restoration service is needed.

Carapace and Corium mappings already use deferred removal. Corium does not wait for their kernel objects to disappear after unmounting, issue additional device-removal commands, or reload modules. Writable state must be cleanly unmounted; read-only mapping disappearance is not a shutdown gate.

It never returns from PID 1. Invoking it as an ordinary process fails without performing init duties. Kernel and real-init output are outside Corium's logging policy. Failures before the console is established may have no Corium diagnostic if the kernel supplied no usable stderr.

Corium does not create /run, leave API mounts for the real init, enforce a JSON boot contract, configure networking, provide a writable root layer, or run as a post-boot service. It has no systemd generator, init-path override, directory listing, or success-marker mode.

The pinned Carapace API currently wraps missing-partition errors as InvalidData. The discovery adapter recognizes that specific diagnostic; other validation errors are not retried. Recheck this adapter when updating Carapace.

Persistent instance state

A writable whole virtio disk provides persistent /var, /home, /root and /srv on the same state filesystem. Without a writable disk, Corium boots without persistent state. Root, /etc and /usr remain read-only.

List image-owned files or directories in /var/.bind, one relative path beneath /var per line (for example lib/dpkg). These paths remain read-only and come from the current image on every boot. An absent or empty file makes all of /var mutable when a state disk is attached. Blank lines and lines beginning with # are ignored. Symlinks, missing sources, escaping paths, overlapping entries and conflicting state types are rejected. Declared paths hide existing state without deleting it; removing a declaration exposes that state again.

Attach exactly one writable whole virtio disk, either without a GPT or containing a GPT state partition of type 32a2f055-1a43-44b2-ab7a-483ff5f13481. Its filesystem must match the root filesystem. Corium accepts 512- and 4096-byte logical sectors, validates both GPT copies and requires one partition aligned to 1 MiB. It rejects unsupported or damaged GPT storage without formatting or repairing it.

For a disk without a GPT signature in either header location, Corium creates the state GPT and runs /usr/sbin/mkfs.<root-filesystem> from the verified image after pivot. Include that formatter and its libraries in the root image. Initialization completes within the first boot. Existing non-GPT data is replaced. Once a GPT exists, interrupted initialization is not retried by formatting; existing filesystem failures are fatal.

The image must provide real /tmp, /var, /home, /root and /srv directories. Corium temporarily mounts state at /tmp, binds each matching state directory onto its image mountpoint, and detaches staging before init. Init remains responsible for runtime mounts and tmpfiles initialization. Image contents of these four directories are hidden except declared /var bindings. New state directories inherit their image directory’s ownership and permissions; existing instance permissions and contents are retained. Initialize other required contents with tmpfiles. This does not authenticate state or hide the disk from guest root.

Boot configuration

Corium consumes the top-level corium namespace from /proc/bootconfig:

corium {
    root = "<Carapace tip hash in hexadecimal>";
    log = error;
    # halt = assembled;
}

corium.root is required. All Corium settings take one scalar value. Unknown keys within corium are fatal; other namespaces are ignored. Linux parses the document and resolves grouping and assignment operators; Corium reads the resulting flattened, quoted key/value records.

Two optional settings control diagnostics:

  • corium.log = off|error|warn|info|debug|trace sets the maximum verbosity. The default is error; successful boots and shutdowns are silent. off suppresses all Corium records, including fatal errors.
  • corium.halt = started|modules|discovered|assembled|mounted|pivoted|state syncs and powers off after the selected phase completes successfully. Without it, Corium executes /sbin/init. It does not change the log level.

Diagnostic values are case-sensitive. Empty values and arrays are fatal. Until the log setting can be read and validated, the filter is error. A valid log setting applies before validation of the root and shutdown settings, regardless of entry order. Invalid log settings leave the bootstrap error filter in effect.

At info level, each completed phase produces a record:

INFO corium: message="phase completed" phase="assembled" elapsed_us=28000 duration_us=8000
INFO corium: message="shutdown requested" phase="assembled"

The second record appears only at a requested shutdown boundary. Phase meanings:

Phase Work completed
started Temporary mounts, console setup, and configuration validation
modules All supplied modules loaded
discovered Root chain discovered and validated
assembled Carapace device activated
mounted Root filesystem mounted read-only
pivoted Root pivot and old-root detachment; API mounts remain until handoff
state Optional persistent directories and image bindings ready; staging and temporary API mounts detached, before init

The state milestone also occurs when persistence is not enabled. State setup failures never report it as completed.

Both times are monotonic microseconds. elapsed_us measures time since entry to the boot routine; duration_us measures this phase, including retries. The preceding phase-completion record's write is excluded from the next phase's duration, but included in total elapsed time.

debug adds module and filesystem selections and assembly details; trace adds individual module, discovery, and mount attempts. warn is available for noteworthy recoverable conditions; there are currently no warning records. Normal dependency retries are trace events. Fatal errors use message="boot failed" with a separate error field.

Records use Rust's log facade with structured key/value fields and a small synchronous console logger. The format is LEVEL target: message="..." key=value. Messages and string fields use JSON-style quoting and escaping; numbers and booleans are unquoted. Each record occupies one line. Targets are corium or its module paths (for example, corium::modules). Logging retains no extra file descriptor and remains available after the pivot until successful exec. Kernel and real-init output are unaffected by these settings.

Build and validation

Rust 1.89 or newer is required; all build settings work on stable Rust. Release builds optimize for size with LTO, stripped symbols, and abort-on-panic. Run tests as an ordinary Linux user.

cargo build --locked --release --target x86_64-unknown-linux-musl
cargo test --locked
cargo fmt --all -- --check
RUSTFLAGS='-D warnings' cargo clippy --locked --all-targets --all-features
cargo deny check

Use aarch64-unknown-linux-musl for an ARM64 guest.

The rootless x86-64 VM tests are ignored Rust integration tests. They require read/write access to /dev/kvm, a running Linux 7.0+ kernel with its readable /boot/vmlinuz-$(uname -r) and matching installed modules/configuration, stable Rust with the x86-64 musl target, QEMU, host systemd, cpio, sfdisk, mkfs.ext4, mkfs.xfs, mkfs.btrfs, matching ext4/XFS/Btrfs kernel drivers, debugfs, e2fsck, modprobe, veritysetup, dmsetup, losetup, mount, umount, mkdir, truncate, sh, ldd, ldconfig, sha256sum, df, and lsblk. Installed compressed modules also require the corresponding xz, gzip, or zstd tool. No root, host mounts, loop devices, or host device-mapper changes are needed. Required prerequisites fail explicitly; optional decompressor and squashfs cases report OMIT with a reason, separately from Cargo's ignored count. The suite requires at least 8 GiB free artifact storage per invocation.

cargo +stable test --locked --test boot -- --ignored --test-threads=1
cargo +stable test --locked --test benchmark -- --ignored --nocapture --test-threads=1

The guest binary is built once per invocation using stable Rust's musl release profile, then copied into the artifact directory. Host test code stays in the ordinary test profile. Run VM tests and benchmarks sequentially on an otherwise idle host; do not run separate invocations concurrently when comparing timings.

The shared large fixture has 50 scutes on one read-only virtio GPT disk with 100 partitions. A bootable ext4 base plus 44 small 4 MiB increments and five 160 MiB increments produces roughly 1 GiB of changed-block payload. Offline filesystem edits, a Rust snapshot-store writer, and veritysetup produce the chain without mounting it. Systemd runs sha256sum to check a surviving sentinel from every layer and the final overwritten value before printing its success marker and powering off. The bulk payload is not read in full during validation. This is a synthetic demanding workload, not a full Fedora image.

Small fixtures exercise bootconfig validation, module failure/compression, discovery timeout, corrupt metadata/data, missing intermediate scutes, failed exec, halt boundaries, log filtering, and filesystem selection. Every expected shutdown requires QMP's explicit guest-shutdown event. A deliberate panic negative control ensures QEMU exit alone is rejected.

The informational benchmark uses the same large recipe and validation service: two warm-up boots and ten measured boots, each in a fresh VM with corium.log = info in bootconfig. The primary metric is state.elapsed_us, from entry to Corium's boot routine through state setup and temporary mount cleanup. This benchmark has no state manifest, so it measures the ordinary immutable boot path. It excludes startup in main, the final log write, and exec; successful systemd validation independently proves handoff. It includes earlier logging overhead. Phase durations explain the total but must not be summed to reconstruct it, because they exclude completion-record writes. Fixture generation, QEMU/kernel startup, and systemd/shutdown are not part of the metric. Host caches are warmed by construction and previous boots; these are not cold-storage measurements. No host cache flushing is performed. Report median/minimum/maximum and raw samples, with no timing pass/fail limit.

Artifacts remain under target/boot-tests-*: GPT/initramfs images, console and QMP logs, case summaries, fixture sizes/roots, input hashes, command lines, compiler/kernel/tool/host metadata, and benchmark CSV/summary. A failed run retains its directory. Each invocation builds fresh fixtures, so tool and host systemd changes are recorded rather than hidden by cross-run caching. Remove obsolete target/boot-tests-* directories when their evidence is no longer needed; they can occupy several GiB each. See CONTRIBUTING.md for the required pre-merge correctness gate and runner setup.

Consumer migration

Image builders install Corium as /init, enable bootconfig, and supply corium.root with the Carapace tip hash. Fedora's module-tree layout matches this contract. Carapace manages the assembled device's identity.

Version 0.2 removes the former library items CORIUM_ROOT_OK, CORIUM_PIVOT, parse_root_carapace, and decode_root_carapace. The library now exposes boot and init_logging for the PID 1 binary. External boot tests must provide a real init and verify its handoff, rather than expect Corium to list files and power off successfully when init is absent.

COPR packages

Pushes to main trigger the existing COPR webhook for npmccallum/pichi. COPR prepares one vendored SRPM and builds the native RPMs. Its build ID is included in the RPM release so later builds of the same version supersede earlier packages. Until the first release, the RPM version is 0.1.0~dev. The packaged executable is static and can be used as an initramfs init.

Ubuntu packages

Launchpad builds main snapshots into ppa:nmccallum/pichi. See debian/README.source for the PPA publication process and setup.

About

The pichi guest agent: mounts the root carapace in the initramfs and pivots.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages