1
0
mirror of https://github.com/containers/bootc.git synced 2026-02-05 06:45:13 +01:00

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>
This commit is contained in:
Pragyan Poudyal
2026-01-22 14:15:36 +05:30
committed by Colin Walters
parent d8347297bf
commit e59e967037
7 changed files with 230 additions and 42 deletions

View File

@@ -6,6 +6,7 @@ pub(crate) mod finalize;
pub(crate) mod gc;
pub(crate) mod repo;
pub(crate) mod rollback;
pub(crate) mod selinux;
pub(crate) mod service;
pub(crate) mod soft_reboot;
pub(crate) mod state;

View File

@@ -0,0 +1,136 @@
use anyhow::{Context, Result};
use bootc_initramfs_setup::mount_composefs_image;
use bootc_mount::tempmount::TempMount;
use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
use cap_std_ext::dirext::CapStdExtDirExt;
use fn_error_context::context;
use crate::bootc_composefs::status::ComposefsCmdline;
use crate::lsm::selinux_enabled;
use crate::store::Storage;
const SELINUX_CONFIG_PATH: &str = "etc/selinux/config";
const SELINUX_TYPE: &str = "SELINUXTYPE=";
const POLICY_FILE_PREFIX: &str = "policy.";
#[context("Getting SELinux policy for deployment {depl_id}")]
fn get_selinux_policy_for_deployment(
storage: &Storage,
booted_cmdline: &ComposefsCmdline,
depl_id: &str,
) -> Result<Option<String>> {
let sysroot_fd = storage.physical_root.reopen_as_ownedfd()?;
// Booted deployment. We want to get the policy from "/etc" as it might have been modified
let (deployment_root, _mount_guard) = if *booted_cmdline.digest == *depl_id {
(Dir::open_ambient_dir("/", ambient_authority())?, None)
} else {
let composefs_fd = mount_composefs_image(&sysroot_fd, depl_id, false)?;
let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?;
(erofs_tmp_mnt.fd.try_clone()?, Some(erofs_tmp_mnt))
};
if !deployment_root.exists(SELINUX_CONFIG_PATH) {
return Ok(None);
}
let selinux_config = deployment_root
.read_to_string(SELINUX_CONFIG_PATH)
.context("Reading selinux config")?;
let type_ = selinux_config
.lines()
.find(|l| l.starts_with(SELINUX_TYPE))
.ok_or_else(|| anyhow::anyhow!("Falied to find SELINUXTYPE"))?
.split("=")
.nth(1)
.ok_or_else(|| anyhow::anyhow!("Failed to parse SELINUXTYPE"))?
.trim();
let policy_dir_path = format!("etc/selinux/{type_}/policy");
let mut highest_policy_version = -1;
let mut latest_policy_name = None;
let policy_dir = deployment_root
.open_dir(&policy_dir_path)
.context("Opening selinux policy dir")?;
for entry in policy_dir
.entries_utf8()
.context("Getting policy dir entries")?
{
let entry = entry?;
if !entry.file_type()?.is_file() {
// We don't want symlinks, another directory etc
continue;
}
let filename = entry.file_name()?;
match filename.strip_prefix(POLICY_FILE_PREFIX) {
Some(version) => {
let v_int = version
.parse::<i32>()
.with_context(|| anyhow::anyhow!("Parsing {version} as int"))?;
if v_int < highest_policy_version {
continue;
}
highest_policy_version = v_int;
latest_policy_name = Some(filename.to_string());
}
None => continue,
};
}
let policy_name =
latest_policy_name.ok_or_else(|| anyhow::anyhow!("Failed to get latest SELinux policy"))?;
let full_path = format!("{policy_dir_path}/{policy_name}");
let mut file = deployment_root
.open(full_path)
.context("Opening policy file")?;
let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())?;
std::io::copy(&mut file, &mut hasher)?;
let hash = hex::encode(hasher.finish().context("Computing hash")?);
Ok(Some(hash))
}
#[context("Checking SELinux policy compatibility")]
pub(crate) fn are_selinux_policies_compatible(
storage: &Storage,
booted_cmdline: &ComposefsCmdline,
depl_id: &str,
) -> Result<bool> {
if !selinux_enabled()? {
return Ok(true);
}
let booted_policy_hash =
get_selinux_policy_for_deployment(storage, booted_cmdline, &booted_cmdline.digest)?;
let depl_policy_hash = get_selinux_policy_for_deployment(storage, booted_cmdline, depl_id)?;
let sl_policy_match = match (booted_policy_hash, depl_policy_hash) {
// both have policies, compare them
(Some(booted_csum), Some(target_csum)) => booted_csum == target_csum,
// one depl has policy while the other doesn't
(Some(_), None) | (None, Some(_)) => false,
// no policy in either
(None, None) => true,
};
if !sl_policy_match {
tracing::debug!("Soft rebooting not allowed due to differing SELinux policies");
}
Ok(sl_policy_match)
}

View File

@@ -23,6 +23,11 @@ const NEXTROOT: &str = "/run/nextroot";
#[context("Resetting soft reboot state")]
pub(crate) fn reset_soft_reboot() -> Result<()> {
// NOTE: By default bootc runs in an unshared mount namespace;
// this sets up our /runto actually be the same as the host/run
// so the umount (at the end of this function) actually affects the host
//
// Similar operation is performed in `prepare_soft_reboot_composefs`
let run = Utf8Path::new("/run");
bind_mount_from_pidns(PID1, &run, &run, true).context("Bind mounting /run")?;
@@ -33,7 +38,7 @@ pub(crate) fn reset_soft_reboot() -> Result<()> {
.context("Opening nextroot")?;
let Some(nextroot) = nextroot else {
tracing::debug!("Nextroot is not a directory");
tracing::debug!("Nextroot does not exist");
println!("No deployment staged for soft rebooting");
return Ok(());
};
@@ -61,7 +66,7 @@ pub(crate) fn reset_soft_reboot() -> Result<()> {
pub(crate) async fn prepare_soft_reboot_composefs(
storage: &Storage,
booted_cfs: &BootedComposefs,
deployment_id: Option<&String>,
deployment_id: Option<&str>,
soft_reboot_mode: SoftRebootMode,
reboot: bool,
) -> Result<()> {

View File

@@ -10,6 +10,7 @@ use crate::{
bootc_composefs::{
boot::BootType,
repo::get_imgref,
selinux::are_selinux_policies_compatible,
utils::{compute_store_boot_digest_for_uki, get_uki_cmdline},
},
composefs_consts::{
@@ -59,6 +60,13 @@ pub(crate) struct ComposefsCmdline {
pub digest: Box<str>,
}
/// Information about a deployment for soft reboot comparison
struct DeploymentBootInfo<'a> {
boot_digest: &'a str,
full_cmdline: &'a Cmdline<'a>,
verity: &'a str,
}
impl ComposefsCmdline {
pub(crate) fn new(s: &str) -> Self {
let (insecure, digest_str) = s
@@ -79,9 +87,14 @@ impl std::fmt::Display for ComposefsCmdline {
}
}
/// The JSON schema for staged deployment information
/// stored in /run/composefs/staged-deployment
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct StagedDeployment {
/// The id (verity hash of the EROFS image) of the staged deployment
pub(crate) depl_id: String,
/// Whether to finalize this staged deployment on reboot or not
/// This also maps to `download_only` field in `BootEntry`
pub(crate) finalization_locked: bool,
}
@@ -371,7 +384,7 @@ fn set_soft_reboot_capability(
storage: &Storage,
host: &mut Host,
bls_entries: Option<Vec<BLSConfig>>,
cmdline: &ComposefsCmdline,
booted_cmdline: &ComposefsCmdline,
) -> Result<()> {
let booted = host.require_composefs_booted()?;
@@ -387,10 +400,10 @@ fn set_soft_reboot_capability(
// vector to check for existence of an entry
bls_entries.extend(staged_entries);
set_reboot_capable_type1_deployments(cmdline, host, bls_entries)
set_reboot_capable_type1_deployments(storage, booted_cmdline, host, bls_entries)
}
BootType::Uki => set_reboot_capable_uki_deployments(storage, cmdline, host),
BootType::Uki => set_reboot_capable_uki_deployments(storage, booted_cmdline, host),
}
}
@@ -430,6 +443,7 @@ fn compare_cmdline_skip_cfs(first: &Cmdline<'_>, second: &Cmdline<'_>) -> bool {
#[context("Setting soft reboot capability for Type1 entries")]
fn set_reboot_capable_type1_deployments(
storage: &Storage,
booted_cmdline: &ComposefsCmdline,
host: &mut Host,
bls_entries: Vec<BLSConfig>,
@@ -445,7 +459,13 @@ fn set_reboot_capable_type1_deployments(
let booted_bls_entry = find_bls_entry(&*booted_cmdline.digest, &bls_entries)?
.ok_or_else(|| anyhow::anyhow!("Booted BLS entry not found"))?;
let booted_cmdline = booted_bls_entry.get_cmdline()?;
let booted_full_cmdline = booted_bls_entry.get_cmdline()?;
let booted_info = DeploymentBootInfo {
boot_digest: booted_boot_digest,
full_cmdline: booted_full_cmdline,
verity: &booted_cmdline.digest,
};
for depl in host
.status
@@ -454,46 +474,64 @@ fn set_reboot_capable_type1_deployments(
.chain(host.status.rollback.iter_mut())
.chain(host.status.other_deployments.iter_mut())
{
let entry = find_bls_entry(&depl.require_composefs()?.verity, &bls_entries)?
let depl_verity = &depl.require_composefs()?.verity;
let entry = find_bls_entry(&depl_verity, &bls_entries)?
.ok_or_else(|| anyhow::anyhow!("Entry not found"))?;
let depl_cmdline = entry.get_cmdline()?;
depl.soft_reboot_capable = is_soft_rebootable(
depl.composefs_boot_digest()?,
booted_boot_digest,
depl_cmdline,
booted_cmdline,
);
let target_info = DeploymentBootInfo {
boot_digest: depl.composefs_boot_digest()?,
full_cmdline: depl_cmdline,
verity: &depl_verity,
};
depl.soft_reboot_capable =
is_soft_rebootable(storage, booted_cmdline, &booted_info, &target_info)?;
}
Ok(())
}
/// Determines whether a soft reboot can be performed between the currently booted
/// deployment and a target deployment.
///
/// # Arguments
///
/// * `storage` - The bootc storage backend
/// * `booted_cmdline` - The composefs command line parameters of the currently booted deployment
/// * `booted` - Boot information for the currently booted deployment
/// * `target` - Boot information for the target deployment
fn is_soft_rebootable(
depl_boot_digest: &str,
booted_boot_digest: &str,
depl_cmdline: &Cmdline,
booted_cmdline: &Cmdline,
) -> bool {
if depl_boot_digest != booted_boot_digest {
storage: &Storage,
booted_cmdline: &ComposefsCmdline,
booted: &DeploymentBootInfo,
target: &DeploymentBootInfo,
) -> Result<bool> {
if target.boot_digest != booted.boot_digest {
tracing::debug!("Soft reboot not allowed due to kernel skew");
return false;
return Ok(false);
}
if depl_cmdline.as_bytes().len() != booted_cmdline.as_bytes().len() {
if target.full_cmdline.as_bytes().len() != booted.full_cmdline.as_bytes().len() {
tracing::debug!("Soft reboot not allowed due to differing cmdline");
return false;
return Ok(false);
}
return compare_cmdline_skip_cfs(depl_cmdline, booted_cmdline)
&& compare_cmdline_skip_cfs(booted_cmdline, depl_cmdline);
let cmdline_eq = compare_cmdline_skip_cfs(target.full_cmdline, booted.full_cmdline)
&& compare_cmdline_skip_cfs(booted.full_cmdline, target.full_cmdline);
let selinux_compatible =
are_selinux_policies_compatible(storage, booted_cmdline, target.verity)?;
return Ok(cmdline_eq && selinux_compatible);
}
#[context("Setting soft reboot capability for UKI deployments")]
fn set_reboot_capable_uki_deployments(
storage: &Storage,
cmdline: &ComposefsCmdline,
booted_cmdline: &ComposefsCmdline,
host: &mut Host,
) -> Result<()> {
let booted = host
@@ -505,10 +543,16 @@ fn set_reboot_capable_uki_deployments(
// Since older booted systems won't have the boot digest for UKIs
let booted_boot_digest = match booted.composefs_boot_digest() {
Ok(d) => d,
Err(_) => &compute_store_boot_digest_for_uki(storage, &cmdline.digest)?,
Err(_) => &compute_store_boot_digest_for_uki(storage, &booted_cmdline.digest)?,
};
let booted_cmdline = get_uki_cmdline(storage, &booted.require_composefs()?.verity)?;
let booted_full_cmdline = get_uki_cmdline(storage, &booted_cmdline.digest)?;
let booted_info = DeploymentBootInfo {
boot_digest: booted_boot_digest,
full_cmdline: &booted_full_cmdline,
verity: &booted_cmdline.digest,
};
for deployment in host
.status
@@ -517,23 +561,24 @@ fn set_reboot_capable_uki_deployments(
.chain(host.status.rollback.iter_mut())
.chain(host.status.other_deployments.iter_mut())
{
let depl_verity = &deployment.require_composefs()?.verity;
// Since older booted systems won't have the boot digest for UKIs
let depl_boot_digest = match deployment.composefs_boot_digest() {
Ok(d) => d,
Err(_) => &compute_store_boot_digest_for_uki(
storage,
&deployment.require_composefs()?.verity,
)?,
Err(_) => &compute_store_boot_digest_for_uki(storage, depl_verity)?,
};
let depl_cmdline = get_uki_cmdline(storage, &deployment.require_composefs()?.verity)?;
deployment.soft_reboot_capable = is_soft_rebootable(
depl_boot_digest,
booted_boot_digest,
&depl_cmdline,
&booted_cmdline,
);
let target_info = DeploymentBootInfo {
boot_digest: depl_boot_digest,
full_cmdline: &depl_cmdline,
verity: depl_verity,
};
deployment.soft_reboot_capable =
is_soft_rebootable(storage, booted_cmdline, &booted_info, &target_info)?;
}
Ok(())

View File

@@ -1,8 +1,5 @@
use std::io::Write;
use anyhow::{Context, Result};
use camino::Utf8PathBuf;
use canon_json::CanonJsonSerialize;
use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt};
use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
use composefs_boot::BootOps;
@@ -346,7 +343,7 @@ pub(crate) async fn upgrade_composefs(
.atomic_replace_with(
COMPOSEFS_STAGED_DEPLOYMENT_FNAME,
|f| -> std::io::Result<()> {
f.write_all(new_staged.to_canon_json_string()?.as_bytes())
serde_json::to_writer(f, &new_staged).map_err(std::io::Error::from)
},
)
.context("Writing staged file")?;

View File

@@ -1860,7 +1860,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
prepare_soft_reboot_composefs(
&storage,
&booted_cfs,
deployment.as_ref(),
deployment.as_deref(),
SoftRebootMode::Required,
reboot,
)

View File

@@ -311,6 +311,10 @@ impl BootEntry {
))
}
/// Get the boot digest for this deployment
/// This is the
/// - SHA256SUM of kernel + initrd for Type1 booted deployments
/// - SHA256SUM of UKI for Type2 booted deployments
pub(crate) fn composefs_boot_digest(&self) -> Result<&String> {
self.require_composefs()?
.boot_digest