Skip to content

Commit 9e8a3ef

Browse files
composefs/soft-reboot: Check for SELinux policy divergence
Until now while checking if a deployment is capable of being soft rebooted, we were not taking into account any differences in SELinux policies between the two deployments. This commit adds such a check We only check for policy diff if SELinux is enabled Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com> composefs: Refactor Add doc comments for StagedDeployment struct Use `serde_json::to_writer` to prevent intermediate string allocation Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com> composefs/selinux: More refactor Move SELinux realted oprations to a separate module Minor refactoring and add some comments Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
1 parent 235818a commit 9e8a3ef

7 files changed

Lines changed: 230 additions & 42 deletions

File tree

crates/lib/src/bootc_composefs/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub(crate) mod finalize;
66
pub(crate) mod gc;
77
pub(crate) mod repo;
88
pub(crate) mod rollback;
9+
pub(crate) mod selinux;
910
pub(crate) mod service;
1011
pub(crate) mod soft_reboot;
1112
pub(crate) mod state;
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use anyhow::{Context, Result};
2+
use bootc_initramfs_setup::mount_composefs_image;
3+
use bootc_mount::tempmount::TempMount;
4+
use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
5+
use cap_std_ext::dirext::CapStdExtDirExt;
6+
use fn_error_context::context;
7+
8+
use crate::bootc_composefs::status::ComposefsCmdline;
9+
use crate::lsm::selinux_enabled;
10+
use crate::store::Storage;
11+
12+
const SELINUX_CONFIG_PATH: &str = "etc/selinux/config";
13+
const SELINUX_TYPE: &str = "SELINUXTYPE=";
14+
const POLICY_FILE_PREFIX: &str = "policy.";
15+
16+
#[context("Getting SELinux policy for deployment {depl_id}")]
17+
fn get_selinux_policy_for_deployment(
18+
storage: &Storage,
19+
booted_cmdline: &ComposefsCmdline,
20+
depl_id: &str,
21+
) -> Result<Option<String>> {
22+
let sysroot_fd = storage.physical_root.reopen_as_ownedfd()?;
23+
24+
// Booted deployment. We want to get the policy from "/etc" as it might have been modified
25+
let (deployment_root, _mount_guard) = if *booted_cmdline.digest == *depl_id {
26+
(Dir::open_ambient_dir("/", ambient_authority())?, None)
27+
} else {
28+
let composefs_fd = mount_composefs_image(&sysroot_fd, depl_id, false)?;
29+
let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?;
30+
31+
(erofs_tmp_mnt.fd.try_clone()?, Some(erofs_tmp_mnt))
32+
};
33+
34+
if !deployment_root.exists(SELINUX_CONFIG_PATH) {
35+
return Ok(None);
36+
}
37+
38+
let selinux_config = deployment_root
39+
.read_to_string(SELINUX_CONFIG_PATH)
40+
.context("Reading selinux config")?;
41+
42+
let type_ = selinux_config
43+
.lines()
44+
.find(|l| l.starts_with(SELINUX_TYPE))
45+
.ok_or_else(|| anyhow::anyhow!("Falied to find SELINUXTYPE"))?
46+
.split("=")
47+
.nth(1)
48+
.ok_or_else(|| anyhow::anyhow!("Failed to parse SELINUXTYPE"))?
49+
.trim();
50+
51+
let policy_dir_path = format!("etc/selinux/{type_}/policy");
52+
53+
let mut highest_policy_version = -1;
54+
let mut latest_policy_name = None;
55+
56+
let policy_dir = deployment_root
57+
.open_dir(&policy_dir_path)
58+
.context("Opening selinux policy dir")?;
59+
60+
for entry in policy_dir
61+
.entries_utf8()
62+
.context("Getting policy dir entries")?
63+
{
64+
let entry = entry?;
65+
66+
if !entry.file_type()?.is_file() {
67+
// We don't want symlinks, another directory etc
68+
continue;
69+
}
70+
71+
let filename = entry.file_name()?;
72+
73+
match filename.strip_prefix(POLICY_FILE_PREFIX) {
74+
Some(version) => {
75+
let v_int = version
76+
.parse::<i32>()
77+
.with_context(|| anyhow::anyhow!("Parsing {version} as int"))?;
78+
79+
if v_int < highest_policy_version {
80+
continue;
81+
}
82+
83+
highest_policy_version = v_int;
84+
latest_policy_name = Some(filename.to_string());
85+
}
86+
87+
None => continue,
88+
};
89+
}
90+
91+
let policy_name =
92+
latest_policy_name.ok_or_else(|| anyhow::anyhow!("Failed to get latest SELinux policy"))?;
93+
94+
let full_path = format!("{policy_dir_path}/{policy_name}");
95+
96+
let mut file = deployment_root
97+
.open(full_path)
98+
.context("Opening policy file")?;
99+
let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())?;
100+
std::io::copy(&mut file, &mut hasher)?;
101+
102+
let hash = hex::encode(hasher.finish().context("Computing hash")?);
103+
104+
Ok(Some(hash))
105+
}
106+
107+
#[context("Checking SELinux policy compatibility")]
108+
pub(crate) fn are_selinux_policies_compatible(
109+
storage: &Storage,
110+
booted_cmdline: &ComposefsCmdline,
111+
depl_id: &str,
112+
) -> Result<bool> {
113+
if !selinux_enabled()? {
114+
return Ok(true);
115+
}
116+
117+
let booted_policy_hash =
118+
get_selinux_policy_for_deployment(storage, booted_cmdline, &booted_cmdline.digest)?;
119+
120+
let depl_policy_hash = get_selinux_policy_for_deployment(storage, booted_cmdline, depl_id)?;
121+
122+
let sl_policy_match = match (booted_policy_hash, depl_policy_hash) {
123+
// both have policies, compare them
124+
(Some(booted_csum), Some(target_csum)) => booted_csum == target_csum,
125+
// one depl has policy while the other doesn't
126+
(Some(_), None) | (None, Some(_)) => false,
127+
// no policy in either
128+
(None, None) => true,
129+
};
130+
131+
if !sl_policy_match {
132+
tracing::debug!("Soft rebooting not allowed due to differing SELinux policies");
133+
}
134+
135+
Ok(sl_policy_match)
136+
}

crates/lib/src/bootc_composefs/soft_reboot.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ const NEXTROOT: &str = "/run/nextroot";
2323

2424
#[context("Resetting soft reboot state")]
2525
pub(crate) fn reset_soft_reboot() -> Result<()> {
26+
// NOTE: By default bootc runs in an unshared mount namespace;
27+
// this sets up our /runto actually be the same as the host/run
28+
// so the umount (at the end of this function) actually affects the host
29+
//
30+
// Similar operation is performed in `prepare_soft_reboot_composefs`
2631
let run = Utf8Path::new("/run");
2732
bind_mount_from_pidns(PID1, &run, &run, true).context("Bind mounting /run")?;
2833

@@ -33,7 +38,7 @@ pub(crate) fn reset_soft_reboot() -> Result<()> {
3338
.context("Opening nextroot")?;
3439

3540
let Some(nextroot) = nextroot else {
36-
tracing::debug!("Nextroot is not a directory");
41+
tracing::debug!("Nextroot does not exist");
3742
println!("No deployment staged for soft rebooting");
3843
return Ok(());
3944
};
@@ -61,7 +66,7 @@ pub(crate) fn reset_soft_reboot() -> Result<()> {
6166
pub(crate) async fn prepare_soft_reboot_composefs(
6267
storage: &Storage,
6368
booted_cfs: &BootedComposefs,
64-
deployment_id: Option<&String>,
69+
deployment_id: Option<&str>,
6570
soft_reboot_mode: SoftRebootMode,
6671
reboot: bool,
6772
) -> Result<()> {

crates/lib/src/bootc_composefs/status.rs

Lines changed: 80 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::{
1010
bootc_composefs::{
1111
boot::BootType,
1212
repo::get_imgref,
13+
selinux::are_selinux_policies_compatible,
1314
utils::{compute_store_boot_digest_for_uki, get_uki_cmdline},
1415
},
1516
composefs_consts::{
@@ -59,6 +60,13 @@ pub(crate) struct ComposefsCmdline {
5960
pub digest: Box<str>,
6061
}
6162

63+
/// Information about a deployment for soft reboot comparison
64+
struct DeploymentBootInfo<'a> {
65+
boot_digest: &'a str,
66+
full_cmdline: &'a Cmdline<'a>,
67+
verity: &'a str,
68+
}
69+
6270
impl ComposefsCmdline {
6371
pub(crate) fn new(s: &str) -> Self {
6472
let (insecure, digest_str) = s
@@ -79,9 +87,14 @@ impl std::fmt::Display for ComposefsCmdline {
7987
}
8088
}
8189

90+
/// The JSON schema for staged deployment information
91+
/// stored in /run/composefs/staged-deployment
8292
#[derive(Debug, Serialize, Deserialize)]
8393
pub(crate) struct StagedDeployment {
94+
/// The id (verity hash of the EROFS image) of the staged deployment
8495
pub(crate) depl_id: String,
96+
/// Whether to finalize this staged deployment on reboot or not
97+
/// This also maps to `download_only` field in `BootEntry`
8598
pub(crate) finalization_locked: bool,
8699
}
87100

@@ -371,7 +384,7 @@ fn set_soft_reboot_capability(
371384
storage: &Storage,
372385
host: &mut Host,
373386
bls_entries: Option<Vec<BLSConfig>>,
374-
cmdline: &ComposefsCmdline,
387+
booted_cmdline: &ComposefsCmdline,
375388
) -> Result<()> {
376389
let booted = host.require_composefs_booted()?;
377390

@@ -387,10 +400,10 @@ fn set_soft_reboot_capability(
387400
// vector to check for existence of an entry
388401
bls_entries.extend(staged_entries);
389402

390-
set_reboot_capable_type1_deployments(cmdline, host, bls_entries)
403+
set_reboot_capable_type1_deployments(storage, booted_cmdline, host, bls_entries)
391404
}
392405

393-
BootType::Uki => set_reboot_capable_uki_deployments(storage, cmdline, host),
406+
BootType::Uki => set_reboot_capable_uki_deployments(storage, booted_cmdline, host),
394407
}
395408
}
396409

@@ -430,6 +443,7 @@ fn compare_cmdline_skip_cfs(first: &Cmdline<'_>, second: &Cmdline<'_>) -> bool {
430443

431444
#[context("Setting soft reboot capability for Type1 entries")]
432445
fn set_reboot_capable_type1_deployments(
446+
storage: &Storage,
433447
booted_cmdline: &ComposefsCmdline,
434448
host: &mut Host,
435449
bls_entries: Vec<BLSConfig>,
@@ -445,7 +459,13 @@ fn set_reboot_capable_type1_deployments(
445459
let booted_bls_entry = find_bls_entry(&*booted_cmdline.digest, &bls_entries)?
446460
.ok_or_else(|| anyhow::anyhow!("Booted BLS entry not found"))?;
447461

448-
let booted_cmdline = booted_bls_entry.get_cmdline()?;
462+
let booted_full_cmdline = booted_bls_entry.get_cmdline()?;
463+
464+
let booted_info = DeploymentBootInfo {
465+
boot_digest: booted_boot_digest,
466+
full_cmdline: booted_full_cmdline,
467+
verity: &booted_cmdline.digest,
468+
};
449469

450470
for depl in host
451471
.status
@@ -454,46 +474,64 @@ fn set_reboot_capable_type1_deployments(
454474
.chain(host.status.rollback.iter_mut())
455475
.chain(host.status.other_deployments.iter_mut())
456476
{
457-
let entry = find_bls_entry(&depl.require_composefs()?.verity, &bls_entries)?
477+
let depl_verity = &depl.require_composefs()?.verity;
478+
479+
let entry = find_bls_entry(&depl_verity, &bls_entries)?
458480
.ok_or_else(|| anyhow::anyhow!("Entry not found"))?;
459481

460482
let depl_cmdline = entry.get_cmdline()?;
461483

462-
depl.soft_reboot_capable = is_soft_rebootable(
463-
depl.composefs_boot_digest()?,
464-
booted_boot_digest,
465-
depl_cmdline,
466-
booted_cmdline,
467-
);
484+
let target_info = DeploymentBootInfo {
485+
boot_digest: depl.composefs_boot_digest()?,
486+
full_cmdline: depl_cmdline,
487+
verity: &depl_verity,
488+
};
489+
490+
depl.soft_reboot_capable =
491+
is_soft_rebootable(storage, booted_cmdline, &booted_info, &target_info)?;
468492
}
469493

470494
Ok(())
471495
}
472496

497+
/// Determines whether a soft reboot can be performed between the currently booted
498+
/// deployment and a target deployment.
499+
///
500+
/// # Arguments
501+
///
502+
/// * `storage` - The bootc storage backend
503+
/// * `booted_cmdline` - The composefs command line parameters of the currently booted deployment
504+
/// * `booted` - Boot information for the currently booted deployment
505+
/// * `target` - Boot information for the target deployment
473506
fn is_soft_rebootable(
474-
depl_boot_digest: &str,
475-
booted_boot_digest: &str,
476-
depl_cmdline: &Cmdline,
477-
booted_cmdline: &Cmdline,
478-
) -> bool {
479-
if depl_boot_digest != booted_boot_digest {
507+
storage: &Storage,
508+
booted_cmdline: &ComposefsCmdline,
509+
booted: &DeploymentBootInfo,
510+
target: &DeploymentBootInfo,
511+
) -> Result<bool> {
512+
if target.boot_digest != booted.boot_digest {
480513
tracing::debug!("Soft reboot not allowed due to kernel skew");
481-
return false;
514+
return Ok(false);
482515
}
483516

484-
if depl_cmdline.as_bytes().len() != booted_cmdline.as_bytes().len() {
517+
if target.full_cmdline.as_bytes().len() != booted.full_cmdline.as_bytes().len() {
485518
tracing::debug!("Soft reboot not allowed due to differing cmdline");
486-
return false;
519+
return Ok(false);
487520
}
488521

489-
return compare_cmdline_skip_cfs(depl_cmdline, booted_cmdline)
490-
&& compare_cmdline_skip_cfs(booted_cmdline, depl_cmdline);
522+
let cmdline_eq = compare_cmdline_skip_cfs(target.full_cmdline, booted.full_cmdline)
523+
&& compare_cmdline_skip_cfs(booted.full_cmdline, target.full_cmdline);
524+
525+
let selinux_compatible =
526+
are_selinux_policies_compatible(storage, booted_cmdline, target.verity)?;
527+
528+
return Ok(cmdline_eq && selinux_compatible);
491529
}
492530

493531
#[context("Setting soft reboot capability for UKI deployments")]
494532
fn set_reboot_capable_uki_deployments(
495533
storage: &Storage,
496-
cmdline: &ComposefsCmdline,
534+
booted_cmdline: &ComposefsCmdline,
497535
host: &mut Host,
498536
) -> Result<()> {
499537
let booted = host
@@ -505,10 +543,16 @@ fn set_reboot_capable_uki_deployments(
505543
// Since older booted systems won't have the boot digest for UKIs
506544
let booted_boot_digest = match booted.composefs_boot_digest() {
507545
Ok(d) => d,
508-
Err(_) => &compute_store_boot_digest_for_uki(storage, &cmdline.digest)?,
546+
Err(_) => &compute_store_boot_digest_for_uki(storage, &booted_cmdline.digest)?,
509547
};
510548

511-
let booted_cmdline = get_uki_cmdline(storage, &booted.require_composefs()?.verity)?;
549+
let booted_full_cmdline = get_uki_cmdline(storage, &booted_cmdline.digest)?;
550+
551+
let booted_info = DeploymentBootInfo {
552+
boot_digest: booted_boot_digest,
553+
full_cmdline: &booted_full_cmdline,
554+
verity: &booted_cmdline.digest,
555+
};
512556

513557
for deployment in host
514558
.status
@@ -517,23 +561,24 @@ fn set_reboot_capable_uki_deployments(
517561
.chain(host.status.rollback.iter_mut())
518562
.chain(host.status.other_deployments.iter_mut())
519563
{
564+
let depl_verity = &deployment.require_composefs()?.verity;
565+
520566
// Since older booted systems won't have the boot digest for UKIs
521567
let depl_boot_digest = match deployment.composefs_boot_digest() {
522568
Ok(d) => d,
523-
Err(_) => &compute_store_boot_digest_for_uki(
524-
storage,
525-
&deployment.require_composefs()?.verity,
526-
)?,
569+
Err(_) => &compute_store_boot_digest_for_uki(storage, depl_verity)?,
527570
};
528571

529572
let depl_cmdline = get_uki_cmdline(storage, &deployment.require_composefs()?.verity)?;
530573

531-
deployment.soft_reboot_capable = is_soft_rebootable(
532-
depl_boot_digest,
533-
booted_boot_digest,
534-
&depl_cmdline,
535-
&booted_cmdline,
536-
);
574+
let target_info = DeploymentBootInfo {
575+
boot_digest: depl_boot_digest,
576+
full_cmdline: &depl_cmdline,
577+
verity: depl_verity,
578+
};
579+
580+
deployment.soft_reboot_capable =
581+
is_soft_rebootable(storage, booted_cmdline, &booted_info, &target_info)?;
537582
}
538583

539584
Ok(())

0 commit comments

Comments
 (0)