Skip to content

Commit 33c84f1

Browse files
composefs/soft-reboot: Handle soft reboot for UKIs
Similar to soft reboots for Type1 entries, we compute the SHA256Sum of .linux + .initrd sections in the UKI, and compare them to check for kernel skew Next, compare the .cmdline section skipping the `composefs=` parameter as that will always be different Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
1 parent e0f64c5 commit 33c84f1

File tree

4 files changed

+207
-74
lines changed

4 files changed

+207
-74
lines changed

crates/lib/src/bootc_composefs/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ pub(crate) mod state;
1010
pub(crate) mod status;
1111
pub(crate) mod switch;
1212
pub(crate) mod update;
13+
pub(crate) mod utils;

crates/lib/src/bootc_composefs/state.rs

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use std::io::Write;
2-
use std::ops::Deref;
32
use std::os::unix::fs::symlink;
43
use std::path::Path;
54
use std::{fs::create_dir_all, process::Command};
@@ -108,20 +107,22 @@ pub(crate) fn copy_etc_to_state(
108107
cp_ret
109108
}
110109

111-
/// Updates the currently booted image's target imgref
112-
pub(crate) fn update_target_imgref_in_origin(
110+
/// Adds or updates the provided key/value pairs in the .origin file of the deployment pointed to
111+
/// by the `deployment_id`
112+
fn add_update_in_origin(
113113
storage: &Storage,
114-
booted_cfs: &BootedComposefs,
115-
imgref: &ImageReference,
114+
deployment_id: &str,
115+
section: &str,
116+
kv_pairs: &[(&str, &str)],
116117
) -> Result<()> {
117-
let path = Path::new(STATE_DIR_RELATIVE).join(booted_cfs.cmdline.digest.deref());
118+
let path = Path::new(STATE_DIR_RELATIVE).join(deployment_id);
118119

119120
let state_dir = storage
120121
.physical_root
121122
.open_dir(path)
122123
.context("Opening state dir")?;
123124

124-
let origin_filename = format!("{}.origin", booted_cfs.cmdline.digest.deref());
125+
let origin_filename = format!("{deployment_id}.origin");
125126

126127
let origin_file = state_dir
127128
.read_to_string(&origin_filename)
@@ -130,11 +131,9 @@ pub(crate) fn update_target_imgref_in_origin(
130131
let mut ini =
131132
tini::Ini::from_string(&origin_file).context("Failed to parse file origin file as ini")?;
132133

133-
// Replace the origin
134-
ini = ini.section("origin").item(
135-
ORIGIN_CONTAINER,
136-
format!("ostree-unverified-image:{imgref}"),
137-
);
134+
for (key, value) in kv_pairs {
135+
ini = ini.section(section).item(*key, *value);
136+
}
138137

139138
state_dir
140139
.atomic_replace_with(origin_filename, move |f| -> std::io::Result<_> {
@@ -151,6 +150,36 @@ pub(crate) fn update_target_imgref_in_origin(
151150
Ok(())
152151
}
153152

153+
/// Updates the currently booted image's target imgref
154+
pub(crate) fn update_target_imgref_in_origin(
155+
storage: &Storage,
156+
booted_cfs: &BootedComposefs,
157+
imgref: &ImageReference,
158+
) -> Result<()> {
159+
add_update_in_origin(
160+
storage,
161+
booted_cfs.cmdline.digest.as_ref(),
162+
"origin",
163+
&[(
164+
ORIGIN_CONTAINER,
165+
&format!("ostree-unverified-image:{imgref}"),
166+
)],
167+
)
168+
}
169+
170+
pub(crate) fn update_boot_digest_in_origin(
171+
storage: &Storage,
172+
digest: &str,
173+
boot_digest: &str,
174+
) -> Result<()> {
175+
add_update_in_origin(
176+
storage,
177+
digest,
178+
ORIGIN_KEY_BOOT,
179+
&[(ORIGIN_KEY_BOOT_DIGEST, boot_digest)],
180+
)
181+
}
182+
154183
/// Creates and populates /sysroot/state/deploy/image_id
155184
#[context("Writing composefs state")]
156185
pub(crate) fn write_composefs_state(
@@ -159,7 +188,7 @@ pub(crate) fn write_composefs_state(
159188
imgref: &ImageReference,
160189
staged: bool,
161190
boot_type: BootType,
162-
boot_digest: Option<String>,
191+
boot_digest: String,
163192
) -> Result<()> {
164193
let state_path = root_path
165194
.join(STATE_DIR_RELATIVE)
@@ -197,11 +226,9 @@ pub(crate) fn write_composefs_state(
197226
.section(ORIGIN_KEY_BOOT)
198227
.item(ORIGIN_KEY_BOOT_TYPE, boot_type);
199228

200-
if let Some(boot_digest) = boot_digest {
201-
config = config
202-
.section(ORIGIN_KEY_BOOT)
203-
.item(ORIGIN_KEY_BOOT_DIGEST, boot_digest);
204-
}
229+
config = config
230+
.section(ORIGIN_KEY_BOOT)
231+
.item(ORIGIN_KEY_BOOT_DIGEST, boot_digest);
205232

206233
let state_dir =
207234
Dir::open_ambient_dir(&state_path, ambient_authority()).context("Opening state dir")?;

crates/lib/src/bootc_composefs/status.rs

Lines changed: 103 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ use bootc_mount::inspect_filesystem;
66
use fn_error_context::context;
77

88
use crate::{
9-
bootc_composefs::boot::BootType,
9+
bootc_composefs::{
10+
boot::BootType,
11+
utils::{compute_store_boot_digest_for_uki, get_uki_cmdline},
12+
},
1013
composefs_consts::{
1114
COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT_DIGEST, TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED, USER_CFG,
1215
},
@@ -307,26 +310,33 @@ pub(crate) async fn get_composefs_status(
307310
composefs_deployment_status_from(&storage, booted_cfs.cmdline).await
308311
}
309312

310-
fn set_soft_reboot_capable_bls(
313+
/// Check whether any deployment is capable of being soft rebooted or not
314+
#[context("Checking soft reboot capability")]
315+
fn set_soft_reboot_capability(
311316
storage: &Storage,
312317
host: &mut Host,
313-
bls_entries: &Vec<BLSConfig>,
318+
bls_entries: Option<Vec<BLSConfig>>,
314319
cmdline: &ComposefsCmdline,
315320
) -> Result<()> {
316321
let booted = host.require_composefs_booted()?;
317322

318323
match booted.boot_type {
319324
BootType::Bls => {
320-
set_reboot_capable_type1_deployments(storage, cmdline, host, bls_entries)?;
321-
}
325+
let mut bls_entries =
326+
bls_entries.ok_or_else(|| anyhow::anyhow!("BLS entries not provided"))?;
322327

323-
BootType::Uki => match booted.bootloader {
324-
Bootloader::Grub => todo!(),
325-
Bootloader::Systemd => todo!(),
326-
},
327-
};
328+
let staged_entries =
329+
get_sorted_staged_type1_boot_entries(storage.require_boot_dir()?, false)?;
328330

329-
Ok(())
331+
// We will have a duplicate booted entry here, but that's fine as we only use this
332+
// vector to check for existence of an entry
333+
bls_entries.extend(staged_entries);
334+
335+
set_reboot_capable_type1_deployments(cmdline, host, bls_entries)
336+
}
337+
338+
BootType::Uki => set_reboot_capable_uki_deployments(storage, cmdline, host),
339+
}
330340
}
331341

332342
fn find_bls_entry<'a>(
@@ -363,73 +373,112 @@ fn compare_cmdline_skip_cfs(first: &Cmdline<'_>, second: &Cmdline<'_>) -> bool {
363373
return true;
364374
}
365375

366-
fn set_soft_reboot_capable_type1(
367-
deployment: &mut BootEntry,
368-
bls_entries: &Vec<BLSConfig>,
369-
booted_bls_entry: &BLSConfig,
370-
booted_boot_digest: &String,
376+
#[context("Setting soft reboot capability for Type1 entries")]
377+
fn set_reboot_capable_type1_deployments(
378+
booted_cmdline: &ComposefsCmdline,
379+
host: &mut Host,
380+
bls_entries: Vec<BLSConfig>,
371381
) -> Result<()> {
372-
let deployment_cfs = deployment.require_composefs()?;
382+
let booted = host
383+
.status
384+
.booted
385+
.as_ref()
386+
.ok_or_else(|| anyhow::anyhow!("Failed to find booted entry"))?;
387+
388+
let booted_boot_digest = booted.composefs_boot_digest()?;
389+
390+
let booted_bls_entry = find_bls_entry(&*booted_cmdline.digest, &bls_entries)?
391+
.ok_or_else(|| anyhow::anyhow!("Booted BLS entry not found"))?;
373392

374-
// TODO: Unwrap
375-
if deployment_cfs.boot_digest.as_ref().unwrap() != booted_boot_digest {
376-
deployment.soft_reboot_capable = false;
377-
return Ok(());
393+
let booted_cmdline = booted_bls_entry.get_cmdline()?;
394+
395+
for depl in host
396+
.status
397+
.staged
398+
.iter_mut()
399+
.chain(host.status.rollback.iter_mut())
400+
.chain(host.status.other_deployments.iter_mut())
401+
{
402+
let entry = find_bls_entry(&depl.require_composefs()?.verity, &bls_entries)?
403+
.ok_or_else(|| anyhow::anyhow!("Entry not found"))?;
404+
405+
let depl_cmdline = entry.get_cmdline()?;
406+
407+
depl.soft_reboot_capable = is_soft_rebootable(
408+
depl.composefs_boot_digest()?,
409+
booted_boot_digest,
410+
depl_cmdline,
411+
booted_cmdline,
412+
);
378413
}
379414

380-
let entry = find_bls_entry(&deployment_cfs.verity, bls_entries)?
381-
.ok_or_else(|| anyhow::anyhow!("Entry not found"))?;
415+
Ok(())
416+
}
382417

383-
let opts = entry.get_cmdline()?;
384-
let booted_cmdline_opts = booted_bls_entry.get_cmdline()?;
418+
fn is_soft_rebootable(
419+
depl_boot_digest: &str,
420+
booted_boot_digest: &str,
421+
depl_cmdline: &Cmdline,
422+
booted_cmdline: &Cmdline,
423+
) -> bool {
424+
if depl_boot_digest != booted_boot_digest {
425+
tracing::debug!("Soft reboot not allowed due to kernel skew");
426+
return false;
427+
}
385428

386-
if opts.len() != booted_cmdline_opts.len() {
429+
if depl_cmdline.as_bytes().len() != booted_cmdline.as_bytes().len() {
387430
tracing::debug!("Soft reboot not allowed due to differing cmdline");
388-
deployment.soft_reboot_capable = false;
389-
return Ok(());
431+
return false;
390432
}
391433

392-
deployment.soft_reboot_capable = compare_cmdline_skip_cfs(opts, booted_cmdline_opts)
393-
&& compare_cmdline_skip_cfs(booted_cmdline_opts, opts);
394-
395-
return Ok(());
434+
return compare_cmdline_skip_cfs(depl_cmdline, booted_cmdline)
435+
&& compare_cmdline_skip_cfs(booted_cmdline, depl_cmdline);
396436
}
397437

398-
fn set_reboot_capable_type1_deployments(
438+
#[context("Setting soft reboot capability for UKI deployments")]
439+
fn set_reboot_capable_uki_deployments(
399440
storage: &Storage,
400441
cmdline: &ComposefsCmdline,
401442
host: &mut Host,
402-
bls_entries: &Vec<BLSConfig>,
403443
) -> Result<()> {
404444
let booted = host
405445
.status
406446
.booted
407447
.as_ref()
408448
.ok_or_else(|| anyhow::anyhow!("Failed to find booted entry"))?;
409449

410-
let booted_boot_digest = booted.composefs_boot_digest()?;
411-
412-
let booted_bls_entry = find_bls_entry(&*cmdline.digest, bls_entries)?
413-
.ok_or_else(|| anyhow::anyhow!("Booted bls entry not found"))?;
450+
// Since older booted systems won't have the boot digest for UKIs
451+
let booted_boot_digest = match booted.composefs_boot_digest() {
452+
Ok(d) => d,
453+
Err(_) => &compute_store_boot_digest_for_uki(storage, &cmdline.digest)?,
454+
};
414455

415-
if let Some(staged) = host.status.staged.as_mut() {
416-
let staged_entries =
417-
get_sorted_staged_type1_boot_entries(storage.require_boot_dir()?, true)?;
456+
let booted_cmdline = get_uki_cmdline(storage, &booted.require_composefs()?.verity)?;
418457

419-
set_soft_reboot_capable_type1(
420-
staged,
421-
&staged_entries,
422-
booted_bls_entry,
423-
booted_boot_digest,
424-
)?;
425-
}
458+
for deployment in host
459+
.status
460+
.staged
461+
.iter_mut()
462+
.chain(host.status.rollback.iter_mut())
463+
.chain(host.status.other_deployments.iter_mut())
464+
{
465+
// Since older booted systems won't have the boot digest for UKIs
466+
let depl_boot_digest = match deployment.composefs_boot_digest() {
467+
Ok(d) => d,
468+
Err(_) => &compute_store_boot_digest_for_uki(
469+
storage,
470+
&deployment.require_composefs()?.verity,
471+
)?,
472+
};
426473

427-
if let Some(rollback) = &mut host.status.rollback {
428-
set_soft_reboot_capable_type1(rollback, bls_entries, booted_bls_entry, booted_boot_digest)?;
429-
}
474+
let depl_cmdline = get_uki_cmdline(storage, &deployment.require_composefs()?.verity)?;
430475

431-
for depl in &mut host.status.other_deployments {
432-
set_soft_reboot_capable_type1(depl, bls_entries, booted_bls_entry, booted_boot_digest)?;
476+
deployment.soft_reboot_capable = is_soft_rebootable(
477+
depl_boot_digest,
478+
booted_boot_digest,
479+
&depl_cmdline,
480+
&booted_cmdline,
481+
);
433482
}
434483

435484
Ok(())
@@ -602,9 +651,7 @@ pub(crate) async fn composefs_deployment_status_from(
602651
host.spec.boot_order = BootOrder::Rollback
603652
};
604653

605-
if let Some(bls_configs) = sorted_bls_config {
606-
set_soft_reboot_capable_bls(storage, &mut host, &bls_configs, cmdline)?;
607-
}
654+
set_soft_reboot_capability(storage, &mut host, sorted_bls_config, cmdline)?;
608655

609656
Ok(host)
610657
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use crate::{
2+
bootc_composefs::{
3+
boot::{compute_boot_digest_uki, SYSTEMD_UKI_DIR},
4+
state::update_boot_digest_in_origin,
5+
},
6+
store::Storage,
7+
};
8+
use anyhow::Result;
9+
use bootc_kernel_cmdline::utf8::Cmdline;
10+
use fn_error_context::context;
11+
12+
fn get_uki(storage: &Storage, deployment_verity: &str) -> Result<Vec<u8>> {
13+
let uki_dir = storage
14+
.esp
15+
.as_ref()
16+
.ok_or_else(|| anyhow::anyhow!("ESP not mounted"))?
17+
.fd
18+
.open_dir(SYSTEMD_UKI_DIR)?;
19+
20+
let req_fname = format!("{deployment_verity}.efi");
21+
22+
for entry in uki_dir.entries_utf8()? {
23+
let pe = entry?;
24+
25+
let filename = pe.file_name()?;
26+
27+
if filename != req_fname {
28+
continue;
29+
}
30+
31+
return Ok(uki_dir.read(filename)?);
32+
}
33+
34+
anyhow::bail!("UKI for deployment {deployment_verity} not found")
35+
}
36+
37+
#[context("Computing and storing boot digest for UKI")]
38+
pub(crate) fn compute_store_boot_digest_for_uki(
39+
storage: &Storage,
40+
deployment_verity: &str,
41+
) -> Result<String> {
42+
let uki = get_uki(storage, deployment_verity)?;
43+
let digest = compute_boot_digest_uki(&uki)?;
44+
45+
update_boot_digest_in_origin(storage, &deployment_verity, &digest)?;
46+
return Ok(digest);
47+
}
48+
49+
#[context("Getting UKI cmdline")]
50+
pub(crate) fn get_uki_cmdline(
51+
storage: &Storage,
52+
deployment_verity: &str,
53+
) -> Result<Cmdline<'static>> {
54+
let uki = get_uki(storage, deployment_verity)?;
55+
let cmdline = composefs_boot::uki::get_cmdline(&uki)?;
56+
57+
return Ok(Cmdline::from(cmdline.to_owned()));
58+
}

0 commit comments

Comments
 (0)