use std::fs::File;
use std::io;
use std::os::fd::AsRawFd;
use std::os::unix::fs::FileExt;
use std::sync::OnceLock;
use std::time::{Duration, Instant};
use microsandbox_protocol::core::RootDiskState;
const EXT4_IOC_RESIZE_FS: libc::c_ulong = 0x4008_6610;
const BLKGETSIZE64: libc::c_ulong = 0x8008_1272;
static ROOT: OnceLock<io::Result<RootFiles>> = OnceLock::new();
struct RootFiles {
mount: File,
device: File,
}
pub(crate) fn register(mount: &str, device: &str) {
let _ = ROOT.set((|| {
Ok(RootFiles {
mount: File::open(mount)?,
device: File::open(device)?,
})
})());
}
pub(crate) fn resize(target: u64, apply: bool) -> Result<RootDiskState, String> {
let files = ROOT
.get()
.ok_or("root has no pinned block filesystem")?
.as_ref()
.map_err(|e| format!("root disk unavailable: {e}"))?;
let caps = std::fs::read_to_string("/proc/self/status").map_err(|e| e.to_string())?;
let admin = caps
.lines()
.find_map(|line| line.strip_prefix("CapEff:\t"))
.and_then(|value| u64::from_str_radix(value.trim(), 16).ok())
.is_some_and(|value| value & (1 << 24) != 0);
if !admin {
return Err("online ext4 growth requires agent CAP_SYS_RESOURCE".into());
}
let mut sb = [0u8; 1024];
files
.device
.read_exact_at(&mut sb, 1024)
.map_err(|e| e.to_string())?;
let current = validate_superblock(&sb, target)?;
let mut device_bytes = device_size(&files.device)?;
if apply && target > current {
let deadline = Instant::now() + Duration::from_secs(10);
while device_bytes < target {
if Instant::now() >= deadline {
return Err("guest block capacity did not converge".into());
}
std::thread::sleep(Duration::from_millis(2));
device_bytes = device_size(&files.device)?;
}
let blocks = target / 4096;
if unsafe { libc::ioctl(files.mount.as_raw_fd(), EXT4_IOC_RESIZE_FS as _, &blocks) } < 0 {
return Err(format!(
"online ext4 expansion: {}",
io::Error::last_os_error()
));
}
if unsafe { libc::syncfs(files.mount.as_raw_fd()) } < 0 {
return Err(format!("sync grown ext4: {}", io::Error::last_os_error()));
}
files
.device
.read_exact_at(&mut sb, 1024)
.map_err(|e| e.to_string())?;
}
let actual = validate_superblock(&sb, target)?;
if apply && actual != target {
return Err(format!(
"filesystem capacity is {actual}, expected {target}"
));
}
Ok(RootDiskState {
filesystem_bytes: actual,
device_bytes,
})
}
fn device_size(file: &File) -> Result<u64, String> {
let mut size = 0u64;
if unsafe { libc::ioctl(file.as_raw_fd(), BLKGETSIZE64 as _, &mut size) } < 0 {
return Err(format!(
"read guest block capacity: {}",
io::Error::last_os_error()
));
}
Ok(size)
}
fn validate_superblock(sb: &[u8; 1024], target: u64) -> Result<u64, String> {
let u16_at = |i| u16::from_le_bytes([sb[i], sb[i + 1]]);
let u32_at = |i| u32::from_le_bytes(sb[i..i + 4].try_into().unwrap());
if u16_at(56) != 0xef53 || u32_at(24) != 2 || u32_at(32) != 32768 {
return Err("online growth requires a supported 4 KiB ext4 root".into());
}
let current_blocks = u64::from(u32_at(4)) | (u64::from(u32_at(0x150)) << 32);
let current = current_blocks
.checked_mul(4096)
.ok_or("ext4 size overflow")?;
if target < current || !target.is_multiple_of(4096) || target == 0 {
return Err("root growth requires a nondecreasing size aligned to 4 KiB".into());
}
if target == current {
return Ok(current);
}
if u32_at(92) & 0x10 == 0 || u32_at(96) & 0x10 != 0 {
return Err(
"root ext4 lacks supported resize-inode metadata; offline preparation is required"
.into(),
);
}
let descriptor_size = u64::from(u16_at(254)).max(32);
let groups = current_blocks.div_ceil(32768);
let gdt_blocks = (groups * descriptor_size).div_ceil(4096);
let max_groups = (gdt_blocks + u64::from(u16_at(206))) * 4096 / descriptor_size;
if target / 4096 > max_groups * 32768 {
return Err("target exceeds ext4 reserved group-descriptor capacity".into());
}
let target_blocks = target / 4096;
let last_group = (target_blocks - 1) / 32768;
if last_group >= groups && !target_blocks.is_multiple_of(32768) {
let sparse = last_group <= 1
|| [3u64, 5, 7].iter().any(|base| {
let mut group = last_group;
while group > 1 && group.is_multiple_of(*base) {
group /= base;
}
group == 1
});
let inode_blocks = (u64::from(u32_at(40)) * u64::from(u16_at(88))).div_ceil(4096);
let metadata = inode_blocks
+ 2
+ if sparse {
1 + gdt_blocks + u64::from(u16_at(206))
} else {
0
};
if target_blocks % 32768 < metadata {
return Err("target leaves a final block group too small for ext4 metadata".into());
}
}
Ok(current)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_unknown_filesystems_before_mutation() {
assert!(validate_superblock(&[0; 1024], 1024 * 1024 * 1024).is_err());
}
}