Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions crates/lib/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use ostree_ext::ostree::Deployment;
use ostree_ext::ostree::{self, Sysroot};
use ostree_ext::sysroot::SysrootLock;
use ostree_ext::tokio_util::spawn_blocking_cancellable_flatten;
use std::os::fd::AsFd;

use crate::progress_jsonl::{Event, ProgressWriter, SubTaskBytes, SubTaskStep};
use crate::spec::ImageReference;
Expand Down Expand Up @@ -361,6 +362,31 @@ pub(crate) async fn prune_container_store(sysroot: &Storage) -> Result<()> {
Ok(())
}

/// Verify there is sufficient disk space to pull an image.
///
/// This checks the available space on the filesystem containing the OSTree repository
/// against the number of bytes that need to be fetched for the image.
pub(crate) fn check_disk_space(
repo_fd: impl AsFd,
image_meta: &PreparedImportMeta,
imgref: &ImageReference,
) -> Result<()> {
let stat = rustix::fs::fstatvfs(repo_fd)?;
let bytes_avail: u64 = stat.f_bsize * stat.f_bavail;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The multiplication stat.f_bsize * stat.f_bavail could overflow on very large filesystems, as both operands are u64. This would cause a panic in debug builds or wrap around in release builds, leading to incorrect disk space checks. Using checked_mul() prevents this by saturating at u64::MAX on overflow, which is a safe upper bound for available space.

Suggested change
let bytes_avail: u64 = stat.f_bsize * stat.f_bavail;
let bytes_avail = stat.f_bsize.checked_mul(stat.f_bavail).unwrap_or(u64::MAX);

tracing::trace!("bytes_avail: {bytes_avail}");

if image_meta.bytes_to_fetch > bytes_avail {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that ostree has a slightly stricter check and has a configurable min-free-space in the repository: the rationale is that we basically never want to get close to filling up a disk with an OS update.

I'm OK with this as is, but how about just a

// TODO consider also syncing with ostree min-free-space

anyhow::bail!(
"Insufficient free space for {image} (available: {bytes_avail} required: {bytes_to_fetch})",
bytes_avail = ostree_ext::glib::format_size(bytes_avail),
bytes_to_fetch = ostree_ext::glib::format_size(image_meta.bytes_to_fetch),
image = imgref.image,
);
}

Ok(())
}

pub(crate) struct PreparedImportMeta {
pub imp: ImageImporter,
pub prep: Box<PreparedImport>,
Expand Down Expand Up @@ -658,6 +684,9 @@ pub(crate) async fn pull(
Ok(existing)
}
PreparedPullResult::Ready(prepared_image_meta) => {
// Check disk space before attempting to pull
check_disk_space(repo.dfd_borrow(), &prepared_image_meta, imgref)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing of course here is this doesn't apply to the composefs backend.

Hmm, I wonder if container-libs has a similar check? If we get to #20 then the check would need to be there.


// Log that we're pulling a new image
const PULLING_NEW_IMAGE_ID: &str = "6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0";
tracing::info!(
Expand Down
31 changes: 6 additions & 25 deletions crates/lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,7 @@ use self::baseline::InstallBlockDeviceOpts;
use crate::bootc_composefs::{boot::setup_composefs_boot, repo::initialize_composefs_repository};
use crate::boundimage::{BoundImage, ResolvedBoundImage};
use crate::containerenv::ContainerExecutionInfo;
use crate::deploy::{
MergeState, PreparedImportMeta, PreparedPullResult, prepare_for_pull, pull_from_prepared,
};
use crate::deploy::{MergeState, PreparedPullResult, prepare_for_pull, pull_from_prepared};
use crate::lsm;
use crate::progress_jsonl::ProgressWriter;
use crate::spec::{Bootloader, ImageReference};
Expand Down Expand Up @@ -1011,27 +1009,6 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result
Ok((storage, has_ostree))
}

fn check_disk_space(
repo_fd: impl AsFd,
image_meta: &PreparedImportMeta,
imgref: &ImageReference,
) -> Result<()> {
let stat = rustix::fs::fstatvfs(repo_fd)?;
let bytes_avail: u64 = stat.f_bsize * stat.f_bavail;
tracing::trace!("bytes_avail: {bytes_avail}");

if image_meta.bytes_to_fetch > bytes_avail {
anyhow::bail!(
"Insufficient free space for {image} (available: {bytes_avail} required: {bytes_to_fetch})",
bytes_avail = ostree_ext::glib::format_size(bytes_avail),
bytes_to_fetch = ostree_ext::glib::format_size(image_meta.bytes_to_fetch),
image = imgref.image,
);
}

Ok(())
}

#[context("Creating ostree deployment")]
async fn install_container(
state: &State,
Expand Down Expand Up @@ -1099,7 +1076,11 @@ async fn install_container(
let pulled_image = match prepared {
PreparedPullResult::AlreadyPresent(existing) => existing,
PreparedPullResult::Ready(image_meta) => {
check_disk_space(root_setup.physical_root.as_fd(), &image_meta, &spec_imgref)?;
crate::deploy::check_disk_space(
root_setup.physical_root.as_fd(),
&image_meta,
&spec_imgref,
)?;
pull_from_prepared(&spec_imgref, false, ProgressWriter::default(), *image_meta).await?
}
};
Expand Down
Loading