Rust-based init process for omnect-os initramfs.
A single binary acting as /init in the initramfs, in place of the shell scripts that
used to do this. Runs as PID 1: it prepares the partitions, the boot environment and the
runtime state for omnect-device-service, then hands over with switch_root.
How to use it on a device (flash modes, factory reset, filesystem check, status file) is described in docs/usage.md.
Implemented functionality:
- Bootloader abstraction:
BootEnvtrait for GRUB (grub-editenv) and U-Boot (fw_printenv/fw_setenv) - Degraded boot mode: a release image continues to boot without a usable bootloader environment and reports it; a debug image stops and opens a shell
- Configuration: parses
/proc/cmdline; build-time constants from the Yocto environment viabuild.rs - Partition management: root device detection, partition layout (GPT/DOS),
/dev/omnect/*symlinks - Filesystem operations: fsck, mount manager, overlayfs for
/etcand/home, bind mounts - Logging: kernel ring buffer (
/dev/kmsg) with log level prefixes - ODS integration: status file and update validation files for
omnect-device-service - fs-links: symlinks from
etc/omnect/fs-link.json,etc/omnect/fs-link.d/and the one-time listetc/omnect/create-fs-links.txt - switch_root: MS_MOVE + chroot + exec systemd
- Factory reset (modes 1-3) (feature
factory-reset) - Flash modes 1-3 (features
flash-mode-1,flash-mode-2,flash-mode-3) - Early module load (feature
early-modules)
The diagram traces every phase from PID 1 start to switch_root. The release-image
feature flag determines the error-handling branch at each fatal failure point.
flowchart TD
START([PID 1 starts]) --> MOUNT_ESS["mount_essential_filesystems\n/dev Β· /proc Β· /sys Β· /run"]
MOUNT_ESS -->|OK| LOGGER["KmsgLogger::init()"]
MOUNT_ESS -->|Fail| EARLY_ERR{Image type?}
EARLY_ERR -->|release| HALT1(["π΄ eprintln loop β halt"])
EARLY_ERR -->|debug| ESHELL(["π emergency sh β respawn"])
LOGGER -->|OK| CONFIG["Config::load()\n/proc/cmdline"]
LOGGER -->|Fail| FEB
CONFIG -->|OK| RDEV["detect_root_device()"]
CONFIG -->|Fail| FEB
RDEV -->|OK| LAYOUT["PartitionLayout::new()\ncreate_omnect_symlinks()"]
RDEV -->|Fail| FEB
LAYOUT -->|OK| CORE["mount_core_partitions()\nrootfs + boot + fsck"]
LAYOUT -->|Fail| FEB
CORE --> BENV["open_boot_env()"]
BENV --> CLASSIFY{"classify_boot_env"}
CLASSIFY -->|"OK β Available"| APPLY["apply_boot_env_decision()\ncore_result Γ env decision\npersist_fsck_results β always"]
CLASSIFY -->|"Fail + release β Degraded"| APPLY
CLASSIFY -->|"Fail + debug β Abort"| FEB
APPLY -->|Fatal| FEB
APPLY -->|"OK\nDegraded: ods.degraded_boot=true"| FBDETECT["set_update_pending()\ndetect_first_boot()\nfalse on a boot after an update"]
FBDETECT --> BMODE{"BootMode::detect()"}
BMODE -->|"Fatal: flash mode 1 and factory reset\nboth queued (both cleared)"| FEB
BMODE -->|"Flash(config)\nfeature = flash-mode"| FLASH
BMODE -->|"Normal / FactoryReset"| ISETUP["init_setup::run()\nextra_bootargs sync β always\nresize-data preflight if feature = resize-data"]
FLASH["flash::run()\nclear flash triggers β clone (mode 1), scp flash (mode 2)\nor download flash (mode 3)\nrun log β data partition"]
FLASH -->|OK| POWEROFF(["β» poweroff (mode 1) / reboot (modes 2, 3)"])
FLASH -->|Fatal| FEB
ISETUP -->|"FsckRequiresReboot\nExtraBootArgsUpdated"| FEB
ISETUP -->|"ResizeData error\nContinueDegraded β warn"| EMOD["early_modules::load()\nfeature = early-modules\nbest effort, never fails"]
ISETUP -->|"Fatal (non-resize)"| FEB
ISETUP -->|OK| EMOD
EMOD --> DISPATCH{"dispatch mode"}
DISPATCH -->|Normal| MREM["mount_remaining_partitions()\ndata Β· factory Β· cert + fsck\npersist_fsck_results β always"]
DISPATCH -->|"FactoryReset(trigger)\nfeature = factory-reset"| FCLEAR
subgraph FRESET["factory_reset::run() β always ContinueDegraded"]
direction TB
FCLEAR["clear factory-reset\nbootloader var (best-effort)"] --> FMOUNT["mount factory(ro) + etc(rw) + data(rw)\n+ overlays"]
FCLEAR -->|"trigger unusable"| FSTATUS
FMOUNT --> FBACKUP["build_preserve_list()\nbackup_all() β /tmp/factory_reset/backup"]
FBACKUP --> FUMOUNT1["umount"]
FUMOUNT1 --> FWIPE["wipe_partitions()\nmode 2: random overwrite Β· mode 3: discard\nmode 1: skipped Β· failure never aborts"]
FWIPE --> FFORMAT["reformat_and_mount_with_retry()\nmkfs data/etc (1 retry each, warn on fail)\nthen mount once β the deciding step"]
FFORMAT -->|"mount ok"| FRESTORE["restore_all()"]
FFORMAT -->|"mount fails on data/etc"| FSIGNAL["record failure signal\nβ bootloader env (in run())"]
FRESTORE --> FUMOUNT2["umount"]
FUMOUNT2 --> FSTATUS["ods_status.set_factory_reset(...)\nsuccess or error β never blocks boot"]
FSIGNAL --> FSTATUS
end
FSTATUS --> MREM
MREM -->|FsckRequiresReboot| FEB
MREM -->|Fatal| FEB
MREM -->|OK| OVL["setup_raw_rootfs_mount()\nsetup_etc_overlay()\nsetup_data_overlay()"]
OVL -->|OK| LINKS["create_fs_links()\ndrain_fsck_env() β env β JSON, then clear\ncreate_ods_runtime_files()\nsets the first-boot marker after an update"]
OVL -->|Fail| FEB
LINKS -->|OK| FBM["write_first_boot_marker()\nif first_boot β§ resize_ok β§ extra_bootargs_ok β§ env_available\nbest-effort β warn on fail"]
LINKS -->|Fail| FEB
FBM --> SR["switch_root β systemd"]
SR -->|OK| SUCCESS(["β
systemd running"])
SR -->|Fail| FEB
FEB{"Error handler\nRecoveryClass?"}
FEB -->|"RebootToApply (e.g. FsckRequiresReboot)"| REBOOT(["π Reboot"])
FEB -->|"Fatal + update_pending"| REBOOT
FEB -->|"Fatal + no update + release"| HALT2(["π΄ kmsg loop β halt forever"])
FEB -->|"Fatal + no update + debug"| DSHELL(["π debug bash/sh β respawn"])
classDef success fill:#2d6a2d,color:#fff,stroke:#1a3d1a
classDef reboot fill:#1a4d7a,color:#fff,stroke:#0d2d4d
classDef halt fill:#7a1a1a,color:#fff,stroke:#4d0d0d
classDef shell fill:#7a4a1a,color:#fff,stroke:#4d2d0d
class SUCCESS,POWEROFF success
class REBOOT reboot
class HALT1,HALT2 halt
class ESHELL,DSHELL shell
Terminal states
| Symbol | Outcome | Trigger |
|---|---|---|
| β | switch_root β systemd takes over |
Normal completion |
| β» | Power off | Flash mode 1 finished; the clone can be moved to its own device |
| π | Reboot | Flash mode 2 or 3 finished; FsckRequiresReboot (unconditional); ExtraBootArgsUpdated (first boot, new extra boot arguments); or any fatal error while omnect_validate_update is set β triggers bootloader OTA rollback |
| π΄ | Halt (kmsg loop, infinite) | Fatal error Β· release image Β· no OTA in flight |
| π | Debug shell (bash β sh fallback, respawning) | Fatal error Β· debug image Β· no OTA in flight |
Notes on error handling
All errors from run_init() reach handle_fatal_error in main.rs, which dispatches on
RecoveryClass:
RebootToApply(e.g.FsckRequiresReboot) β always Reboot, regardless of image typeFatal+omnect_validate_updateset β Reboot (bootloader OTA rollback)Fatal+ no OTA in flight + release β Halt (kmsg loop)Fatal+ no OTA in flight + debug β debug shell
FsckRequiresReboot edges in the diagram flow through this handler.
Notes on overlay, fs-link, and ODS setup (OVL / LINKS blocks)
These steps (setup_raw_rootfs_mount, setup_etc_overlay, setup_data_overlay,
create_fs_links, create_ods_runtime_files) abort the boot on any failure: the error
reaches handle_fatal_error, which halts the device on a release image, drops to a debug
shell on a debug image, or reboots when an OTA update is in flight (update_pending).
No dedicated design spec covers this region.
Notes on apply_boot_env_decision
mount_core_partitions result is captured rather than propagated immediately so that
fsck diagnostics can be persisted to the bootloader environment before the error
propagates.
apply_boot_env_decision propagates an error from mount_core_partitions before a
concurrent DegradedBoot β the two can occur together when GRUB's boot partition
is unmountable. persist_fsck_results runs on every mount path,
including degraded boot.
Notes on factory reset (FRESET block)
Enabled by the factory-reset feature. BootMode::detect() reads the factory-reset
bootloader env key; any non-blank value dispatches to mode::factory_reset::run() instead of
mode::normal::run(). A blank value is not a request to reset. A trigger can be cleared two
ways β by unsetting the key, or by writing an empty value β and the two backends disagree about
the second: fw_printenv reports an empty variable as unset, while grub-editenv still lists
the key. On GRUB a cleared trigger can therefore come back as a present but empty value, so
treating blank as no trigger makes both backends behave the same and keeps such a device from
being answered with a reset failure nobody asked for. A value the init cannot use is cleared and
reported there β status 1 when it does not name a mode the init can run, unparsable json
included, status 3 when the problem is preserve β rather than booting on in silence, which
would leave the caller waiting for a result forever. The reset sequence (mount β backup β wipe β
reformat β mount β restore) always completes with a FactoryResetStatus recorded in the ODS
status JSON β success or error β and then falls through into the same mode::normal::run() path
a normal boot takes (MREM onward), so a failed or unsupported reset never blocks the device
from booting: FactoryResetError is classified as ContinueDegraded.
Factory reset β wipe modes (FWIPE)
The behaviour is described in factory reset. A
factory-reset.d file without a usable paths array fails the reset, because skipping it
would wipe the paths it was meant to keep and still report success.
The wipe runs once the backup is in initramfs RAM and before the reformat. Mode 2 writes
data from /dev/urandom, mode 3 uses the BLKDISCARD ioctl, which the hardware has to
support. A failed wipe is reported as Error but does not abort the reset, so the device
stays usable.
Factory reset β reformat/mount retry (FFORMAT)
Inside reformat_and_mount_with_retry, each of data/etc is mkfs'd with one retry on
failure. The mount is then attempted once and decides the outcome: even after two failed
mkfs attempts the mount is still tried, since the filesystem may be usable. A mount failure
on data/etc is recorded as a bootloader-env signal (omnect_factory_reset_last_error), so
even if the following Normal boot halts on the same partition the cause is still readable
(fw_printenv / grub-editenv list). A mount failure is not re-mkfs'd β a repeated mkfs
would produce the same filesystem the mount just rejected.
flowchart TD
RF["mkfs(data), mkfs(etc)\none retry each on failure\nwarn! on every mkfs failure"] --> MNT{"mount + overlays\n(once)"}
MNT -->|ok| OKP["reset proceeds\nrestore β boot"]
MNT -->|"fails on data/etc"| SIG["write bootloader-env signal\nomnect_factory_reset_last_error"]
SIG --> GIVEUP["give up β Normal boot\nβ Fatal β halt (diagnosable)"]
MNT -->|"fails on factory/unknown"| PROP["propagate error\nno signal"]
| mkfs | mount | 2nd mkfs? | warn! |
bootloader signal | ODS status | boot |
|---|---|---|---|---|---|---|
| ok | ok | no | no | no | Success |
normal |
| 1Γ fail, then ok | ok | yes | yes | no | Warning + note |
normal |
| 2Γ fail | ok | yes | yes | no | Error + note |
normal |
| 2Γ fail | fails | yes | yes | yes | Error |
halt |
| ok | fails (data/etc) | no | yes | yes | Error |
halt |
The mkfs history is reported in the ODS status even when the mount succeeds: a recovered
single failure is Warning, two failures are Error β an early sign of failing storage.
# A bootloader and a partition table are both mandatory; build.rs rejects
# any other combination
cargo build --features grub,gpt # x86-64 EFI targets
cargo build --features uboot,gpt # ARM targets (dos applies too)
# Release build (optimized for size); U-Boot targets use gpt or dos
cargo build --release --features grub,gpt
cargo build --release --features uboot,gpt
cargo build --release --features uboot,dos
# With additional optional features
cargo build --release --features grub,gpt,factory-reset,persistent-var-log| Feature | Description | Status |
|---|---|---|
core |
Core boot sequence (default) | Implemented |
grub |
GRUB bootloader support β x86-64 EFI targets | Implemented |
uboot |
U-Boot bootloader support β ARM targets | Implemented |
gpt |
GPT partition table layout | Implemented |
dos |
DOS/MBR partition table layout | Implemented |
early-modules |
Load the modules listed in the rootfs before switch_root, see Early module load |
Implemented |
persistent-var-log |
Bind-mount /var/log to data partition |
Implemented |
release-image |
Release error handling: loop on fatal error; continue in degraded boot | Implemented |
resize-data |
Data partition auto-resize on first boot | Implemented |
test-utils |
Expose MockBootEnv for integration tests (never enabled in production) |
Test only |
factory-reset |
Factory reset support (modes 1-3: selective-preserve backup β wipe β reformat β restore) | Implemented |
flash-mode |
Shared flash layer: trigger detection, dispatch, log capture. Pulled in by a mode feature, never selected on its own | Implemented |
flash-mode-1 |
Disk cloning (part of the default feature set) | Implemented |
flash-mode-2 |
Flash a wic.xz pushed in over scp |
Implemented |
flash-mode-2-direct |
Implies flash-mode-2; no verify pass, flashes straight from the scp stream |
Implemented |
flash-mode-3 |
Flash a wic.xz downloaded from a URL |
Implemented |
flash-mode-network |
Shared layer of modes 2 and 3: network, unmount sweep, xz decoding. Pulled in by a mode feature, never selected on its own | Implemented |
rtc |
The machine has a reliable real-time clock, so mode 3 checks TLS certificates; without it, mode 3 does not authenticate the server | Implemented |
Note:
grubandubootare mutually exclusive, and so aregptanddos. Exactly one of each pair must be set at build time βbuild.rspanics otherwise. The Yocto recipe setsCARGO_FEATURESfromMACHINE_FEATURES,DISTRO_FEATURESand recipe variables; see the meta-omnect README.flash-mode-1is in the default feature set, so it is already enabled in thecargo buildexamples above; add--no-default-featuresto build without it.
build.rs reads these Yocto environment variables. The build does not fail
without them, but a flash mode that needs a missing value refuses to run. A
value that is not a number is ignored with a cargo warning.
| Env var | Constant | Unit | Needed by |
|---|---|---|---|
OMNECT_PART_OFFSET_UBOOT_ENV1 |
UBOOT_ENV1_START |
KiB | flash-mode-1 with uboot; with grub only when BOOTLOADER_SEEK is set |
OMNECT_PART_OFFSET_UBOOT_ENV2 |
UBOOT_ENV2_START |
KiB | flash-mode-1 with uboot, optional |
OMNECT_PART_SIZE_UBOOT_ENV |
UBOOT_ENV_SIZE |
KiB | flash-mode-1 with uboot |
OMNECT_PART_SIZE_DATA |
DATA_SIZE |
KiB | flash-mode-1 |
BOOTLOADER_SEEK |
BOOTLOADER_START |
KiB | flash-mode-1, optional |
OMNECT_PART_OFFSET_BOOT |
BOOT_START |
KiB | flash-mode-2 |
OMNECT_PART_SIZE_BOOT |
BOOT_SIZE |
KiB | flash-mode-2 |
OMNECT_USER_ID |
OMNECT_USER_ID |
uid and gid of the omnect user |
flash-mode-2 |
The init calls these tools and reads these files from the initramfs image. The
image recipe has to install them, or the step that needs them fails at run
time. The paths are the *_CMD and *_SOURCE constants in the source, plus
files the called tools need.
| Tool or file | Needed by | Yocto package |
|---|---|---|
fsck |
core |
util-linux-fsck |
fsck.ext4 backend (e2fsck) |
core |
e2fsprogs-e2fsck |
fsck.vfat backend |
core (boot partition) |
dosfstools |
blkid |
core |
util-linux-blkid |
gzip, gunzip |
core (fsck output in the bootloader env) |
busybox |
base64, cp |
core |
coreutils |
bash |
debug shell | bash |
sh |
emergency shell, fallback of the debug shell, shell of the omnect user for scp |
bash |
grub-editenv |
grub |
grub-editenv |
fw_printenv, fw_setenv |
uboot |
libubootenv-bin |
/etc/fw_env.config |
uboot |
libubootenv |
mkfs.ext4, tune2fs |
factory-reset, flash-mode-1 |
e2fsprogs-mke2fs, e2fsprogs-tune2fs |
sync |
factory-reset, resize-data |
coreutils |
sgdisk, parted, resize2fs |
resize-data |
gptfdisk, parted, e2fsprogs-resize2fs |
sfdisk |
flash-mode-1 |
util-linux-sfdisk |
e2image |
flash-mode-1 |
e2fsprogs |
fatlabel |
flash-mode-1 with grub |
dosfstools |
efibootmgr |
flash-mode-1, flash-mode-2, flash-mode-3 on EFI machines |
efibootmgr |
efivarfs filesystem |
flash-mode-1, flash-mode-2, flash-mode-3 on EFI machines |
kernel (CONFIG_EFIVAR_FS) |
/etc/omnect/grubenv.in |
flash-mode-1 with grub |
grub-env |
/etc/omnect/uboot-env.bin |
flash-mode-1 with uboot |
image recipe (add_uboot_env) |
/sbin/ip |
flash-mode-2, flash-mode-3 |
busybox |
/sbin/dhcpcd |
flash-mode-2, flash-mode-3 |
dhcpcd, base-passwd for its system user |
/sbin/dropbear, scp |
flash-mode-2 |
dropbear |
omnect user and /home/omnect |
flash-mode-2 |
image recipe (inherit omnect_user) |
devpts filesystem |
flash-mode-2 |
kernel (CONFIG_UNIX98_PTYS) |
/usr/bin/curl |
flash-mode-3 |
curl |
| CA certificates | flash-mode-3, checked only with rtc |
ca-certificates |
With early-modules, the init also reads files from the rootfs, see
Early module load.
# All four valid base combinations (bootloader Γ partition table)
# test-utils is required for the degraded_boot integration test; factory_reset
# needs the factory-reset feature as well, see the next block
cargo test --features grub,gpt,test-utils
cargo test --features grub,dos,test-utils
cargo test --features uboot,gpt,test-utils
cargo test --features uboot,dos,test-utils
# With factory-reset feature
cargo test --features grub,gpt,factory-reset,test-utils
cargo test --features grub,dos,factory-reset,test-utils
cargo test --features uboot,gpt,factory-reset,test-utils
cargo test --features uboot,dos,factory-reset,test-utils
# With resize-data feature
cargo test --features grub,gpt,resize-data,test-utils
cargo test --features grub,dos,resize-data,test-utils
cargo test --features uboot,gpt,resize-data,test-utils
cargo test --features uboot,dos,resize-data,test-utils
# With persistent-var-log feature
cargo test --features grub,gpt,persistent-var-log,test-utils
cargo test --features uboot,dos,persistent-var-log,factory-reset,test-utils
# With early-modules feature
cargo test --features grub,gpt,early-modules,test-utils
cargo test --features uboot,gpt,early-modules,test-utils
# With release-image feature
cargo test --features grub,gpt,release-image,test-utils
cargo test --features grub,dos,release-image,test-utils
cargo test --features uboot,gpt,release-image,test-utils
cargo test --features uboot,dos,release-image,test-utils
# With both resize-data and release-image
cargo test --features grub,gpt,resize-data,release-image,test-utils
cargo test --features uboot,gpt,resize-data,release-image,test-utils
# flash-mode-1 is in the default feature set, so every combination above
# already builds and tests it; the factory-reset ones also cover the refusal
# of flash mode 1 queued together with a factory reset.
# Without any flash feature: flash-mode-1 is in the default set, so
# --no-default-features is required to exclude it; --features alone is
# additive and cannot turn a default feature off
cargo test --no-default-features --features grub,gpt,factory-reset,test-utils
# Flash mode 2 (mode-2-only build)
cargo test --no-default-features --features core,grub,gpt,flash-mode-2,test-utils
cargo test --no-default-features --features core,grub,dos,flash-mode-2,test-utils
cargo test --no-default-features --features core,uboot,gpt,flash-mode-2,test-utils
cargo test --no-default-features --features core,uboot,dos,flash-mode-2,test-utils
cargo test --no-default-features --features core,uboot,gpt,flash-mode-2-direct,test-utils
cargo test --no-default-features --features core,grub,gpt,flash-mode-2-direct,test-utils
# Flash mode 3 (mode-3-only build), also with a real-time clock; two download tests run the
# host's /usr/bin/curl and fail without it
cargo test --no-default-features --features core,grub,gpt,flash-mode-3,test-utils
cargo test --no-default-features --features core,grub,dos,flash-mode-3,test-utils
cargo test --no-default-features --features core,uboot,gpt,flash-mode-3,test-utils
cargo test --no-default-features --features core,uboot,dos,flash-mode-3,test-utils
cargo test --no-default-features --features core,uboot,gpt,flash-mode-3,rtc,test-utils
# Flash modes together
cargo test --features uboot,gpt,flash-mode-1,flash-mode-2,factory-reset,test-utils
cargo test --no-default-features --features core,uboot,gpt,flash-mode-2,flash-mode-3,test-utils
cargo test --features uboot,gpt,flash-mode-3,factory-reset,test-utils
cargo test --features grub,gpt,flash-mode-1,flash-mode-2,flash-mode-3,factory-reset,test-utils
# Lint without --tests too: dev-dependencies enable crate features that the
# release build does not have
cargo clippy --no-default-features --features core,uboot,gpt,flash-mode-2,flash-mode-3 -- -D warnings
# Verbose output
cargo test --features grub,gpt,test-utils -- --nocaptureflash-mode or flash-mode-network alone, without a mode feature,
is not a supported configuration β no combination above builds it that way,
and no gate covers it.
Some U-Boot targets are 32-bit ARM, where usize is 4 bytes and a cast from
a 64-bit byte count silently truncates. The test run above is host-only and
cannot see that, so compile and lint for a 32-bit target as well (cargo check
and cargo clippy need no cross-linker):
rustup target add armv7-unknown-linux-gnueabihf
cargo clippy --target armv7-unknown-linux-gnueabihf --tests \
--features uboot,dos,factory-reset,test-utils -- -D warnings
# Run the tests on a 32-bit host target (needs no cross-linker on x86-64)
rustup target add i686-unknown-linux-gnu
cargo test --target i686-unknown-linux-gnu --no-default-features \
--features core,uboot,dos,flash-mode-2,flash-mode-3,test-utils
# Narrowing casts, reviewed by hand β not part of the gate, it also flags safe ones
cargo clippy --target armv7-unknown-linux-gnueabihf \
--features uboot,dos,factory-reset -- -W clippy::cast_possible_truncationMIT OR Apache-2.0