diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7dc0a58..cfb3142 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -120,6 +120,7 @@ windows-sys = { version = "0.61", features = [ "Win32_Security", "Win32_System_Diagnostics_ToolHelp", "Win32_System_JobObjects", + "Win32_System_SystemInformation", "Win32_System_Threading", ] } diff --git a/src-tauri/src/managed_runtime.rs b/src-tauri/src/managed_runtime.rs index 86d693c..bbb4ba8 100644 --- a/src-tauri/src/managed_runtime.rs +++ b/src-tauri/src/managed_runtime.rs @@ -121,6 +121,7 @@ pub(crate) struct VerifiedCommand { argv: Vec, environment: BTreeMap, ephemeral_environment: BTreeSet, + host_environment: Vec<(OsString, OsString)>, generation: String, target: String, } @@ -155,7 +156,7 @@ impl VerifiedCommand { command.env(name, value); } let mut seen = BTreeSet::new(); - for (name, value) in ephemeral { + for (name, value) in self.host_environment.into_iter().chain(ephemeral) { let Some(name) = name.to_str() else { return Err("managed runtime environment name is not UTF-8".into()); }; @@ -212,11 +213,13 @@ pub(crate) fn service_root(assets: &Path, service: Service) -> PathBuf { } pub(crate) fn resolve(assets: &Path, service: Service) -> Result { - resolve_at( + let mut verified = resolve_at( &service_root(assets, service), service.wire_name(), &host_target(), - ) + )?; + verified.host_environment = crate::platform_paths::get().managed_child_env()?; + Ok(verified) } fn resolve_at(root: &Path, service: &str, target: &str) -> Result { @@ -261,6 +264,7 @@ fn resolve_at(root: &Path, service: &str, target: &str) -> Result = command + .get_envs() + .filter_map(|(name, value)| { + value.map(|value| { + ( + name.to_string_lossy().into_owned(), + value.to_string_lossy().into_owned(), + ) + }) + }) + .collect(); + assert_eq!(environment.get("SYSTEMROOT"), Some(&r"C:\Windows".into())); + assert_eq!(environment.get("WINDIR"), Some(&r"C:\Windows".into())); + assert_eq!(environment.get("TEMP"), Some(&r"C:\LSDJ\tmp".into())); + assert_eq!(environment.get("TMP"), Some(&r"C:\LSDJ\tmp".into())); + assert_eq!(environment.get("LSDJ_API_CAPABILITY"), Some(&"cap".into())); + for forbidden in [ + "PATH", + "HOME", + "HF_TOKEN", + "HUGGING_FACE_HUB_TOKEN", + "PYTHONPATH", + "LSDJ_GENERATION_CMD", + "LSDJ_SIDECAR_CMD", + "LSDJ_ALLOW_UNVERIFIED_MRT2_CUDA", + "LSDJ_ALLOW_UNVERIFIED_SA3_CUDA", + ] { + assert!(!environment.contains_key(forbidden)); + } + let _ = fs::remove_dir_all(root); + } } diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 8eac191..66396df 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -87,6 +87,10 @@ const BACKEND_SOURCES: &[(&str, &[u8])] = &[ ), ("engine.py", include_bytes!("../../backend/lsdj/engine.py")), ("frozen.py", include_bytes!("../../backend/lsdj/frozen.py")), + ( + "gpu_broker.py", + include_bytes!("../../backend/lsdj/gpu_broker.py"), + ), ("loras.py", include_bytes!("../../backend/lsdj/loras.py")), ("mrt2.py", include_bytes!("../../backend/lsdj/mrt2.py")), ( @@ -98,6 +102,10 @@ const BACKEND_SOURCES: &[(&str, &[u8])] = &[ include_bytes!("../../backend/lsdj/runtime_paths.py"), ), ("sa3.py", include_bytes!("../../backend/lsdj/sa3.py")), + ( + "sa3_cuda.py", + include_bytes!("../../backend/lsdj/sa3_cuda.py"), + ), ( "sa3_audio.py", include_bytes!("../../backend/lsdj/sa3_audio.py"), @@ -113,6 +121,28 @@ const BACKEND_SOURCES: &[(&str, &[u8])] = &[ ("worker.py", include_bytes!("../../backend/lsdj/worker.py")), ]; +const BACKEND_PATH_ENVIRONMENT: &[&str] = &[ + "LSDJ_ASSETS_HOME", + "LSDJ_CACHE_HOME", + "LSDJ_CONFIG_HOME", + "LSDJ_DATA_HOME", + "LSDJ_STAGING_HOME", + "MAGENTA_HOME", + "SA3_HOME", + "SA3_LORAS_HOME", + "SA3_MLX_HOME", +]; + +const WINDOWS_CHILD_ENVIRONMENT: &[&str] = &["SYSTEMROOT", "WINDIR", "TEMP", "TMP"]; + +fn service_ephemeral_environment(secret: &str) -> Vec { + std::iter::once(secret) + .chain(BACKEND_PATH_ENVIRONMENT.iter().copied()) + .chain(WINDOWS_CHILD_ENVIRONMENT.iter().copied()) + .map(str::to_string) + .collect() +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Sa3Backend { Mlx, @@ -1363,6 +1393,12 @@ fn install_mrt2_managed( )?; write_mrt2_identity(&candidate, &pin)?; materialize_contained_file_links(&candidate)?; + smoke_test_materialized_backend( + shared, + &candidate, + &crate::platform_paths::venv_python(&candidate.join("runtime").join(".venv")), + &["lsdj.gpu_broker", "lsdj.mrt2_pytorch"], + )?; seal_mrt2_candidate(&candidate, &pin, python)?; validate_mrt2_candidate(&candidate, &pin, name, &cancelled_now)?; progress("promote", None, None); @@ -1851,6 +1887,12 @@ fn build_sa3_candidate( )?; if backend == Sa3Backend::Tflite { materialize_contained_file_links(candidate)?; + smoke_test_materialized_backend( + shared, + candidate, + &crate::platform_paths::venv_python(&runtime.join(".venv")), + &["lsdj.sa3_cuda", "lsdj.sa3"], + )?; seal_sa3_candidate(candidate, pin, python)?; } validate_sa3_install_cancellable(candidate, pin, backend, &|| { @@ -2434,6 +2476,82 @@ fn install_backend_sources(candidate: &Path) -> Result<(), String> { ) } +fn source_closure_digest(sources: &[(&str, &[u8])]) -> String { + use sha2::{Digest, Sha256}; + + let mut digest = Sha256::new(); + for (name, bytes) in sources { + digest.update((name.len() as u64).to_le_bytes()); + digest.update(name.as_bytes()); + digest.update((bytes.len() as u64).to_le_bytes()); + digest.update(bytes); + } + hex::encode(digest.finalize()) +} + +fn backend_sources_digest() -> String { + source_closure_digest(BACKEND_SOURCES) +} + +const ISOLATED_IMPORT_SCRIPT: &str = r#" +import importlib +import pathlib +import sys + +package_root = pathlib.Path(sys.argv[1]).resolve(strict=True) +sys.path.insert(0, str(package_root)) +for name in sys.argv[2:]: + module = importlib.import_module(name) + pathlib.Path(module.__file__).resolve(strict=True).relative_to(package_root) +"#; + +fn isolated_backend_import_command( + python: &Path, + candidate: &Path, + modules: &[&str], + host_environment: impl IntoIterator, +) -> Result { + if modules.is_empty() + || modules.iter().any(|name| { + name.is_empty() + || !name + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.')) + }) + { + return Err("managed backend import smoke module list is invalid".into()); + } + let package_root = candidate.join("lsdj_backend"); + if !package_root.join("lsdj").is_dir() { + return Err("managed backend source closure is missing".into()); + } + let mut command = Command::new(python); + command + .env_clear() + .env("PYTHONDONTWRITEBYTECODE", "1") + .env("PYTHONNOUSERSITE", "1") + .env("PYTHONUTF8", "1") + .current_dir(candidate) + .args(["-I", "-c", ISOLATED_IMPORT_SCRIPT]) + .arg(package_root) + .args(modules); + for (name, value) in host_environment { + command.env(name, value); + } + Ok(command) +} + +fn smoke_test_materialized_backend( + shared: &InstallShared, + candidate: &Path, + python: &Path, + modules: &[&str], +) -> Result<(), String> { + let host_environment = crate::platform_paths::get().managed_child_env()?; + let command = isolated_backend_import_command(python, candidate, modules, host_environment)?; + stream_child(shared, "managed-backend-import", command, |_| {}) +} + fn relative_wire(root: &Path, path: &Path) -> Result { let relative = path .strip_prefix(root) @@ -2471,6 +2589,7 @@ fn seal_sa3_candidate( provenance.insert("source.repository".into(), pin.repo.clone()); provenance.insert("source.revision".into(), pin.commit.clone()); provenance.insert("source.sha256".into(), pin.source.artifact.sha256.clone()); + provenance.insert("backend.sources.sha256".into(), backend_sources_digest()); provenance.insert("python.version".into(), python_pin.version.clone()); provenance.insert( "python.sha256".into(), @@ -2500,21 +2619,7 @@ fn seal_sa3_candidate( .into_iter() .map(|(key, value)| (key.to_string(), value.to_string())) .collect(); - let ephemeral_environment = [ - "LSDJ_API_CAPABILITY", - "LSDJ_ASSETS_HOME", - "LSDJ_CACHE_HOME", - "LSDJ_CONFIG_HOME", - "LSDJ_DATA_HOME", - "LSDJ_STAGING_HOME", - "MAGENTA_HOME", - "SA3_HOME", - "SA3_LORAS_HOME", - "SA3_MLX_HOME", - ] - .into_iter() - .map(str::to_string) - .collect(); + let ephemeral_environment = service_ephemeral_environment("LSDJ_API_CAPABILITY"); let spec = crate::managed_runtime::CommandSpec { program: relative_wire(candidate, &program)?, argv: vec!["launch.py".into(), "--generation-server".into()], @@ -2589,6 +2694,7 @@ fn seal_mrt2_candidate(candidate: &Path, pin: &Mrt2Pin, python: &PythonPin) -> R "runtime.pin.sha256".into(), content_digest(MRT2_PIN_JSON.as_bytes()), ); + provenance.insert("backend.sources.sha256".into(), backend_sources_digest()); provenance.insert( "runtime.wheels.sha256".into(), content_digest(MRT2_WHEEL_PIN_JSON.as_bytes()), @@ -2622,21 +2728,7 @@ fn seal_mrt2_candidate(candidate: &Path, pin: &Mrt2Pin, python: &PythonPin) -> R .into_iter() .map(|(key, value)| (key.to_string(), value.to_string())) .collect(); - let ephemeral_environment = [ - "LSDJ_WORKER_LAUNCH_TOKEN", - "LSDJ_ASSETS_HOME", - "LSDJ_CACHE_HOME", - "LSDJ_CONFIG_HOME", - "LSDJ_DATA_HOME", - "LSDJ_STAGING_HOME", - "MAGENTA_HOME", - "SA3_HOME", - "SA3_LORAS_HOME", - "SA3_MLX_HOME", - ] - .into_iter() - .map(str::to_string) - .collect(); + let ephemeral_environment = service_ephemeral_environment("LSDJ_WORKER_LAUNCH_TOKEN"); let spec = crate::managed_runtime::CommandSpec { program: relative_wire(candidate, &program)?, argv: vec!["launch.py".into()], @@ -3083,6 +3175,29 @@ pub fn open_model_folder(app: AppHandle, family: Family) -> Result<(), String> { mod tests { use super::*; + fn import_smoke_python() -> PathBuf { + let backend = Path::new(env!("CARGO_MANIFEST_DIR")).join("../backend"); + let managed = crate::platform_paths::venv_python(&backend.join(".venv")); + if managed.is_file() { + return managed; + } + let candidates: &[&str] = if cfg!(target_os = "windows") { + &["python.exe", "python3.exe"] + } else { + &["python3", "python"] + }; + let search = std::env::var_os("PATH").expect("Python is available on CI PATH"); + for directory in std::env::split_paths(&search) { + for name in candidates { + let candidate = directory.join(name); + if candidate.is_file() { + return candidate; + } + } + } + panic!("Python executable is unavailable for managed import smoke test"); + } + fn touch(path: &Path) { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).unwrap(); @@ -3184,6 +3299,114 @@ mod tests { assert_eq!(pin.processor.files.len(), 5); } + #[test] + fn backend_source_closure_carries_gpu_modules_and_service_exclusive_secrets() { + let sources: BTreeMap<_, _> = BACKEND_SOURCES.iter().copied().collect(); + assert_eq!( + sources.get("gpu_broker.py").copied(), + Some(include_bytes!("../../backend/lsdj/gpu_broker.py").as_slice()) + ); + assert_eq!( + sources.get("sa3_cuda.py").copied(), + Some(include_bytes!("../../backend/lsdj/sa3_cuda.py").as_slice()) + ); + assert_eq!(backend_sources_digest().len(), 64); + assert_ne!( + source_closure_digest(&[("gpu_broker.py", b"changed")]), + backend_sources_digest() + ); + + let sa3: BTreeSet<_> = service_ephemeral_environment("LSDJ_API_CAPABILITY") + .into_iter() + .collect(); + let mrt2: BTreeSet<_> = service_ephemeral_environment("LSDJ_WORKER_LAUNCH_TOKEN") + .into_iter() + .collect(); + assert!(sa3.contains("LSDJ_API_CAPABILITY")); + assert!(!sa3.contains("LSDJ_WORKER_LAUNCH_TOKEN")); + assert!(mrt2.contains("LSDJ_WORKER_LAUNCH_TOKEN")); + assert!(!mrt2.contains("LSDJ_API_CAPABILITY")); + for name in BACKEND_PATH_ENVIRONMENT + .iter() + .chain(WINDOWS_CHILD_ENVIRONMENT) + { + assert!(sa3.contains(*name)); + assert!(mrt2.contains(*name)); + } + } + + #[test] + fn materialized_backend_imports_are_isolated_and_missing_modules_fail_closed() { + let root = std::env::temp_dir().join(format!( + "lsdj-backend-import-{}-{:?}", + std::process::id(), + std::thread::current().id() + )); + let _ = std::fs::remove_dir_all(&root); + let candidate = root.join("materialized candidate"); + let unavailable_checkout = root.join("source checkout unavailable"); + std::fs::create_dir_all(&candidate).unwrap(); + std::fs::create_dir_all(&unavailable_checkout).unwrap(); + install_backend_sources(&candidate).unwrap(); + let python = import_smoke_python(); + let host_environment = crate::platform_paths::managed_child_env_for_current_host( + &root.join("safe managed temp"), + ) + .unwrap(); + + let mut command = isolated_backend_import_command( + &python, + &candidate, + &["lsdj.gpu_broker", "lsdj.sa3_cuda"], + host_environment.clone(), + ) + .unwrap(); + command.current_dir(&unavailable_checkout); + let declared: BTreeSet<_> = command + .get_envs() + .filter_map(|(name, value)| value.map(|_| name.to_string_lossy().into_owned())) + .collect(); + for forbidden in [ + "PATH", + "HOME", + "HF_TOKEN", + "HUGGING_FACE_HUB_TOKEN", + "PYTHONPATH", + "LSDJ_GENERATION_CMD", + "LSDJ_SIDECAR_CMD", + "LSDJ_ALLOW_UNVERIFIED_MRT2_CUDA", + "LSDJ_ALLOW_UNVERIFIED_SA3_CUDA", + ] { + assert!(!declared.contains(forbidden)); + } + let output = command.output().unwrap(); + assert!( + output.status.success(), + "isolated imports failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + for module in ["gpu_broker", "sa3_cuda"] { + let path = candidate + .join("lsdj_backend") + .join("lsdj") + .join(format!("{module}.py")); + let bytes = std::fs::read(&path).unwrap(); + std::fs::remove_file(&path).unwrap(); + let mut missing = isolated_backend_import_command( + &python, + &candidate, + &[&format!("lsdj.{module}")], + host_environment.clone(), + ) + .unwrap(); + missing.current_dir(&unavailable_checkout); + assert!(!missing.output().unwrap().status.success()); + std::fs::write(path, bytes).unwrap(); + } + let _ = std::fs::remove_dir_all(root); + } + #[cfg(unix)] #[test] fn managed_runtime_materializes_contained_file_links_and_rejects_escapes() { diff --git a/src-tauri/src/platform_paths.rs b/src-tauri/src/platform_paths.rs index d65ed81..0b8a5a2 100644 --- a/src-tauri/src/platform_paths.rs +++ b/src-tauri/src/platform_paths.rs @@ -97,6 +97,10 @@ impl AppPaths { &self.loras_home } + fn managed_runtime_temp(&self) -> PathBuf { + self.cache.join("managed-runtime-tmp") + } + /// The old macOS Documents brand root, used only to migrate generated /// libraries independently when a destination already contains other data. pub fn legacy_data(&self) -> Option<&Path> { @@ -118,6 +122,118 @@ impl AppPaths { pair("SA3_LORAS_HOME", &self.loras_home), ] } + + /// Environment that an absolute-path managed child still needs after + /// `env_clear`. Unix needs nothing. Windows gets its canonical system root + /// from the host directory API for runtime/DLL discovery and uses a checked, + /// app-owned temp directory; never inherit `PATH`, a profile home, + /// credentials, or developer overrides. + pub(crate) fn managed_child_env(&self) -> Result, String> { + managed_child_env_for_current_host(&self.managed_runtime_temp()) + } +} + +pub(crate) fn managed_child_env_for_current_host( + safe_temp: &Path, +) -> Result, String> { + managed_child_env_for(platform(), safe_temp, windows_system_root) +} + +fn managed_child_env_for( + platform: Platform, + safe_temp: &Path, + system_root: impl FnOnce() -> Result, +) -> Result, String> { + if platform != Platform::Windows { + return Ok(Vec::new()); + } + let system_root = std::fs::canonicalize(PathBuf::from(system_root()?)) + .map_err(|error| format!("cannot resolve Windows system root: {error}"))?; + require_real_directory(&system_root, "Windows system root")?; + let safe_parent = safe_temp + .parent() + .ok_or("managed runtime temp directory has no app-owned parent")?; + std::fs::create_dir_all(safe_parent) + .map_err(|error| format!("cannot create managed runtime temp parent: {error}"))?; + require_real_directory(safe_parent, "managed runtime temp parent")?; + match std::fs::create_dir(safe_temp) { + Ok(()) => {} + Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {} + Err(error) => { + return Err(format!( + "cannot create managed runtime temp directory: {error}" + )) + } + } + require_real_directory(safe_temp, "managed runtime temp directory")?; + let safe_parent = std::fs::canonicalize(safe_parent) + .map_err(|error| format!("cannot resolve managed runtime temp parent: {error}"))?; + let safe_temp = std::fs::canonicalize(safe_temp) + .map_err(|error| format!("cannot resolve managed runtime temp directory: {error}"))?; + if safe_temp.parent() != Some(safe_parent.as_path()) { + return Err("managed runtime temp directory escapes its app-owned parent".into()); + } + Ok(vec![ + pair("SYSTEMROOT", &system_root), + pair("WINDIR", &system_root), + pair("TEMP", &safe_temp), + pair("TMP", &safe_temp), + ]) +} + +fn require_real_directory(path: &Path, label: &str) -> Result<(), String> { + let metadata = std::fs::symlink_metadata(path) + .map_err(|error| format!("cannot inspect {label}: {error}"))?; + if !metadata.is_dir() || metadata.file_type().is_symlink() || is_reparse_point(&metadata) { + return Err(format!("{label} is not a real directory")); + } + Ok(()) +} + +#[cfg(windows)] +fn is_reparse_point(metadata: &std::fs::Metadata) -> bool { + use std::os::windows::fs::MetadataExt; + + const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400; + metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 +} + +#[cfg(not(windows))] +fn is_reparse_point(_: &std::fs::Metadata) -> bool { + false +} + +#[cfg(windows)] +fn windows_system_root() -> Result { + use std::os::windows::ffi::OsStringExt; + use windows_sys::Win32::System::SystemInformation::GetWindowsDirectoryW; + + let mut buffer = vec![0_u16; 260]; + loop { + // SAFETY: `buffer` is writable for the advertised length. The API + // returns either the number of UTF-16 code units written (excluding + // NUL), the required capacity, or zero with a Win32 error. + let length = unsafe { GetWindowsDirectoryW(buffer.as_mut_ptr(), buffer.len() as u32) }; + if length == 0 { + return Err(format!( + "Windows directory API failed: {}", + io::Error::last_os_error() + )); + } + let length = length as usize; + if length < buffer.len() { + return Ok(OsString::from_wide(&buffer[..length])); + } + if length >= 32_768 { + return Err("Windows directory API returned an invalid path length".into()); + } + buffer.resize(length + 1, 0); + } +} + +#[cfg(not(windows))] +fn windows_system_root() -> Result { + Err("Windows directory API is unavailable on this host".into()) } fn pair(name: &str, value: &Path) -> (OsString, OsString) { @@ -484,6 +600,100 @@ mod tests { ); } + #[test] + fn managed_windows_children_receive_only_system_root_and_app_owned_temp() { + let root = std::env::temp_dir().join(format!( + "lsdj-managed-child-env-{}-{:?}", + std::process::id(), + std::thread::current().id() + )); + let _ = std::fs::remove_dir_all(&root); + let system_root = root.join("Windows Root"); + let safe_temp = root.join("cache").join("managed tmp"); + std::fs::create_dir_all(&system_root).unwrap(); + + let environment = managed_child_env_for(Platform::Windows, &safe_temp, || { + Ok(system_root.as_os_str().to_owned()) + }) + .unwrap(); + let values: std::collections::HashMap<_, _> = environment.into_iter().collect(); + let canonical_root = std::fs::canonicalize(&system_root).unwrap(); + let canonical_temp = std::fs::canonicalize(&safe_temp).unwrap(); + assert_eq!(values.len(), 4); + for name in ["SYSTEMROOT", "WINDIR"] { + assert_eq!( + values.get(std::ffi::OsStr::new(name)), + Some(&canonical_root.as_os_str().to_owned()) + ); + } + for name in ["TEMP", "TMP"] { + assert_eq!( + values.get(std::ffi::OsStr::new(name)), + Some(&canonical_temp.as_os_str().to_owned()) + ); + } + for forbidden in [ + "PATH", + "HOME", + "HF_TOKEN", + "HUGGING_FACE_HUB_TOKEN", + "PYTHONPATH", + "LSDJ_GENERATION_CMD", + "LSDJ_SIDECAR_CMD", + ] { + assert!(!values.contains_key(std::ffi::OsStr::new(forbidden))); + } + let _ = std::fs::remove_dir_all(root); + } + + #[test] + fn non_windows_managed_children_inherit_no_host_environment() { + let safe_temp = Path::new("/unused-managed-temp"); + for platform in [Platform::MacOs, Platform::Linux] { + assert!(managed_child_env_for(platform, safe_temp, || { + panic!("non-Windows launch must not query the Windows directory API") + }) + .unwrap() + .is_empty()); + } + } + + #[cfg(unix)] + #[test] + fn managed_windows_temp_rejects_a_link_escape() { + use std::os::unix::fs::symlink; + + let root = std::env::temp_dir().join(format!( + "lsdj-managed-temp-link-{}-{:?}", + std::process::id(), + std::thread::current().id() + )); + let _ = std::fs::remove_dir_all(&root); + let system_root = root.join("Windows Root"); + let safe_parent = root.join("cache"); + let outside = root.join("outside"); + let safe_temp = safe_parent.join("managed tmp"); + std::fs::create_dir_all(&system_root).unwrap(); + std::fs::create_dir_all(&safe_parent).unwrap(); + std::fs::create_dir_all(&outside).unwrap(); + symlink(&outside, &safe_temp).unwrap(); + + let error = managed_child_env_for(Platform::Windows, &safe_temp, || { + Ok(system_root.as_os_str().to_owned()) + }) + .unwrap_err(); + assert!(error.contains("not a real directory")); + let _ = std::fs::remove_dir_all(root); + } + + #[cfg(windows)] + #[test] + fn windows_system_root_comes_from_the_host_directory_api() { + let root = PathBuf::from(windows_system_root().unwrap()); + assert!(root.is_absolute()); + assert!(root.is_dir()); + } + #[test] fn migration_is_atomic_and_restart_safe() { let root = std::env::temp_dir().join(format!(