Watch
0
0
Fork
You've already forked hyperhive
0

harden the ephemeral-root salvage: clone, check space, lock

- clone each file with FICLONE, copy the data only where that fails
- skip the salvage when the copy would leave too little free space
- hold a per-container flock and one copy at a time across containers
- run WriteNspawnFlags on the blocking pool
This commit is contained in:
atlas 2026-10-28 21:37:27 +01:00 • committed by mara
commit 836b15a46e

View file

@ -0,0 +1,84 @@
//! What the root salvage ([`crate::rootfs`]) asks of the host: free space,
//! reflinks, and the locks that keep two salvages apart.
//!
//! The space and reflink calls sit behind [`Backend`] so a test can set the
//! free space and make a clone fail.
use std::fs::{File, OpenOptions};
use std::io;
use std::os::unix::fs::OpenOptionsExt as _;
use std::path::Path;
use std::sync::Mutex;
use anyhow::{Context as _, Result};
/// Where the per-container lock files live. A root-owned runtime directory
/// (`RuntimeDirectory=` of `hive-priv.service`), so no container can write it.
pub(crate) const LOCK_DIR: &str = "/run/hive-priv";
/// Held by the one salvage that is copying data at any moment.
pub(crate) static ONE_AT_A_TIME: Mutex<()> = Mutex::new(());
/// Size and free space of a filesystem, in bytes. `free` is what an
/// unprivileged writer could still use.
#[derive(Debug, Clone, Copy)]
pub(crate) struct FsSpace {
pub(crate) free: u64,
pub(crate) total: u64,
}
/// The host calls the salvage makes.
pub(crate) trait Backend {
/// Space on the filesystem holding `path`.
fn space(&self, path: &Path) -> io::Result<FsSpace>;
/// Make `dst`, an empty file, share `src`'s data blocks. An error leaves
/// `dst` as it was: the filesystem cannot clone, or `src` and `dst` are on
/// different ones.
fn clone_file(&self, src: &File, dst: &File) -> io::Result<()>;
}
/// The real filesystem: `statvfs(2)` and `ioctl(FICLONE)`.
pub(crate) struct Os;
impl Backend for Os {
fn space(&self, path: &Path) -> io::Result<FsSpace> {
let st = rustix::fs::statvfs(path)?;
Ok(FsSpace {
free: st.f_bavail.saturating_mul(st.f_frsize),
total: st.f_blocks.saturating_mul(st.f_frsize),
})
}
fn clone_file(&self, src: &File, dst: &File) -> io::Result<()> {
Ok(rustix::fs::ioctl_ficlone(dst, src)?)
}
}
/// An exclusive `flock(2)` on a container's lock file, released on drop.
pub(crate) struct ContainerLock {
_file: File,
}
impl ContainerLock {
/// Wait for the lock of `name` in `dir`, creating the file if needed.
///
/// # Errors
/// Returns an error when the directory or file cannot be created or the
/// lock call fails.
pub(crate) fn acquire(dir: &Path, name: &str) -> Result<Self> {
std::fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?;
let path = dir.join(format!("salvage-{name}.lock"));
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.mode(0o600)
.custom_flags(libc::O_NOFOLLOW)
.open(&path)
.with_context(|| format!("open {}", path.display()))?;
file.lock()
.with_context(|| format!("lock {}", path.display()))?;
Ok(Self { _file: file })
}
}