Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

estate/holger-live/tests/the_live_slot.rs

//! ★ **A Holger box's live slot** (L20): korp's own legs on a bench ESP — the running report,
//! a signed stage for this BASE into the other slot, korp-start's boot of it on trial, the
//! confirm after serving, the rollback of a slot that never confirmed, and every refusal by name.

use std::time::Duration;

use holger_live::bench::BenchBox;
use holger_live::{
    after_stop, confirm_plan, AfterStop, ConfirmStep, CONFIRM_AFTER, CONFIRM_DEADLINE,
};

fn dir(tag: &str) -> std::path::PathBuf {
    let d = std::env::temp_dir().join(format!("holger-live-{tag}-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&d);
    d
}

fn app(n: u8) -> Vec<u8> {
    (0..64 * 1024u32).map(|i| (i % 251) as u8 ^ n).collect()
}

/// ★ Staged, booted on trial, confirmed after serving: the box runs the new app in slot B.
#[test]
fn a_signed_app_for_this_base_is_staged_booted_on_trial_and_confirmed() {
    let d = dir("ok");
    let b = BenchBox::lay(&d, "kernel-1", 42, &app(1)).unwrap();
    let r = b.slot.running().unwrap();
    assert_eq!(
        (r.slot.as_str(), r.live_update, r.app.as_str()),
        ("a", true, blake3::hash(&app(1)).to_hex().as_str())
    );
    let st = b.stage_of(&app(2), 42);
    let said = b.slot.stage(&st).unwrap();
    assert!(said.contains("slot B"), "{said}");
    assert!(holger_live::staged(), "the stop ends in a re-exec");
    assert_eq!(
        b.written
            .lock()
            .unwrap()
            .iter()
            .map(|(s, a)| (*s, a.len()))
            .collect::<Vec<_>>(),
        [(holger_live::Slot::B, app(2).len())]
    );
    assert_eq!(b.slot.running().unwrap().slot, "b trial");
    // The re-exec: korp-start boots the staged slot and marks it booted.
    b.slot.boot_as_korp_start().unwrap();
    let r = b.slot.running().unwrap();
    assert_eq!(
        (r.slot.as_str(), r.app.as_str()),
        ("b booted", st.pin_hex.as_str())
    );
    assert!(b.slot.on_trial());
    assert_eq!(
        b.slot.confirm_step(Duration::from_secs(10), None).unwrap(),
        ConfirmStep::Wait
    );
    assert_eq!(
        b.slot
            .confirm_step(Duration::from_secs(90), Some(Duration::from_secs(20)))
            .unwrap(),
        ConfirmStep::Confirm
    );
    assert_eq!(b.slot.running().unwrap().slot, "b", "confirmed");
    let _ = std::fs::remove_dir_all(&d);
}

/// ★ A slot that never served is rolled back at the next start: the old app in slot A again.
#[test]
fn a_new_app_that_never_confirms_is_rolled_back() {
    let d = dir("back");
    let b = BenchBox::lay(&d, "kernel-1", 42, &app(1)).unwrap();
    b.slot.stage(&b.stage_of(&app(3), 42)).unwrap();
    b.slot.boot_as_korp_start().unwrap();
    assert_eq!(
        b.slot
            .confirm_step(CONFIRM_DEADLINE + Duration::from_secs(1), None)
            .unwrap(),
        ConfirmStep::RollBack
    );
    let said = b.slot.boot_as_korp_start().unwrap();
    assert!(said.contains("rolled back"), "{said}");
    let r = b.slot.running().unwrap();
    assert_eq!(
        (r.slot.as_str(), r.app.as_str()),
        ("a", blake3::hash(&app(1)).to_hex().as_str())
    );
    let _ = std::fs::remove_dir_all(&d);
}

/// ★ Refused by name, nothing written: another root's signature, another BASE, a trial still
/// open, a box whose live switch is off.
#[test]
fn a_stage_that_fails_its_check_is_refused_and_nothing_moves() {
    let d = dir("refuse");
    let b = BenchBox::lay(&d, "kernel-1", 42, &app(1)).unwrap();
    let e = b.slot.stage(&b.stage_of(&app(2), 7)).unwrap_err();
    assert!(
        e.contains("[live-verify]") && e.contains("signature"),
        "{e}"
    );
    let mut other = b.stage_of(&app(2), 42);
    other.base = "f".repeat(64);
    assert!(b
        .slot
        .stage(&other)
        .unwrap_err()
        .contains("[live-base-mismatch]"));
    assert!(b.written.lock().unwrap().is_empty(), "nothing written");
    assert_eq!(b.slot.running().unwrap().slot, "a");
    b.slot.stage(&b.stage_of(&app(2), 42)).unwrap();
    assert!(b
        .slot
        .stage(&b.stage_of(&app(3), 42))
        .unwrap_err()
        .contains("[live-trial-open]"));
    let d2 = dir("off");
    let mut off = BenchBox::lay(&d2, "kernel-1", 42, &app(1)).unwrap();
    off.slot.policy.live_update = false;
    assert!(!off.slot.running().unwrap().live_update);
    assert!(off
        .slot
        .stage(&off.stage_of(&app(2), 42))
        .unwrap_err()
        .contains("[live-update-off]"));
    let _ = std::fs::remove_dir_all(&d);
    let _ = std::fs::remove_dir_all(&d2);
}

/// ★ The pure rules: confirm only after serving, roll back at the deadline; a staged update
/// ends the stop in a re-exec.
#[test]
fn the_trial_rules_and_the_end_of_the_stop() {
    let s = Duration::from_secs;
    assert_eq!(confirm_plan(false, s(9999), None), ConfirmStep::Nothing);
    assert_eq!(confirm_plan(true, s(30), Some(s(20))), ConfirmStep::Wait);
    assert_eq!(
        confirm_plan(true, s(20) + CONFIRM_AFTER, Some(s(20))),
        ConfirmStep::Confirm
    );
    assert_eq!(
        confirm_plan(true, CONFIRM_DEADLINE + s(1), None),
        ConfirmStep::RollBack
    );
    assert_eq!(
        (after_stop(true), after_stop(false)),
        (AfterStop::ReExec, AfterStop::PowerOff)
    );
}

/// ★ A different kernel is a different BASE: what the front reads to choose the keep medium.
#[test]
fn a_different_kernel_is_a_different_base() {
    let (d1, d2) = (dir("k1"), dir("k2"));
    let a = BenchBox::lay(&d1, "kernel-1", 42, &app(1)).unwrap();
    let b = BenchBox::lay(&d2, "kernel-2", 42, &app(1)).unwrap();
    assert_ne!(a.base(), b.base());
    let _ = std::fs::remove_dir_all(&d1);
    let _ = std::fs::remove_dir_all(&d2);
}

/// ★ **A live update with a changed declaration** (lane B5): the box runs `holger.ron` "(one)";
/// the stage carries the new app with "(two)", signed for it. The box stages slot B, korp-start
/// boots it, and the box's `/run/korp/release` holds "(two)". A tampered declaration is refused
/// by name and nothing moves; a stage with none is refused while the box carries one
/// (`[live-release-absent]`: it keeps its app and its release).
#[test]
fn a_live_update_changes_the_release_declaration_and_a_tampered_one_is_refused() {
    use nordisk_pairing::live::Declared;
    let d = dir("declared");
    let one = vec![("holger.ron".to_string(), b"(one)".to_vec())];
    let b = BenchBox::lay_declared(&d, "kernel-1", 42, &app(1), &one).unwrap();
    b.slot.boot_as_korp_start().unwrap();
    assert_eq!(
        std::fs::read(b.release_dir().join("holger.ron")).unwrap(),
        b"(one)",
        "the box's declaration at its boot"
    );
    let st = b.stage_of(&app(2), 42);
    let two = vec![("holger.ron".to_string(), b"(two)".to_vec())];
    let (bundle, sig) = holger_live::bench::sign_release(&two, &app(2), 42);
    // Tampered under the right signature: refused, nothing written.
    let evil = Declared {
        release: Some((
            korp_installer::release::encode(&[("holger.ron".to_string(), b"(evil)".to_vec())])
                .unwrap(),
            sig.clone(),
        )),
        env: None,
    };
    let e = b.slot.stage_declared(&st, &evil).unwrap_err();
    assert!(e.contains("[release-sig]"), "{e}");
    // None at all while the box carries one: kept.
    let e = b.slot.stage(&st).unwrap_err();
    assert!(
        e.contains("[live-release-absent]") && nordisk_pairing::live::falls_to_keep(&e),
        "{e}"
    );
    assert!(b.written.lock().unwrap().is_empty(), "nothing written");
    assert_eq!(b.slot.running().unwrap().slot, "a");
    // The new declaration: staged, booted, exposed.
    let said = b
        .slot
        .stage_declared(
            &st,
            &Declared {
                release: Some((bundle, sig)),
                env: Some(vec![]),
            },
        )
        .unwrap();
    assert!(
        said.contains("slot B") && said.contains("holger.ron"),
        "{said}"
    );
    b.slot.boot_as_korp_start().unwrap();
    assert_eq!(b.slot.running().unwrap().app, st.pin_hex);
    assert_eq!(
        std::fs::read(b.release_dir().join("holger.ron")).unwrap(),
        b"(two)",
        "korp-start lays the new slot's declaration"
    );
    let _ = std::fs::remove_dir_all(&d);
}