Skip to content

Commit 5969072

Browse files
say-paulclaude
andcommitted
feat: replace rollback_trigger with next-deployment-id for GRUB fallback detection
Replace the boolean `greenboot_rollback_trigger` GRUB variable with `greenboot_next_deployment_id`, which stores the staged deployment's ID (imageDigest for bootc, checksum for rpm-ostree). This serves a dual purpose: it acts as the rollback trigger and enables GRUB-level kernel fallback detection by comparing the stored ID against the actually booted deployment. Key changes: - grub.rs: replace set/get/unset_rollback_trigger with set/get/unset_next_deployment_id using new string-valued GRUB variable helpers (set_grub_str_var, get_grub_str_var) - handler.rs: add get_booted_deployment_id and get_staged_deployment_id to query deployment IDs from bootc or rpm-ostree; add force flag to handle_rollback to bypass boot_counter checks when making a GRUB fallback permanent - main.rs: add detect_grub_fallback() which compares stored vs booted deployment ID; on mismatch, make fallback permanent via forced rollback and clear next_deployment_id to prevent rollback loops; SetRollbackTrigger now queries the staged deployment ID and sets both next_deployment_id and fallback independently Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6f96afd commit 5969072

3 files changed

Lines changed: 312 additions & 126 deletions

File tree

src/lib/grub.rs

Lines changed: 123 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -82,25 +82,6 @@ fn unset_boot_counter_at(grub_path: &str) -> Result<()> {
8282
unset_grub_var("boot_counter", grub_path)
8383
}
8484

85-
/// sets greenboot_rollback_trigger=1 and fallback=1
86-
pub fn set_rollback_trigger() -> Result<()> {
87-
set_rollback_trigger_at(GRUB_PATH)
88-
}
89-
90-
fn set_rollback_trigger_at(grub_path: &str) -> Result<()> {
91-
set_grub_var("greenboot_rollback_trigger", 1, grub_path)?;
92-
set_grub_var("fallback", 1, grub_path)
93-
}
94-
95-
/// unsets greenboot_rollback_trigger
96-
pub fn unset_rollback_trigger() -> Result<()> {
97-
unset_rollback_trigger_at(GRUB_PATH)
98-
}
99-
100-
fn unset_rollback_trigger_at(grub_path: &str) -> Result<()> {
101-
unset_grub_var("greenboot_rollback_trigger", grub_path)
102-
}
103-
10485
/// sets fallback=1 for GRUB-level kernel fallback protection
10586
pub fn set_fallback() -> Result<()> {
10687
set_fallback_at(GRUB_PATH)
@@ -128,13 +109,31 @@ fn get_fallback_at(grub_path: &str) -> Result<bool> {
128109
get_grub_bool_var("fallback", grub_path)
129110
}
130111

131-
/// gets greenboot_rollback_trigger value, returns true if set to 1
132-
pub fn get_rollback_trigger() -> Result<bool> {
133-
get_rollback_trigger_at(GRUB_PATH)
112+
/// sets greenboot_next_deployment_id to the given deployment ID string
113+
pub fn set_next_deployment_id(id: &str) -> Result<()> {
114+
set_next_deployment_id_at(id, GRUB_PATH)
115+
}
116+
117+
fn set_next_deployment_id_at(id: &str, grub_path: &str) -> Result<()> {
118+
set_grub_str_var("greenboot_next_deployment_id", id, grub_path)
134119
}
135120

136-
fn get_rollback_trigger_at(grub_path: &str) -> Result<bool> {
137-
get_grub_bool_var("greenboot_rollback_trigger", grub_path)
121+
/// unsets greenboot_next_deployment_id
122+
pub fn unset_next_deployment_id() -> Result<()> {
123+
unset_next_deployment_id_at(GRUB_PATH)
124+
}
125+
126+
fn unset_next_deployment_id_at(grub_path: &str) -> Result<()> {
127+
unset_grub_var("greenboot_next_deployment_id", grub_path)
128+
}
129+
130+
/// returns the stored deployment ID, or None if not set
131+
pub fn get_next_deployment_id() -> Result<Option<String>> {
132+
get_next_deployment_id_at(GRUB_PATH)
133+
}
134+
135+
fn get_next_deployment_id_at(grub_path: &str) -> Result<Option<String>> {
136+
get_grub_str_var("greenboot_next_deployment_id", grub_path)
138137
}
139138

140139
fn get_grub_bool_var(key: &str, grub_path: &str) -> Result<bool> {
@@ -194,12 +193,56 @@ fn set_grub_var(key: &str, val: u16, grub_path: &str) -> Result<()> {
194193
Ok(())
195194
}
196195

196+
fn set_grub_str_var(key: &str, val: &str, grub_path: &str) -> Result<()> {
197+
let grub_result = Command::new("grub2-editenv")
198+
.arg(grub_path)
199+
.arg("set")
200+
.arg(format!("{key}={val}"))
201+
.status()
202+
.context("Unable to set grubenv")?;
203+
204+
if !grub_result.success() {
205+
bail!("Failed to set grubenv key: {key}");
206+
}
207+
208+
log::info!("Set grubenv: {key}={val}");
209+
Ok(())
210+
}
211+
212+
fn get_grub_str_var(key: &str, grub_path: &str) -> Result<Option<String>> {
213+
let grub_vars = Command::new("grub2-editenv")
214+
.arg(grub_path)
215+
.arg("list")
216+
.output()
217+
.context(format!("Unable to list grubenv variables for key: {key}"))?;
218+
219+
if !grub_vars.status.success() {
220+
bail!(
221+
"grub2-editenv failed to list variables: {}",
222+
String::from_utf8_lossy(&grub_vars.stderr)
223+
);
224+
}
225+
226+
let prefix = format!("{key}=");
227+
let output = String::from_utf8_lossy(&grub_vars.stdout);
228+
for line in output.lines() {
229+
if let Some(value) = line.strip_prefix(&prefix) {
230+
let trimmed = value.trim();
231+
if trimmed.is_empty() {
232+
return Ok(None);
233+
}
234+
return Ok(Some(trimmed.to_string()));
235+
}
236+
}
237+
Ok(None)
238+
}
239+
197240
#[cfg(test)]
198241
mod tests {
199242
use super::{
200-
get_boot_counter_at, get_fallback_at, get_rollback_trigger_at, set_boot_counter_at,
201-
set_fallback_at, set_rollback_trigger_at, unset_boot_counter_at, unset_fallback_at,
202-
unset_rollback_trigger_at,
243+
get_boot_counter_at, get_fallback_at, get_next_deployment_id_at, set_boot_counter_at,
244+
set_fallback_at, set_next_deployment_id_at, unset_boot_counter_at, unset_fallback_at,
245+
unset_next_deployment_id_at,
203246
};
204247
use anyhow::Context;
205248
use std::fs;
@@ -273,41 +316,66 @@ mod tests {
273316
}
274317

275318
#[test]
276-
fn test_rollback_trigger_functions() {
319+
fn test_next_deployment_id_set_and_get() {
277320
let (_temp_dir, grubenv) = setup_test_paths();
278321

279-
assert!(!get_rollback_trigger_at(&grubenv).unwrap());
280-
assert!(!get_fallback_at(&grubenv).unwrap());
322+
assert_eq!(get_next_deployment_id_at(&grubenv).unwrap(), None);
281323

282-
set_rollback_trigger_at(&grubenv).unwrap();
283-
assert!(get_rollback_trigger_at(&grubenv).unwrap());
284-
assert!(get_fallback_at(&grubenv).unwrap());
324+
let id = "sha256:abc123def456";
325+
set_next_deployment_id_at(id, &grubenv).unwrap();
326+
assert_eq!(
327+
get_next_deployment_id_at(&grubenv).unwrap(),
328+
Some(id.to_string())
329+
);
330+
}
285331

286-
// unset_rollback_trigger only clears the trigger, not fallback
287-
unset_rollback_trigger_at(&grubenv).unwrap();
288-
assert!(!get_rollback_trigger_at(&grubenv).unwrap());
289-
assert!(get_fallback_at(&grubenv).unwrap());
332+
#[test]
333+
fn test_next_deployment_id_unset() {
334+
let (_temp_dir, grubenv) = setup_test_paths();
290335

291-
// fallback has its own lifecycle, unset independently
292-
unset_fallback_at(&grubenv).unwrap();
293-
assert!(!get_fallback_at(&grubenv).unwrap());
336+
let id = "sha256:abc123def456";
337+
set_next_deployment_id_at(id, &grubenv).unwrap();
338+
assert!(get_next_deployment_id_at(&grubenv).unwrap().is_some());
339+
340+
unset_next_deployment_id_at(&grubenv).unwrap();
341+
assert_eq!(get_next_deployment_id_at(&grubenv).unwrap(), None);
294342
}
295343

296344
#[test]
297-
fn test_rollback_trigger_with_other_vars() {
345+
fn test_next_deployment_id_coexists_with_boot_counter() {
298346
let (_temp_dir, grubenv) = setup_test_paths();
299347

348+
let id = "sha256:abc123def456";
300349
set_boot_counter_at(3, &grubenv).unwrap();
301-
set_rollback_trigger_at(&grubenv).unwrap();
350+
set_next_deployment_id_at(id, &grubenv).unwrap();
302351

303352
assert_eq!(get_boot_counter_at(&grubenv).unwrap(), Some(3));
304-
assert!(get_rollback_trigger_at(&grubenv).unwrap());
305-
assert!(get_fallback_at(&grubenv).unwrap());
353+
assert_eq!(
354+
get_next_deployment_id_at(&grubenv).unwrap(),
355+
Some(id.to_string())
356+
);
306357

307-
// unset_rollback_trigger leaves boot_counter and fallback intact
308-
unset_rollback_trigger_at(&grubenv).unwrap();
358+
unset_next_deployment_id_at(&grubenv).unwrap();
309359
assert_eq!(get_boot_counter_at(&grubenv).unwrap(), Some(3));
310-
assert!(!get_rollback_trigger_at(&grubenv).unwrap());
360+
assert_eq!(get_next_deployment_id_at(&grubenv).unwrap(), None);
361+
}
362+
363+
#[test]
364+
fn test_next_deployment_id_coexists_with_fallback() {
365+
let (_temp_dir, grubenv) = setup_test_paths();
366+
367+
let id = "sha256:abc123def456";
368+
set_next_deployment_id_at(id, &grubenv).unwrap();
369+
set_fallback_at(&grubenv).unwrap();
370+
371+
assert_eq!(
372+
get_next_deployment_id_at(&grubenv).unwrap(),
373+
Some(id.to_string())
374+
);
375+
assert!(get_fallback_at(&grubenv).unwrap());
376+
377+
unset_next_deployment_id_at(&grubenv).unwrap();
378+
assert_eq!(get_next_deployment_id_at(&grubenv).unwrap(), None);
311379
assert!(get_fallback_at(&grubenv).unwrap());
312380
}
313381

@@ -340,36 +408,30 @@ mod tests {
340408
assert!(!get_fallback_at(&grubenv).unwrap());
341409
}
342410

343-
#[test]
344-
fn test_fallback_set_via_rollback_trigger() {
345-
let (_temp_dir, grubenv) = setup_test_paths();
346-
347-
set_rollback_trigger_at(&grubenv).unwrap();
348-
assert!(get_fallback_at(&grubenv).unwrap());
349-
assert!(get_rollback_trigger_at(&grubenv).unwrap());
350-
}
351-
352411
#[test]
353412
fn test_fallback_coexists_with_boot_counter() {
354413
let (_temp_dir, grubenv) = setup_test_paths();
355414

356415
set_boot_counter_at(3, &grubenv).unwrap();
357-
set_rollback_trigger_at(&grubenv).unwrap();
416+
set_fallback_at(&grubenv).unwrap();
358417

359418
assert_eq!(get_boot_counter_at(&grubenv).unwrap(), Some(3));
360419
assert!(get_fallback_at(&grubenv).unwrap());
361-
assert!(get_rollback_trigger_at(&grubenv).unwrap());
362420
}
363421

364422
#[test]
365-
fn test_fallback_independent_unset() {
423+
fn test_fallback_independent_of_deployment_id() {
366424
let (_temp_dir, grubenv) = setup_test_paths();
367425

368-
set_rollback_trigger_at(&grubenv).unwrap();
426+
let id = "sha256:abc123def456";
427+
set_next_deployment_id_at(id, &grubenv).unwrap();
428+
set_fallback_at(&grubenv).unwrap();
429+
430+
unset_next_deployment_id_at(&grubenv).unwrap();
431+
assert_eq!(get_next_deployment_id_at(&grubenv).unwrap(), None);
369432
assert!(get_fallback_at(&grubenv).unwrap());
370433

371434
unset_fallback_at(&grubenv).unwrap();
372435
assert!(!get_fallback_at(&grubenv).unwrap());
373-
assert!(get_rollback_trigger_at(&grubenv).unwrap());
374436
}
375437
}

0 commit comments

Comments
 (0)