astrid-runtime/astrid · critical
signed channel generation rollback rejected
Error message
signed channel generation rollback rejected
What it means
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.
Source
Thrown at crates/astrid-cli/src/commands/update_channel.rs:755
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(error).context("could not read accepted channel state"),
};
let text =
std::str::from_utf8(&previous_bytes).context("accepted channel state is not UTF-8")?;
let previous: ChannelPointer =
toml::from_str(text).context("accepted channel state is invalid TOML")?;
validate_pointer(&previous, channel, None)?;
enforce_continuity_values(candidate, candidate_bytes, &previous, &previous_bytes)
}
fn enforce_continuity_values(
candidate: &ChannelPointer,
candidate_bytes: &[u8],
previous: &ChannelPointer,
previous_bytes: &[u8],
) -> anyhow::Result<()> {
if candidate.generation < previous.generation {
bail!("signed channel generation rollback rejected");
}
if candidate.generation == previous.generation && candidate_bytes != previous_bytes {
bail!("signed channel same-generation equivocation rejected");
}
Ok(())
}
pub(super) fn persist_accepted(
channel: UpdateChannel,
pointer: &[u8],
bundle: &[u8],
) -> anyhow::Result<()> {
let (pointer_path, bundle_path) = state_paths(channel)?;
let dir = pointer_path
.parent()
.ok_or_else(|| anyhow::anyhow!("channel state path has no parent"))?;
std::fs::create_dir_all(dir).context("could not create channel state directory")?;
// The pointer is the continuity commit marker and is replaced last. AView on GitHub (pinned to affd8760f4)
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
Example fix
// before
let candidate = ChannelPointer { generation: previous.generation - 1, .. };
// after
let candidate = ChannelPointer { generation: previous.generation + 1, .. }; Defensive patterns
Strategy: validation
Validate before calling
fn generation_ok(candidate: &ChannelPointer, previous: &ChannelPointer) -> bool { candidate.generation >= previous.generation } Try / catch
if candidate.generation < previous.generation {
eprintln!("candidate channel is older than the installed one; refusing downgrade");
return Ok(ExitCode::FAILURE);
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- signed channel same-generation equivocation rejected
- {label} release metadata identity is invalid
- {label} release metadata does not match the authenticated le
- {label} release metadata does not bind the authenticated leg
- manifest exceeds its installed capability approval: {details
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/dfab87d59abf19ef.
Report an issue: GitHub.