॥ श्री ॥

Atomic Updates

Concepts 2026-08-28

Shanios uses an intelligent multi-layered update system with automatic checking, user notifications, and the shani-deploy tool for atomic system updates. Updates are all-or-nothing — the running system is never touched while an update is being prepared.

Update Process Flow

  1. Checkshani-update timer runs automatically (15 min after boot, then every 2 hours) and finds a new release
  2. Notify — GUI dialog (yad/zenity/kdialog) prompts for user approval
  3. Download — R2 CDN primary, SourceForge fallback; resume support via aria2c/wget/curl
  4. Verify — SHA256 checksum + GPG signature; Btrfs snapshot of old slot taken as backup
  5. Deploy — New image extracted to inactive slot; UKI generated by gen-efi; bootloader updated
  6. Reboot — Reboot prompt shown; system boots into updated slot
  7. Confirmmark-boot-success writes /data/boot-ok once multi-user.target is reached; bless-boot then stops the boot counter

If boot fails, the system automatically falls back to the previous slot.

The Full Update Pipeline

Here is every step shani-deploy runs internally, in order.

1. Fetch and Verify

# Download version manifest from R2 CDN (with SourceForge fallback)
# Download image — streamed with resume support
# SHA256 verified after download
# GPG signature verified against known public key (7B927BFFD4A9EAAA8B666B77DE217F3DA8014792)

Nothing is written to the OS subvolumes until both the checksum and signature verify. A corrupted or tampered image never touches your system. If the download is interrupted, it resumes from where it stopped.

2. Snapshot the Inactive Slot

btrfs subvolume snapshot @green @green_backup_20260820153000

This safety snapshot (named <slot>_backup_<timestamp>) is created before any changes to the inactive slot, and only if that slot already holds a previous system (a first-ever deploy has nothing to back up). If extraction or the swap below fails partway, shani-deploy's error trap (restore_candidate) uses this snapshot to put @green back exactly as it was — the running @blue slot is never at risk either way.

3. Extract the New Image

# Decompress and receive the btrfs send stream into a scratch workspace
zstd -d --long=31 -T0 shanios-<version>-<profile>.zst -c | btrfs receive /mnt/temp_update

# Only once extraction succeeds: swap the candidate slot
btrfs property set -f -ts @green ro false
btrfs subvolume delete @green
btrfs subvolume snapshot /mnt/temp_update/shanios_base @green
btrfs property set -f -ts @green ro true

btrfs receive reconstructs the subvolume from the send stream into a scratch workspace first — @green itself is only deleted and replaced after the receive has fully succeeded, so a failed or interrupted extraction leaves the old @green (and the safety snapshot from step 2) untouched. The result is byte-for-byte identical to the subvolume that was snapshotted, signed, and shipped — not a package application or diff, but a complete verified reconstitution of exactly what passed build QA.

4. Generate the UKI

gen-efi generates and signs a new Unified Kernel Image for the updated slot, run inside a chroot of the candidate slot:

gen-efi configure green

The kernel cmdline embedded in the UKI is regenerated from the live disk state — current LUKS UUID, swap offset — and signed with the MOK keypair. The signed UKI is placed in the ESP and the bootloader is updated to set it as the next-boot default.

5. Boot Counting

The new entry gets a +3-0 suffix: 3 tries allowed, 0 done. Each failed boot attempt decrements the tries-left counter. If it reaches zero, systemd-boot automatically falls back to the previous slot's UKI.

If the new slot boots successfully, bless-boot calls bootctl set-good, stopping the countdown. The slot becomes the permanent default.

Before / After

Before update — running @blue:

  • @blue ✅ Active (Booted) — current system
  • @green ⏸️ Inactive (Old) — update written here
  • shani-deploy writes new image into @green, keeps a Btrfs snapshot of old @green
  • Bootloader updated → @green set as next default

After reboot — running @green:

  • @blue ⏸️ Inactive — instant rollback available
  • @green ✅ Active (Updated!) — new system running
  • shani-update --startup (run at login) checks for a fallback or candidate boot and confirms success

Automatic Rollback

Rollback actually happens in two stages — one automatic at the bootloader level, one confirmed by you at the login screen.

Stage 1 — systemd-boot falls back on its own. Every boot of the newly updated slot counts against its +3-0 tries. bless-boot only calls bootctl set-good (clearing the counter) after mark-boot-success has confirmed multi-user.target was reached and written /data/boot-ok. So any boot that crashes, hangs, or never reaches a working session — including a hard failure where the Btrfs root itself fails to mount, caught by a dracut hook that writes /data/boot_hard_failure before the mount is even attempted — leaves the counter un-cleared. After three such attempts, systemd-boot automatically boots the fallback slot's .conf entry instead. This part requires no user interaction and no login: you land on the previous, working system.

Stage 2 — the actual data rollback is offered, not silent. systemd-boot switching slots only changes which UKI boots next; it does not touch Btrfs subvolume contents, so the failed candidate slot is still sitting there broken. On next login, shani-update detects the mismatch between the booted slot and /data/current-slot (or the boot_hard_failure marker) and shows a "Roll Back Now / Ignore" dialog. Choosing to roll back runs shani-deploy --rollback, which restores the failed slot from its pre-deploy safety snapshot (see step 2 above) and rewrites both boot entries with no tries needed, since both slots are now known-good. No data on @home, @data, or any other persistent subvolume is touched by any of this — see Persistence Strategy.

The boot health pipeline behind stage 1:

  • mark-boot-in-progress — clears prior markers and plants /data/boot_in_progress at boot start
  • mark-boot-success — writes /data/boot-ok once multi-user.target is reached
  • bless-boot — calls bootctl set-good once /data/boot-ok exists, stopping the boot-count decrement
  • check-boot-failure (15-minute timer) — if /data/boot_in_progress is still present and /data/boot-ok never appeared, records the slot in /data/boot_failure
  • a dracut pre-mount hook writes /data/boot_hard_failure unconditionally before the root Btrfs mount is attempted, and a pre-pivot hook clears it on success — this catches failures too early for the above systemd units to ever run

Release Channels

shani-deploy supports two release channels:

ChannelCadenceUse Case
stableMonthlyDefault — recommended for all users
latestMore frequentEarly access, testing

For channel switching commands, manual update commands, rollback, and storage management, see System Updates.

Storage Efficiency

The dual-slot architecture is not as expensive on disk as it sounds, for three concrete reasons rather than one fixed number:

  • Every subvolume in fstab (@root, @home, @data, and the @data/varlib/* bind-mount sources) mounts with compress=zstd, so file content is transparently compressed on write.
  • @blue/@green are seeded as Btrfs snapshots of each other, not independent copies — Copy-on-Write means only blocks that actually differ between the two slots consume extra space; identical blocks are shared on disk.
  • bees runs continuously in the background (beesd@<uuid>.service) deduplicating shared content across all subvolumes, on top of whatever CoW sharing already gives you for free.

There's no fixed "X% overhead" figure to quote here — actual savings depend on how much changes between updates and how compressible your data is — but the combination means a second slot rarely costs anywhere near a second full copy of the OS.

See Also