#![cfg(target_os = "windows")]
use std::io;
use std::path::{Path, PathBuf};
use crate::windows::unpacker::UvmBootFiles;
const UVMS_SUBDIR: &str = "uvms";
const SANDBOX_VHDX: &str = "sandbox.vhdx";
const DEBUG_SHARE_DIR: &str = "debug";
const CRASH_DIR: &str = "crash";
#[derive(Debug)]
pub struct Uvm {
container_id: String,
scratch_vhdx: PathBuf,
os_files_dir: PathBuf,
debug_dir: PathBuf,
crash_dir: PathBuf,
needs_cleanup: bool,
runtime_id: windows::core::GUID,
}
impl Uvm {
pub fn create(
container_id: &str,
storage_root: &Path,
boot_files: &UvmBootFiles,
) -> io::Result<Self> {
if container_id.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"container_id must not be empty",
));
}
let mut uvm_dir = storage_root.to_path_buf();
uvm_dir.push(UVMS_SUBDIR);
uvm_dir.push(container_id);
std::fs::create_dir_all(&uvm_dir).map_err(|e| {
io::Error::new(
e.kind(),
format!("create UVM dir {}: {e}", uvm_dir.display()),
)
})?;
let scratch_vhdx = uvm_dir.join(SANDBOX_VHDX);
let debug_dir = uvm_dir.join(DEBUG_SHARE_DIR);
let crash_dir = uvm_dir.join(CRASH_DIR);
if scratch_vhdx.exists() {
std::fs::remove_file(&scratch_vhdx).map_err(|e| {
io::Error::new(
e.kind(),
format!("remove stale sandbox VHDX {}: {e}", scratch_vhdx.display()),
)
})?;
}
std::fs::create_dir_all(&debug_dir).map_err(|e| {
io::Error::new(
e.kind(),
format!("create UVM debug dir {}: {e}", debug_dir.display()),
)
})?;
std::fs::create_dir_all(&crash_dir).map_err(|e| {
io::Error::new(
e.kind(),
format!("create UVM crash dir {}: {e}", crash_dir.display()),
)
})?;
let runtime_id =
windows::core::GUID::new().unwrap_or_else(|_| windows::core::GUID::zeroed());
match std::fs::copy(&boot_files.system_template_vhdx, &scratch_vhdx) {
Ok(_) => {
for path in [
scratch_vhdx.as_path(),
boot_files.os_files_dir.as_path(),
boot_files.system_template_vhdx.as_path(),
debug_dir.as_path(),
crash_dir.as_path(),
] {
if let Err(e) = crate::windows::wclayer::grant_vm_access(runtime_id, path) {
let _ = std::fs::remove_file(&scratch_vhdx);
let _ = std::fs::remove_dir_all(&debug_dir);
let _ = std::fs::remove_dir_all(&crash_dir);
let _ = std::fs::remove_dir(&uvm_dir);
return Err(io::Error::new(
e.kind(),
format!("HcsGrantVmAccess({}) failed: {e}", path.display()),
));
}
}
Ok(Self {
container_id: container_id.to_string(),
scratch_vhdx,
os_files_dir: boot_files.os_files_dir.clone(),
debug_dir,
crash_dir,
needs_cleanup: true,
runtime_id,
})
}
Err(e) => {
let _ = std::fs::remove_dir_all(&debug_dir);
let _ = std::fs::remove_dir_all(&crash_dir);
let _ = std::fs::remove_dir(&uvm_dir);
Err(io::Error::new(
e.kind(),
format!(
"copy UVM SystemTemplate {} -> {}: {e}",
boot_files.system_template_vhdx.display(),
scratch_vhdx.display(),
),
))
}
}
}
#[must_use]
pub fn scratch_vhdx(&self) -> &Path {
&self.scratch_vhdx
}
#[must_use]
pub fn os_files_dir(&self) -> &Path {
&self.os_files_dir
}
#[must_use]
pub fn debug_dir(&self) -> &Path {
&self.debug_dir
}
#[must_use]
pub fn crash_dir(&self) -> &Path {
&self.crash_dir
}
#[must_use]
pub fn container_id(&self) -> &str {
&self.container_id
}
#[must_use]
pub fn runtime_id(&self) -> windows::core::GUID {
self.runtime_id
}
#[cfg(test)]
#[must_use]
pub fn for_test(container_id: &str, scratch_vhdx: PathBuf, os_files_dir: PathBuf) -> Self {
let debug_dir = scratch_vhdx
.parent()
.map_or_else(|| scratch_vhdx.with_extension("debug"), |p| p.join("debug"));
let crash_dir = scratch_vhdx
.parent()
.map_or_else(|| scratch_vhdx.with_extension("crash"), |p| p.join("crash"));
Self {
container_id: container_id.to_string(),
scratch_vhdx,
os_files_dir,
debug_dir,
crash_dir,
needs_cleanup: false,
runtime_id: windows::core::GUID::from_u128(0xdead_beef_cafe_f00d_1234_5678_9abc_def0),
}
}
pub fn cleanup(mut self) -> io::Result<()> {
if keep_uvm_on_failure() {
tracing::warn!(
container_id = %self.container_id,
scratch_vhdx = %self.scratch_vhdx.display(),
debug_dir = %self.debug_dir.display(),
"ZLAYER_KEEP_UVM_ON_FAILURE=1 — skipping UVM cleanup (sandbox VHDX and debug dir kept for offline inspection)",
);
self.needs_cleanup = false;
return Ok(());
}
let res = remove_sandbox_vhdx(&self.scratch_vhdx);
let _ = std::fs::remove_dir_all(&self.debug_dir);
let _ = std::fs::remove_dir_all(&self.crash_dir);
self.needs_cleanup = false;
if let Some(parent) = self.scratch_vhdx.parent() {
let _ = std::fs::remove_dir(parent);
}
res
}
}
fn keep_uvm_on_failure() -> bool {
matches!(
std::env::var("ZLAYER_KEEP_UVM_ON_FAILURE").as_deref(),
Ok("1")
)
}
impl Drop for Uvm {
fn drop(&mut self) {
if !self.needs_cleanup {
return;
}
if keep_uvm_on_failure() {
tracing::warn!(
container_id = %self.container_id,
scratch_vhdx = %self.scratch_vhdx.display(),
debug_dir = %self.debug_dir.display(),
"ZLAYER_KEEP_UVM_ON_FAILURE=1 — skipping UVM drop cleanup",
);
return;
}
if let Err(e) = remove_sandbox_vhdx(&self.scratch_vhdx) {
tracing::warn!(
container_id = %self.container_id,
vhdx = %self.scratch_vhdx.display(),
error = %e,
"sandbox VHDX cleanup failed on Uvm drop",
);
}
let _ = std::fs::remove_dir_all(&self.debug_dir);
let _ = std::fs::remove_dir_all(&self.crash_dir);
if let Some(parent) = self.scratch_vhdx.parent() {
let _ = std::fs::remove_dir(parent);
}
}
}
fn remove_sandbox_vhdx(path: &Path) -> io::Result<()> {
match std::fs::remove_file(path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture_boot_files(tmp: &Path, template_bytes: &[u8]) -> UvmBootFiles {
let uvm_dir = tmp.join("layer-uvm");
let files_dir = uvm_dir.join("Files");
let template = uvm_dir.join("SystemTemplate.vhdx");
std::fs::create_dir_all(&files_dir).expect("mkdir files");
std::fs::write(&template, template_bytes).expect("write template");
UvmBootFiles {
uvm_layer_dir: uvm_dir,
os_files_dir: files_dir,
system_template_vhdx: template,
boot_rel_path: r"\EFI\Microsoft\Boot\bootmgfw.efi",
}
}
#[test]
fn remove_sandbox_vhdx_is_idempotent_when_missing() {
let tmp = tempfile::tempdir().expect("tempdir");
let absent = tmp.path().join("never_existed.vhdx");
remove_sandbox_vhdx(&absent).expect("idempotent remove");
}
#[test]
fn create_with_empty_container_id_fails() {
let tmp = tempfile::tempdir().expect("tempdir");
let boot = fixture_boot_files(tmp.path(), b"fake vhdx");
let err = Uvm::create("", tmp.path(), &boot).expect_err("empty container id must fail");
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
#[test]
fn create_with_missing_template_fails() {
let tmp = tempfile::tempdir().expect("tempdir");
let mut boot = fixture_boot_files(tmp.path(), b"fake vhdx");
std::fs::remove_file(&boot.system_template_vhdx).expect("rm template");
boot.system_template_vhdx = tmp.path().join("does-not-exist.vhdx");
let err = Uvm::create("ctr-no-template", tmp.path(), &boot)
.expect_err("missing template must fail");
assert!(
err.kind() == io::ErrorKind::NotFound || err.kind() == io::ErrorKind::PermissionDenied,
"unexpected error kind: {:?}",
err.kind(),
);
}
#[test]
fn create_copies_template_to_sandbox_vhdx() {
let tmp = tempfile::tempdir().expect("tempdir");
let payload = b"PRETEND THIS IS A VHDX HEADER";
let boot = fixture_boot_files(tmp.path(), payload);
let storage_root = tmp.path().join("agent-state");
let sandbox_path = {
let uvm = Uvm::create("ctr-copy", &storage_root, &boot).expect("create UVM");
assert!(uvm.scratch_vhdx().is_file(), "sandbox VHDX should exist");
assert_eq!(
std::fs::read(uvm.scratch_vhdx()).expect("read sandbox"),
payload,
"sandbox VHDX must be a byte-for-byte copy of the template",
);
assert_eq!(uvm.container_id(), "ctr-copy");
assert_eq!(uvm.os_files_dir(), boot.os_files_dir.as_path());
uvm.scratch_vhdx().to_path_buf()
};
assert!(
!sandbox_path.exists(),
"sandbox VHDX must be removed on Uvm drop",
);
}
}