{"record":{"id":"dfab87d59abf19ef","repo":"astrid-runtime/astrid","slug":"signed-channel-generation-rollback-rejected","errorCode":null,"errorMessage":"signed channel generation rollback rejected","messagePattern":"signed channel generation rollback rejected","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/astrid-cli/src/commands/update_channel.rs","lineNumber":755,"sourceCode":"        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),\n        Err(error) => return Err(error).context(\"could not read accepted channel state\"),\n    };\n    let text =\n        std::str::from_utf8(&previous_bytes).context(\"accepted channel state is not UTF-8\")?;\n    let previous: ChannelPointer =\n        toml::from_str(text).context(\"accepted channel state is invalid TOML\")?;\n    validate_pointer(&previous, channel, None)?;\n    enforce_continuity_values(candidate, candidate_bytes, &previous, &previous_bytes)\n}\n\nfn enforce_continuity_values(\n    candidate: &ChannelPointer,\n    candidate_bytes: &[u8],\n    previous: &ChannelPointer,\n    previous_bytes: &[u8],\n) -> anyhow::Result<()> {\n    if candidate.generation < previous.generation {\n        bail!(\"signed channel generation rollback rejected\");\n    }\n    if candidate.generation == previous.generation && candidate_bytes != previous_bytes {\n        bail!(\"signed channel same-generation equivocation rejected\");\n    }\n    Ok(())\n}\n\npub(super) fn persist_accepted(\n    channel: UpdateChannel,\n    pointer: &[u8],\n    bundle: &[u8],\n) -> anyhow::Result<()> {\n    let (pointer_path, bundle_path) = state_paths(channel)?;\n    let dir = pointer_path\n        .parent()\n        .ok_or_else(|| anyhow::anyhow!(\"channel state path has no parent\"))?;\n    std::fs::create_dir_all(dir).context(\"could not create channel state directory\")?;\n    // The pointer is the continuity commit marker and is replaced last. A","sourceCodeStart":737,"sourceCodeEnd":773,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/update_channel.rs#L737-L773","documentation":"This guard enforces monotonic generation numbers on the signed update channel: a newly proposed channel pointer whose generation is lower than the currently persisted previous generation is rejected. This prevents rollback attacks where an attacker republishes an older signed channel file to downgrade clients.","triggerScenarios":"Attempting to accept/persist a candidate ChannelPointer whose generation field is numerically less than previous.generation — checked in the validation helper at update_channel.rs:755 called from the signed-channel accept flow.","commonSituations":"Publishing from a stale checkout or old release metadata file; a compromised/misconfigured publisher pushes an archived channel JSON; clock/generation bookkeeping mistakes in the release pipeline; manually reverting the channel file on the distribution host.","solutions":["Bump the candidate generation number to at least previous.generation + 1 before publishing","Re-publish from the latest channel pointer state, not a stale local copy","Fetch the current channel file and rebuild the candidate on top of its generation","If a genuine downgrade is required, do it through the sanctioned process rather than lowering generation"],"exampleFix":"// before\nlet candidate = ChannelPointer { generation: previous.generation - 1, .. };\n// after\nlet candidate = ChannelPointer { generation: previous.generation + 1, .. };","handlingStrategy":"validation","validationCode":"fn generation_ok(candidate: &ChannelPointer, previous: &ChannelPointer) -> bool { candidate.generation >= previous.generation }","typeGuard":null,"tryCatchPattern":"if candidate.generation < previous.generation {\n    eprintln!(\"candidate channel is older than the installed one; refusing downgrade\");\n    return Ok(ExitCode::FAILURE);\n}","preventionTips":["Always publish from the latest channel state; refetch before editing","Auto-increment generation in the publish tooling","Monitor the release pipeline for stale-artifact publishes"],"tags":["security","anti-rollback","update-channel"],"backgroundTag":"invalid-state-transition","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}