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:
committed by
Colin Walters
parent
d8347297bf
commit
e59e967037
@@ -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;
|
||||
|
||||
136
crates/lib/src/bootc_composefs/selinux.rs
Normal file
136
crates/lib/src/bootc_composefs/selinux.rs
Normal 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)
|
||||
}
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user