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:
parent
1ee481c0b1
commit
836b15a46e
1 changed files with 877 additions and 88 deletions
84
hive-priv/src/salvage_io.rs
Normal file
84
hive-priv/src/salvage_io.rs
Normal 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 })
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue