estate/holger-live/src/lib.rs
//! **Holger's live slot** (L20, 2026-10-18): a Holger box — main, twin, or the front — takes a
//! live update into an app slot of its own, the lifecycle step Gunnar's boxes take, in Holger's
//! own code. The decisions are korp's (`korp_installer::live`: the A/B slots, the verification
//! legs, the ONE setting, the confirm, the rollback); the wire is the shared nordisk-pairing
//! live door (`/live/running`, `/live/stage`, the installer's own certificate only):
//!
//! * [`LiveSlot::running`] — what this box runs: its BASE, the running slot's app pin, the slot
//! word (`a`, `b trial`, `b booted`), and its own off switch.
//! * [`LiveSlot::stage`] — a new signed app verified against this box's enrolled root, rollback
//! floor and BASE, written to the slot NOT running, the setting last (`<slot> trial`); then the
//! box is asked to stop gracefully and re-exec korp-start into it ([`staged`] tells the PID-1
//! stop to re-exec instead of powering off).
//! * [`LiveSlot::confirm_step`] — the new app confirms its slot once it has served for
//! [`CONFIRM_AFTER`]; a slot still not serving at [`CONFIRM_DEADLINE`] is rolled back (the
//! re-exec: korp-start finds it `booted`, never confirmed, and boots the other slot).
//!
//! A box whose BASE moved, whose live switch is off, or whose stage is refused, is updated from
//! its keep medium instead (the front reports `needs-keep` for itself, `done:keep` for the
//! others): the keep path stays the fallback.
/// A first-boot wait, said by name once a minute (test3-holger-rs 2026-10-22).
pub mod waits;
/// The trial confirm as jera jobs (feature `jobs`).
#[cfg(feature = "jobs")]
pub mod trial;
/// A box's own background duty (a watch, a retry) as jera jobs (feature `jobs`, A28 M18).
#[cfg(feature = "jobs")]
pub mod duty;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use korp_installer::live::{self as korp, Esp, Trial};
pub use korp_installer::live::{Policy, Slot};
pub use nordisk_pairing::live::{Declared, Running, Stage};
/// The trial's deadlines and its step are korp's (lane B3): ONE rule for every product.
pub use korp_installer::live::{confirm_plan, ConfirmStep, CONFIRM_AFTER, CONFIRM_DEADLINE};
/// How long the graceful stop of a live update has before the box re-execs anyway.
pub const STOP_GRACE: Duration = Duration::from_secs(30);
/// Writes one slot's partition (the real partition on a box, a buffer in a test).
pub type SlotWriter = Box<dyn Fn(Slot, &[u8]) -> Result<(), String> + Send + Sync>;
/// Asks the box to stop gracefully and re-exec korp-start (nothing in a test).
pub type Restart = Box<dyn Fn() + Send + Sync>;
/// A live update staged in this process: the PID-1 stop ends in a re-exec ([`after_stop`]).
static STAGED: AtomicBool = AtomicBool::new(false);
/// Whether a live update waits for the stop.
pub fn staged() -> bool {
STAGED.load(Ordering::SeqCst)
}
/// What a clean stop ends in.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AfterStop {
/// The ordinary end of an appliance.
PowerOff,
/// A live update is staged: re-exec korp-start.
ReExec,
}
/// Pure: **after the graceful stop** — a staged live update re-execs, everything else powers off.
pub fn after_stop(staged: bool) -> AfterStop {
if staged {
AfterStop::ReExec
} else {
AfterStop::PowerOff
}
}
/// **A box's live slot**: its ESP (where korp keeps the setting and each slot's records), its
/// live policy, how a slot's partition is written, and how the box restarts into it.
pub struct LiveSlot {
pub esp: Esp,
pub policy: Policy,
write_slot: SlotWriter,
restart: Restart,
/// Where korp-start lays the booted slot's release declaration (`/run/korp/release`; a
/// bench box's own directory).
pub release_dir: PathBuf,
}
impl LiveSlot {
pub fn new(esp: Esp, policy: Policy, write_slot: SlotWriter, restart: Restart) -> LiveSlot {
LiveSlot {
esp,
policy,
write_slot,
restart,
release_dir: PathBuf::from(korp_installer::release::RUN_DIR),
}
}
fn korp_file(&self, rel: &str) -> PathBuf {
self.esp.root.join("korp").join(rel)
}
/// **What this box runs** (`GET /live/running`).
pub fn running(&self) -> Result<Running, String> {
let s = self.esp.read_setting()?;
let pin_at = self.esp.records(s.active).pin;
let pin = std::fs::read_to_string(&pin_at).map_err(|e| {
format!(
"[live-running] the running slot's pin {}: {e}",
pin_at.display()
)
})?;
Ok(Running {
base: korp::running_base(&self.esp)?,
app: pin.trim().to_string(),
slot: korp::render_setting(&s).trim().to_string(),
live_update: self.policy.live_update,
})
}
/// **A new app** (`POST /live/stage`): verified against this box's enrolled root, floor and
/// BASE, written to the slot not running, the setting last; then the box is asked to stop
/// gracefully and re-exec. Refused by name (korp's words) when any leg fails: nothing moves.
pub fn stage(&self, st: &Stage) -> Result<String, String> {
self.stage_declared(st, &Declared::default())
}
/// **A new app with what its release declares** (lane B5, korp `stage_declared`): the
/// release declaration verified for THIS app and laid beside its pin (`[release-sig]`
/// refuses the whole stage), the release's env held against this box's cmdline
/// (`[live-env-moved]`), and a stage that carries no declaration refused while this box
/// carries one (`[live-release-absent]`: the box keeps its app and its release, the front
/// keep-boots it). Then the graceful re-exec.
pub fn stage_declared(&self, st: &Stage, d: &Declared) -> Result<String, String> {
let pubkey = std::fs::read(self.korp_file("root.pub"))
.ok()
.and_then(|b| <[u8; 32]>::try_from(b.as_slice()).ok());
let floor = self.floor().map_err(|e| format!("[live-floor] {e:#}"))?;
let base = korp::running_base(&self.esp)?;
let p = korp::LivePayload {
archive: &st.archive,
pin_hex: &st.pin_hex,
signature: Some(&st.signature),
declared_base: Some(&st.base),
};
let decl = korp::Declaration {
release: d
.release
.as_ref()
.map(|(b, s)| (b.as_slice(), (!s.is_empty()).then_some(s.as_slice()))),
env: d.env.as_deref(),
};
let mut write = |slot: Slot, bytes: &[u8]| (self.write_slot)(slot, bytes);
let staged = korp::stage_declared(
&self.esp,
&p,
&decl,
pubkey.as_ref(),
floor,
&base,
self.policy,
&mut write,
)?;
for line in &staged.said {
eprintln!("holger-live: {line}");
}
STAGED.store(true, Ordering::SeqCst);
(self.restart)();
Ok(format!(
"staged into slot {:?} ({} bytes, app {}; {}); stopping gracefully, then re-exec",
staged.slot,
st.archive.len(),
st.pin_hex,
staged.said.join("; ")
))
}
/// **The rollback floor a stage is held against** (lane B3): on a korp box the store
/// korp-start reads (`rollback::floor_for_live`: the chip's witness when there is one),
/// so a TPM box's stage is never refused by a store that cannot read a witness.
fn floor(&self) -> Result<u64, String> {
#[cfg(target_os = "linux")]
let f = korp_installer::rollback::floor_for_live(&self.esp.root, self.esp.remount);
#[cfg(not(target_os = "linux"))]
let f = korp_installer::rollback::FloorStore::floor(
&mut korp_installer::rollback::FloorFile::at(self.korp_file("rollback.floor")),
);
f.map_err(|e| format!("{e:#}"))
}
/// Whether the running slot is on trial (booted once, not yet confirmed).
pub fn on_trial(&self) -> bool {
self.esp
.read_setting()
.map(|s| s.trial == Trial::Booted)
.unwrap_or(false)
}
/// **One look of the trial watcher**: on a slot on trial, confirm it once the app has served
/// for [`CONFIRM_AFTER`]; [`ConfirmStep::RollBack`] tells the caller to re-exec.
pub fn confirm_step(
&self,
up: Duration,
serving_since: Option<Duration>,
) -> Result<ConfirmStep, String> {
let step = confirm_plan(self.on_trial(), up, serving_since);
if step == ConfirmStep::Confirm {
korp::confirm_on(&self.esp)?;
}
Ok(step)
}
/// **The bench's korp-start** (feature `bench`): what korp-start does with the setting when
/// the box re-execs — a staged slot is marked booted, a booted one never confirmed is rolled
/// back. Answers korp-start's sentence.
#[cfg(any(test, feature = "bench"))]
pub fn boot_as_korp_start(&self) -> Result<String, String> {
let plan = korp::plan_boot(&self.esp, self.policy.live_update)?;
if let Some(next) = plan.write {
self.esp.write(&[(
PathBuf::from("korp").join(korp::SETTING_FILE),
korp::render_setting(&next).into_bytes(),
)])?;
}
// The booted slot's release declaration, as korp-start lays it (lane B5): verified for
// the slot's app, under `release_dir`; none, or one that does not hold, lays nothing.
let pin = std::fs::read_to_string(&plan.records.pin)
.map_err(|e| format!("[live-running] {}: {e}", plan.records.pin.display()))?;
let root = korp_installer::loader::parse_blake3_hex(pin.trim())
.map_err(|e| format!("[live-pin] {e:#}"))?;
let pubkey = std::fs::read(self.korp_file("root.pub"))
.ok()
.and_then(|b| <[u8; 32]>::try_from(b.as_slice()).ok());
let files = korp::release_of(
&plan.records,
&root,
pubkey.as_ref(),
self.policy.require_signed,
)
.ok()
.flatten()
.unwrap_or_default();
korp_installer::release::expose(&files, &self.release_dir)
.map_err(|e| format!("[release-expose] {e}"))?;
Ok(plan.said)
}
}
/// **The box's own slot on a korp appliance** (Linux): `/boot` as korp-start mounted it, the
/// policy from the baked cmdline, the slot partitions, and the graceful stop then re-exec.
#[cfg(target_os = "linux")]
pub mod appliance {
use super::*;
/// This box's live policy: `korp.live_update`, `require_signed` as korp-start decided it.
pub fn policy() -> Policy {
let cl = korp_installer::cmdline::effective();
let signed = korp_installer::cmdline::decide(
&cl,
std::env::var(korp_installer::cmdline::REQUIRE_SIGNED_ENV)
.ok()
.as_deref(),
)
.require_signed;
Policy {
live_update: cl.live_update_on(),
require_signed: signed,
}
}
/// The slot of this box.
pub fn slot() -> LiveSlot {
LiveSlot::new(
Esp::boot(),
policy(),
Box::new(korp::write_slot_partition),
Box::new(stop_then_reexec),
)
}
/// SIGTERM to PID 1 itself a second from now (after the stage's answer left): the ordinary
/// graceful stop, which ends in [`reexec_now`] because [`staged`] is set. A stop that has
/// not reached its end within [`STOP_GRACE`] is re-exec'd from here (synced first).
fn stop_then_reexec() {
// A28 M18, stays a thread: it IS the stop — two sleeps, a signal and the re-exec; nothing outlives it.
let _ = std::thread::Builder::new()
.name("live-stop".into())
.spawn(|| {
std::thread::sleep(Duration::from_secs(1));
let _ = nix::sys::signal::kill(
nix::unistd::Pid::this(),
nix::sys::signal::Signal::SIGTERM,
);
std::thread::sleep(STOP_GRACE);
let e = reexec_now();
eprintln!("holger-live: the re-exec after the grace FAILED: {e}");
});
}
/// **The re-exec**: synced, then korp-start (`/init`) again. Only returns when it failed.
pub fn reexec_now() -> String {
nix::unistd::sync();
let argv = [std::ffi::CString::new("/init").expect("no NUL")];
let envp: Vec<std::ffi::CString> = std::env::vars()
.filter_map(|(k, v)| std::ffi::CString::new(format!("{k}={v}")).ok())
.collect();
match korp::reexec(std::path::Path::new("/init"), &argv, &envp) {
Ok(never) => match never {},
Err(e) => e,
}
}
/// **The end of PID 1's stop**: a staged live update re-execs; otherwise `power_off` runs.
pub fn end_of_stop<R>(power_off: impl FnOnce() -> R) -> R {
if after_stop(staged()) == AfterStop::ReExec {
let e = reexec_now();
eprintln!("holger-live: the live update's re-exec FAILED ({e}); powering off");
}
power_off()
}
/// Whether this process runs on a korp ESP at all (a dev run has none).
pub fn present() -> bool {
Esp::boot().root.join("korp").is_dir()
}
}
/// **A box's ESP on the bench** (feature `bench`): a korp ESP in a directory — a kernel, a
/// start-app, the baked cmdline, the enrolled root key and slot A's app — so the live update runs
/// korp's own legs with no PID 1. The slots' partitions are a record of what was written; the
/// re-exec is [`LiveSlot::boot_as_korp_start`].
#[cfg(feature = "bench")]
pub mod bench {
use super::*;
use std::sync::{Arc, Mutex};
/// The root key a bench box enrols (`seed` byte repeated).
pub fn root_key(seed: u8) -> ed25519_dalek::SigningKey {
ed25519_dalek::SigningKey::from_bytes(&[seed; 32])
}
/// `archive`'s pin (hex) and its signature as a release signs it: a VERSIONED record under
/// the root key of `seed`.
pub fn sign(archive: &[u8], seed: u8) -> (String, Vec<u8>) {
use ed25519_dalek::Signer as _;
use korp_installer::rollback::{PayloadVersion, RootSignature};
let pin = blake3::hash(archive);
let raw = root_key(seed)
.sign(&korp_installer::rollback::signed_record(3, &pin).expect("a legal record"))
.to_bytes();
(
pin.to_hex().to_string(),
RootSignature {
version: PayloadVersion::Versioned(3),
signature: raw,
}
.to_blob(),
)
}
/// **A release declaration** `(bundle, signature)` for `archive`, signed as the composer
/// signs it under the root key of `seed` (lane B5).
pub fn sign_release(
files: &[(String, Vec<u8>)],
archive: &[u8],
seed: u8,
) -> (Vec<u8>, Vec<u8>) {
let key = korp_installer::rootkey::RootSigningKey::from_seed(&[seed; 32], "bench");
assert_eq!(
key.public_bytes(),
root_key(seed).verifying_key().to_bytes(),
"one root key"
);
let (bundle, sig) = korp_installer::release::sign_bundle(files, archive, Some(&key))
.expect("a legal declaration")
.expect("files declared");
(bundle, sig.expect("signed"))
}
/// A release declaration's bundle of `files`, unsigned (a test's tampered one).
pub fn encode_release(files: &[(String, Vec<u8>)]) -> Vec<u8> {
korp_installer::release::encode(files).expect("a legal declaration")
}
/// A box laid on the bench: its slot, and what its slot partitions were written.
pub struct BenchBox {
pub slot: LiveSlot,
pub written: Arc<Mutex<Vec<(Slot, Vec<u8>)>>>,
}
impl BenchBox {
/// **Lay a box** at `dir`: kernel `kernel` (a different kernel is a different BASE), the
/// root key of `seed` enrolled, slot A running `app`, live update on and signed.
pub fn lay(
dir: &std::path::Path,
kernel: &str,
seed: u8,
app: &[u8],
) -> Result<BenchBox, String> {
let w = |rel: &str, b: &[u8]| {
let p = dir.join(rel);
std::fs::create_dir_all(p.parent().expect("a parent"))
.and_then(|_| std::fs::write(&p, b))
.map_err(|e| format!("{}: {e}", p.display()))
};
w("vmlinuz", kernel.as_bytes())?;
w("korp-start", b"korp-start")?;
w(
"korp/cmdline",
b"korp.live_update=on korp.require_signed=1 korp.entry=holger",
)?;
w("korp/root.pub", &root_key(seed).verifying_key().to_bytes())?;
w("korp/app.blake3", blake3::hash(app).to_hex().as_bytes())?;
let written: Arc<Mutex<Vec<(Slot, Vec<u8>)>>> = Arc::default();
let into = Arc::clone(&written);
let slot = LiveSlot::new(
Esp {
root: dir.to_path_buf(),
remount: false,
},
Policy {
live_update: true,
require_signed: true,
},
Box::new(move |s, b| {
into.lock()
.map_err(|e| e.to_string())?
.push((s, b.to_vec()));
Ok(())
}),
Box::new(|| {}),
);
let mut slot = slot;
slot.release_dir = dir.join("run/korp/release");
Ok(BenchBox { slot, written })
}
/// [`BenchBox::lay`] with slot A carrying the release declaration `files`, signed for
/// `app` under the root key of `seed` (lane B5).
pub fn lay_declared(
dir: &std::path::Path,
kernel: &str,
seed: u8,
app: &[u8],
files: &[(String, Vec<u8>)],
) -> Result<BenchBox, String> {
let b = Self::lay(dir, kernel, seed, app)?;
let (bundle, sig) = sign_release(files, app, seed);
let r = b.slot.esp.records(Slot::A);
std::fs::write(&r.release, bundle)
.and_then(|_| std::fs::write(&r.release_sig, sig))
.map_err(|e| e.to_string())?;
Ok(b)
}
/// Where this box's korp-start lays its release declaration.
pub fn release_dir(&self) -> &std::path::Path {
&self.slot.release_dir
}
/// This box's BASE.
pub fn base(&self) -> String {
korp::running_base(&self.slot.esp).unwrap_or_default()
}
/// A stage of `archive` for this box's BASE, signed under the root key of `seed`.
pub fn stage_of(&self, archive: &[u8], seed: u8) -> Stage {
let (pin_hex, signature) = sign(archive, seed);
Stage {
pin_hex,
signature,
base: self.base(),
archive: archive.to_vec(),
}
}
}
}