astrid-runtime/astrid · critical

manifest hash mismatch: lock records

Error message

manifest hash mismatch: lock records {recorded}, archive Distro.toml hashes to {actual} — the shuttle is inconsistent or tampered

What it means

check_manifest_binding recomputes the hash of the archive's Distro.toml and compares it to the manifest_hash recorded in the sealed lock. A mismatch means the lock and the manifest inside the shuttle disagree — either the shuttle was assembled inconsistently or the manifest was swapped/tampered after sealing. The install aborts to guarantee the authenticated lock actually binds to this manifest.

Solutions

  1. Rebuild the shuttle from a clean build so lock.manifest_hash matches the archive's Distro.toml
  2. Do not edit Distro.toml after the shuttle is sealed; change the manifest and rebuild
  3. Re-copy the shuttle if media corruption is suspected and retry
Defensive patterns

Strategy: validation

Validate before calling

let actual = manifest_hash(manifest_bytes);
if lock.manifest_hash.as_deref() != Some(actual.as_str()) {
    return Err(anyhow!("manifest hash mismatch: lock vs Distro.toml"));
}

Try / catch

match install_from_shuttle(...) {
    Err(e) if e.to_string().contains("manifest hash mismatch") => {
        // quarantine the shuttle, notify the operator; never retry in place
    }
    ...
}

Prevention

When it happens

Trigger: install_from_shuttle on a shuttle whose Distro.toml was edited after the lock was sealed; a mismatched lock/manifest pair copied from two different distro builds; corruption of either file on the shuttle media.

Common situations: Manually tweaking env/selection in Distro.toml on a prepared shuttle; mixing files from two build attempts onto one shuttle; transport corruption that survives file copy.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/0e7ce506a666621e. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-cli/src/commands/distro/shuttle_install.rs:278

/// to that signature. So when the shuttle is `signed`, a `manifest_hash`
/// is mandatory and must match — a `None` is a hard fail (an attacker
/// could otherwise keep a signed lock+sig+pubkey and swap only the
/// manifest, leaving env/selection unauthenticated). For an unsigned
/// install there is no signed lock to bind against, so a `None` is
/// tolerated and a present hash is still checked best-effort.
///
/// Pure (no I/O) so the binding gate is unit-testable.
fn check_manifest_binding(
    distro_id: &str,
    signed: bool,
    lock: &DistroLock,
    manifest_bytes: &[u8],
) -> anyhow::Result<()> {
    let actual = manifest_hash(manifest_bytes);
    match &lock.manifest_hash {
        Some(recorded) => {
            if recorded != &actual {
                bail!(
                    "manifest hash mismatch: lock records {recorded}, archive Distro.toml hashes \
                     to {actual} — the shuttle is inconsistent or tampered"
                );
            }
            Ok(())
        },
        None if signed => bail!(
            "signed shuttle for '{distro_id}' is missing its manifest_hash binding — refusing. \
             The signature covers the lock, not Distro.toml; without manifest_hash the manifest \
             (env/selection) is unauthenticated and could be swapped."
        ),
        None => Ok(()),
    }
}

/// Verify the per-capsule blake3 of every lock entry against the bytes
/// actually present in the mirror. Returns an error on the first
/// mismatch or missing file. Pure (no install side effects) so the

View on GitHub (pinned to affd8760f4)