diff --git a/Cargo.lock b/Cargo.lock index 607fc15a..50a2c588 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -386,6 +386,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compression" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a82b366ae14633c67a1cbb4aa3738210a23f77d2868a0fd50faa23a956f9ec4" +dependencies = [ + "cfg-if", + "lazy_static", + "log", + "num-traits", +] + [[package]] name = "const_parse" version = "1.0.0" @@ -656,16 +668,21 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hermit-entry" version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8646a7eb3029cb587f5d1d8742dd4790d4cb128c78817be42b7b375e7c36f366" +source = "git+https://github.com/fogti/hermit-entry?branch=image-reader#b7201091c1ac406c1a3dfcc97d6aeed77a2ae3c9" dependencies = [ "align-address", + "byte-unit", + "compression", "const_parse", "goblin", "log", + "num-traits", "plain", + "serde", "time", + "toml", "uhyve-interface 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "yoke", ] [[package]] @@ -774,6 +791,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.177" @@ -1529,6 +1552,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "sysinfo" version = "0.37.2" @@ -2307,9 +2341,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ "stable_deref_trait", + "yoke-derive", "zerofrom", ] +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.26" @@ -2335,3 +2382,18 @@ name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "synstructure", +] diff --git a/Cargo.toml b/Cargo.toml index 1383be32..c35c1e0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +members = ["uhyve-interface"] exclude = ["tests/test-kernels", "hermit-rs", "hermit-rs/*", "kernel"] [package] @@ -48,7 +49,6 @@ core_affinity = "0.8" env_logger = "0.11" gdbstub = "0.7" gdbstub_arch = "0.3" -hermit-entry = { version = "0.10.5", features = ["loader"] } libc = "0.2" log = "0.4" mac_address = "1.1" @@ -72,6 +72,12 @@ merge = "0.2" yoke = "0.8" nohash = "0.2" +[dependencies.hermit-entry] +version = "0.10.5" +git = "https://github.com/fogti/hermit-entry" +branch = "image-reader" +features = ["loader", "std"] + [target.'cfg(target_os = "linux")'.dependencies] kvm-bindings = "0.14" kvm-ioctls = "0.24" diff --git a/src/bin/uhyve.rs b/src/bin/uhyve.rs index deb942f9..fc2cb906 100644 --- a/src/bin/uhyve.rs +++ b/src/bin/uhyve.rs @@ -5,7 +5,7 @@ use std::{fs, num::ParseIntError, path::PathBuf, process, str::FromStr}; use clap::{Command, CommandFactory, Parser, error::ErrorKind}; use core_affinity::CoreId; use env_logger::Builder; -use log::{LevelFilter, info}; +use log::{LevelFilter, info, warn}; use merge::Merge; use serde::Deserialize; use thiserror::Error; @@ -346,7 +346,7 @@ impl<'de> serde::de::Deserialize<'de> for Affinity { #[derive(Debug, Default, Deserialize, Merge, Parser)] #[cfg_attr(test, derive(PartialEq))] struct GuestArgs { - /// The kernel to execute + /// The kernel or image to execute #[clap(value_parser)] #[serde(skip)] #[merge(skip)] @@ -493,14 +493,14 @@ fn run_uhyve() -> i32 { load_vm_config(&mut args); - let stats = args.uhyve.stats.unwrap_or_default(); let kernel_path = args.guest.kernel.clone(); + let stats = args.uhyve.stats.unwrap_or_default(); let affinity = args.cpu.clone().get_affinity(&mut app); let params = Params::from(args); let vm = UhyveVm::new(kernel_path, params).unwrap_or_else(|e| panic!("Error: {e}")); - let res = vm.run(affinity); + if stats && let Some(stats) = res.stats { println!("Run statistics:"); println!("{stats}"); @@ -514,7 +514,7 @@ fn main() { #[cfg(test)] mod tests { - use std::env; + use std::{env, path::Path}; use tempfile::tempdir; @@ -605,9 +605,9 @@ mod tests { ) .unwrap(); - assert!(&config.uhyve.file_mapping.is_empty()); - assert!(&config.uhyve.config.is_none()); - assert!(&config.guest.kernel.to_str().unwrap().is_empty()) + assert!(config.uhyve.file_mapping.is_empty()); + assert!(config.uhyve.config.is_none()); + assert!(config.guest.kernel == Path::new("")); } /// Tests whether TOML merge works as expected. diff --git a/src/hypercall.rs b/src/hypercall.rs index ec883a4d..357b54b0 100644 --- a/src/hypercall.rs +++ b/src/hypercall.rs @@ -11,6 +11,7 @@ use crate::{ isolation::{ fd::{FdData, GuestFd}, filemap::UhyveFileMap, + image::MappedFile, }, mem::{MemoryError, MmapMemory}, params::EnvVars, @@ -92,8 +93,18 @@ pub fn unlink(mem: &MmapMemory, sysunlink: &mut UnlinkParams, file_map: &mut Uhy sysunlink.ret = if let Some(host_path) = file_map.get_host_path(guest_path) { // We can safely unwrap here, as host_path.as_bytes will never contain internal \0 bytes // As host_path_c_string is a valid CString, this implementation is presumed to be safe. - let host_path_c_string = CString::new(host_path.as_bytes()).unwrap(); - unsafe { libc::unlink(host_path_c_string.as_c_str().as_ptr()) } + match host_path { + MappedFile::OnHost(oh) => { + let host_path_c_string = CString::new(oh.as_os_str().as_bytes()).unwrap(); + unsafe { libc::unlink(host_path_c_string.as_c_str().as_ptr()) } + } + MappedFile::InImage(_) => { + error!( + "The kernel requested to unlink() a ROM path ({guest_path:?}): Rejecting..." + ); + -EROFS + } + } } else { error!("The kernel requested to unlink() an unknown path ({guest_path:?}): Rejecting..."); -ENOENT @@ -113,20 +124,49 @@ pub fn open(mem: &MmapMemory, sysopen: &mut OpenParams, file_map: &mut UhyveFile return; } - sysopen.ret = if let Some(host_path) = file_map.get_host_path(guest_path) { + sysopen.ret = if let Some(guest_entry) = file_map.get_host_path(guest_path) { debug!("{guest_path:#?} found in file map."); - // We can safely unwrap here, as host_path.as_bytes will never contain internal \0 bytes - // As host_path_c_string is a valid CString, this implementation is presumed to be safe. - let host_path_c_string = CString::new(host_path.as_bytes()).unwrap(); + match guest_entry { + MappedFile::OnHost(host_path) => { + // We can safely unwrap here, as host_path.as_bytes will never contain internal \0 bytes + // As host_path_c_string is a valid CString, this implementation is presumed to be safe. + let host_path_c_string = CString::new(host_path.as_os_str().as_bytes()).unwrap(); - let host_fd = - unsafe { libc::open(host_path_c_string.as_c_str().as_ptr(), flags, sysopen.mode) }; - if let Some(guest_fd) = file_map.fdmap.insert(FdData::Raw(host_fd)) { - guest_fd.0 - } else if host_fd < 0 { - host_fd - } else { - -ENOENT + let host_fd = unsafe { + libc::open(host_path_c_string.as_c_str().as_ptr(), flags, sysopen.mode) + }; + if let Some(guest_fd) = file_map.fdmap.insert(FdData::Raw(host_fd)) { + guest_fd.0 + } else if host_fd < 0 { + host_fd + } else { + -ENOENT + } + } + MappedFile::InImage(yk) => { + match yk.try_map_project(move |data, _| { + if let hermit_entry::ThinTree::File { content, .. } = data { + Ok(content) + } else { + Err(()) + } + }) { + Ok(data) => { + file_map + .fdmap + .insert(FdData::Virtual { + data: data.erase_arc_cart(), + offset: 0, + }) + .expect("virtual file fdmapping should never fail") + .0 + } + Err(()) => { + debug!("Returning -EISDIR for {guest_path:#?}"); + -EISDIR + } + } + } } } else { debug!("{guest_path:#?} not found in file map."); @@ -206,6 +246,7 @@ pub fn read( amt, ) }; + *offset += u64::try_from(amt).unwrap(); amt as isize } } diff --git a/src/isolation/filemap.rs b/src/isolation/filemap.rs index 5de3e9be..992c0c30 100644 --- a/src/isolation/filemap.rs +++ b/src/isolation/filemap.rs @@ -1,7 +1,7 @@ use std::{ collections::HashMap, - ffi::{CStr, CString, OsStr, OsString}, - fs::{canonicalize, metadata}, + ffi::{CStr, CString, OsStr}, + fs::metadata, os::unix::ffi::OsStrExt, path::{Path, PathBuf}, }; @@ -11,13 +11,16 @@ use tempfile::TempDir; use uuid::Uuid; use crate::isolation::{ - fd::UhyveFileDescriptorLayer, split_guest_and_host_path, tempdir::create_temp_dir, + fd::UhyveFileDescriptorLayer, + image::{Cache, MappedFile}, + split_guest_and_host_path, + tempdir::create_temp_dir, }; /// Wrapper around a `HashMap` to map guest paths to arbitrary host paths and track file descriptors. #[derive(Debug)] pub struct UhyveFileMap { - files: HashMap, + files: HashMap, tempdir: TempDir, pub fdmap: UhyveFileDescriptorLayer, } @@ -25,103 +28,130 @@ pub struct UhyveFileMap { impl UhyveFileMap { /// Creates a UhyveFileMap. /// - /// * `mappings` - A list of host->guest path mappings with the format "./host_path.txt:guest.txt" + /// * `mappings` - A list of host->guest path mappings with the format + /// "./host_path.txt:guest.txt" or "./hermit_image.hermit:contained.txt:guest.txt" /// * `tempdir` - Path to create temporary directory on - pub fn new(mappings: &[String], tempdir: Option) -> UhyveFileMap { - let fm = UhyveFileMap { - files: mappings - .iter() - .map(String::as_str) - .map(split_guest_and_host_path) - .map(Result::unwrap) - .collect(), - tempdir: create_temp_dir(tempdir), - fdmap: UhyveFileDescriptorLayer::default(), - }; + pub fn new( + mappings: &[String], + tempdir: Option, + hermit_image_cache: &mut Cache, + ) -> Self { + let tempdir = create_temp_dir(tempdir); + let mut files = HashMap::new(); + + for i in mappings { + let (guest_path, maybe_in_image_str, host_path) = split_guest_and_host_path(i).unwrap(); + if let Some(mut x) = maybe_in_image_str { + let image = hermit_image_cache.register(&host_path); + if x == "." || x == "/" { + x = "".to_string(); + } + + // resolve file + if let Ok(resolved) = image.try_map_project_cloned(|yoked, _| { + let ret: Result, ()> = + yoked.resolve((&*x).into()).ok_or(()).cloned(); + ret + }) { + files.insert(guest_path, MappedFile::InImage(resolved)); + } else { + warn!( + "In hermit image {}: unable to find file {:?} -> {}", + host_path.display(), + x, + guest_path.display() + ); + } + } else { + files.insert(guest_path, MappedFile::OnHost(host_path)); + } + } + assert_eq!( - fm.files.len(), + files.len(), mappings.len(), "Error when creating filemap. Are duplicate paths present?" ); - fm + + Self { + files, + tempdir, + fdmap: UhyveFileDescriptorLayer::default(), + } } /// Returns the host_path on the host filesystem given a requested guest_path, if it exists. /// /// * `guest_path` - The guest path that is to be looked up in the map. - pub fn get_host_path(&self, guest_path: &CStr) -> Option { + pub fn get_host_path(&self, guest_path: &CStr) -> Option { + if self.files.is_empty() { + debug!("UhyveFileMap is empty, returning None..."); + return None; + } + // TODO: Replace clean-path in favor of Path::normalize_lexically, which has not // been implemented yet. See: https://github.com/rust-lang/libs-team/issues/396 let guest_pathbuf = clean(OsStr::from_bytes(guest_path.to_bytes())); - if let Some(host_path) = self.files.get(&guest_pathbuf) { - let host_path = OsString::from(host_path); - trace!("get_host_path (host_path): {host_path:#?}"); - Some(host_path) - } else { - debug!("Guest requested to open a path that was not mapped."); - if self.files.is_empty() { - debug!("UhyveFileMap is empty, returning None..."); - return None; - } - if let Some(parent_of_guest_path) = guest_pathbuf.parent() { - debug!("The file is in a child directory, searching for a parent directory..."); - for searched_parent_guest in parent_of_guest_path.ancestors() { - // If one of the guest paths' parent directories (parent_host) is mapped, - // use the mapped host path and push the "remainder" (the path's components - // that come after the mapped guest path) onto the host path. - if let Some(parent_host) = self.files.get(searched_parent_guest) { - let mut host_path = PathBuf::from(parent_host); - let guest_path_remainder = - guest_pathbuf.strip_prefix(searched_parent_guest).unwrap(); - host_path.push(guest_path_remainder); - - // Handles symbolic links. - return canonicalize(&host_path) - .map_or(host_path.into_os_string(), PathBuf::into_os_string) - .into(); - } - } + for searched_parent_guest in guest_pathbuf.ancestors() { + // If one of the guest paths' parent directories (parent_host) is mapped, + // use the mapped host path and push the "remainder" (the path's components + // that come after the mapped guest path) onto the host path. + if let Some(parent_host) = self.files.get(searched_parent_guest) { + let guest_path_remainder = + guest_pathbuf.strip_prefix(searched_parent_guest).unwrap(); + return parent_host.resolve(guest_path_remainder); } - debug!("The file is not in a child directory, returning None..."); - None } + debug!("The file is not in a child directory, returning None..."); + None } /// Returns an array of all host paths (for Landlock). #[cfg(target_os = "linux")] pub(crate) fn get_all_host_paths(&self) -> impl Iterator { - self.files.values().map(|i| i.as_os_str()) + self.files.values().filter_map(|i| match i { + MappedFile::OnHost(f) => Some(f.as_os_str()), + _ => None, + }) } /// Returns an iterator (non-unique) over all mountable guest directories. pub(crate) fn get_all_guest_dirs(&self) -> impl Iterator { self.files.iter().filter_map(|(gp, hp)| { - // We check the host_path filetype, and return the parent directory for everything non-file. - if let Ok(hp_metadata) = metadata(hp) { - if hp_metadata.is_dir() { - Some(gp.as_path()) - } else if hp_metadata.is_file() { - Some(gp.as_path().parent().unwrap()) - } else if hp_metadata.is_symlink() { - error!( - "{} is a symlink. This is not supported (yet?)", - hp.display() - ); - None - } else { - Some(gp.as_path().parent().unwrap()) + match hp { + MappedFile::OnHost(hp) => { + // We check the host_path filetype, and return the parent directory for everything non-file. + if let Ok(hp_metadata) = metadata(hp) { + if hp_metadata.is_dir() { + Some(gp.as_path()) + } else if hp_metadata.is_file() { + Some(gp.as_path().parent().unwrap()) + } else if hp_metadata.is_symlink() { + error!( + "{} is a symlink. This is not supported (yet?)", + hp.display() + ); + None + } else { + Some(gp.as_path().parent().unwrap()) + } + } else if let Some(parent_path) = hp.parent() + && let Ok(parent_metadata) = metadata(parent_path) + && parent_metadata.is_dir() + { + // Parent directory exists, so this is a mounted file + Some(gp.as_path().parent().unwrap()) + } else { + error!("{} isn't a valid host path", hp.display()); + // return Err(ErrorKind::InvalidFilename); + None + } } - } else if let Some(parent_path) = hp.parent() - && let Ok(parent_metadata) = metadata(parent_path) - && parent_metadata.is_dir() - { - // Parent directory exists, so this is a mounted file - Some(gp.as_path().parent().unwrap()) - } else { - error!("{} isn't a valid host path", hp.display()); - // return Err(ErrorKind::InvalidFilename); - None + MappedFile::InImage(ii) => Some(match ii.get() { + hermit_entry::ThinTree::Directory(_) => gp.as_path(), + _ => gp.as_path().parent().unwrap(), + }), } }) } @@ -142,7 +172,7 @@ impl UhyveFileMap { let ret = CString::new(host_path.as_os_str().as_bytes()).unwrap(); self.files.insert( PathBuf::from(OsStr::from_bytes(guest_path.to_bytes())), - host_path, + MappedFile::OnHost(host_path), ); ret } @@ -180,31 +210,39 @@ mod tests { path_prefix.clone() + "/this_symlink_leads_to_a_file" + ":guest_file_symlink", ]; - let map = UhyveFileMap::new(&map_parameters, None); + let map = UhyveFileMap::new(&map_parameters, None, &mut Cache::default()); assert_eq!( - map.get_host_path(c"readme_file.md").unwrap(), - OsString::from(&map_results[0]) + map.get_host_path(c"readme_file.md") + .unwrap() + .unwrap_on_host(), + PathBuf::from(&map_results[0]) ); assert_eq!( - map.get_host_path(c"guest_folder").unwrap(), - OsString::from(&map_results[1]) + map.get_host_path(c"guest_folder").unwrap().unwrap_on_host(), + PathBuf::from(&map_results[1]) ); assert_eq!( - map.get_host_path(c"guest_symlink").unwrap(), - OsString::from(&map_results[2]) + map.get_host_path(c"guest_symlink") + .unwrap() + .unwrap_on_host(), + PathBuf::from(&map_results[2]) ); assert_eq!( - map.get_host_path(c"guest_dangling_symlink").unwrap(), - OsString::from(&map_results[3]) + map.get_host_path(c"guest_dangling_symlink") + .unwrap() + .unwrap_on_host(), + PathBuf::from(&map_results[3]) ); assert_eq!( - map.get_host_path(c"guest_file").unwrap(), - OsString::from(&map_results[4]) + map.get_host_path(c"guest_file").unwrap().unwrap_on_host(), + PathBuf::from(&map_results[4]) ); assert_eq!( - map.get_host_path(c"guest_file_symlink").unwrap(), - OsString::from(&map_results[5]) + map.get_host_path(c"guest_file_symlink") + .unwrap() + .unwrap_on_host(), + PathBuf::from(&map_results[5]) ); assert!(map.get_host_path(c"this_file_is_not_mapped").is_none()); @@ -232,7 +270,7 @@ mod tests { host_path_map.to_str().unwrap(), guest_path_map.to_str().unwrap() )]; - let mut map = UhyveFileMap::new(&uhyvefilemap_params, None); + let mut map = UhyveFileMap::new(&uhyvefilemap_params, None, &mut Cache::default()); let mut found_host_path = map.get_host_path( CString::new(target_guest_path.as_os_str().as_bytes()) @@ -241,8 +279,8 @@ mod tests { ); assert_eq!( - found_host_path.unwrap(), - target_host_path.as_os_str().to_str().unwrap() + found_host_path.unwrap().unwrap_on_host(), + target_host_path.as_os_str() ); // Tests successful directory traversal of the child directory. @@ -257,8 +295,8 @@ mod tests { .as_c_str(), ); assert_eq!( - found_host_path.unwrap(), - target_host_path.as_os_str().to_str().unwrap() + found_host_path.unwrap().unwrap_on_host(), + target_host_path.as_os_str() ); // Tests directory traversal leading to valid symbolic link with an @@ -271,7 +309,7 @@ mod tests { guest_path_map.to_str().unwrap() )]; - map = UhyveFileMap::new(&uhyvefilemap_params, None); + map = UhyveFileMap::new(&uhyvefilemap_params, None, &mut Cache::default()); target_guest_path = PathBuf::from("/root/this_symlink_leads_to_a_file"); target_host_path = fixture_path.clone(); @@ -282,13 +320,12 @@ mod tests { .as_c_str(), ); assert_eq!( - found_host_path.unwrap(), - target_host_path.as_os_str().to_str().unwrap() + found_host_path.unwrap().unwrap_on_host(), + target_host_path.as_os_str() ); // Tests directory traversal with no maps - let empty_array: [String; 0] = []; - map = UhyveFileMap::new(&empty_array, None); + map = UhyveFileMap::new(&[], None, &mut Cache::default()); found_host_path = map.get_host_path( CString::new(target_guest_path.as_os_str().as_bytes()) .unwrap() diff --git a/src/isolation/image.rs b/src/isolation/image.rs new file mode 100644 index 00000000..11f47cb9 --- /dev/null +++ b/src/isolation/image.rs @@ -0,0 +1,120 @@ +use std::{ + collections::HashMap, + fmt, + fs::canonicalize, + path::{Path, PathBuf}, + sync::Arc, +}; + +use yoke::Yoke; + +/// A "mounted" hermit image, the decompressed contents of it are mmap'ed into this process +pub type HermitImage = Box; + +#[derive(Clone)] +pub enum MappedFile { + OnHost(PathBuf), + InImage(Yoke, Arc>), +} + +impl fmt::Debug for MappedFile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MappedFile::OnHost(oh) => write!(f, "OnHost({:?})", oh), + MappedFile::InImage(_) => write!(f, "InImage(..)"), + } + } +} + +impl MappedFile { + #[cfg(test)] + pub(super) fn unwrap_on_host(self) -> PathBuf { + if let Self::OnHost(p) = self { + p + } else { + panic!("unexpected mapped file: {:?}", self) + } + } + + pub fn resolve(&self, entry: &Path) -> Option { + match self { + MappedFile::OnHost(host_path) => { + let host_path = if Path::new("") == entry { + host_path.clone() + } else { + host_path.join(entry) + }; + // Handles symbolic links. + Some(MappedFile::OnHost( + canonicalize(&host_path) + .map_or(host_path.into_os_string(), PathBuf::into_os_string) + .into(), + )) + } + MappedFile::InImage(yoked) => Some(MappedFile::InImage({ + let entry = entry.to_str()?; + yoked + .try_map_project_cloned(move |yk: &hermit_entry::ThinTree<'_>, _| { + let ret: Result, ()> = + yk.resolve(entry.into()).ok_or(()).cloned(); + ret + }) + .ok()? + })), + } + } +} + +/// A cache for decompressed hermit images +#[derive(Default)] +pub struct Cache { + images: HashMap, Arc>>, +} + +impl Cache { + pub fn register_with_data( + &mut self, + host_path: &Path, + data: impl FnOnce(&Path) -> Vec, + ) -> &Yoke, Arc> { + self.images + .entry(host_path.to_path_buf()) + .or_insert_with(move || { + let data = data(host_path); + let decompressed = hermit_entry::decompress_image(&data[..]).unwrap_or_else(|e| { + panic!( + "{}: unable to decompress hermit image file: {}", + host_path.display(), + e, + ) + }); + + let image: Arc = Arc::new(decompressed); + + Yoke::attach_to_cart(image, |image| { + hermit_entry::ThinTree::try_from_image(image).unwrap_or_else(|e| { + panic!( + "{}: unable to parse hermit image file entry: {:?}", + host_path.display(), + e, + ) + }) + }) + }) + } + + pub fn register( + &mut self, + host_path: &Path, + ) -> &Yoke, Arc> { + self.register_with_data(host_path, |host_path| { + std::fs::read(host_path).unwrap_or_else(|e| { + panic!( + "{}: unable to read hermit image: {}", + host_path.display(), + e + ) + }) + }) + } +} diff --git a/src/isolation/mod.rs b/src/isolation/mod.rs index 98bc9f85..f65dbea0 100644 --- a/src/isolation/mod.rs +++ b/src/isolation/mod.rs @@ -1,7 +1,10 @@ pub mod fd; pub mod filemap; +pub mod image; + #[cfg(target_os = "linux")] pub mod landlock; + pub mod tempdir; use std::{ @@ -15,11 +18,22 @@ use clean_path::clean; /// Separates a string of the format "./host_dir/host_path.txt:guest_path.txt" /// into a guest_path (String) and host_path (OsString) respectively. /// -/// * `mapping` - A mapping of the format `./host_path.txt:guest.txt`. -fn split_guest_and_host_path(mapping: &str) -> Result<(PathBuf, PathBuf), ErrorKind> { +/// * `mapping` - A mapping of the format `./host_path.txt:guest.txt` or `./hermit_image.hermit:contained.txt:guest.txt. +fn split_guest_and_host_path( + mapping: &str, +) -> Result<(PathBuf, Option, PathBuf), ErrorKind> { let mut mappingiter = mapping.split(':'); + let host_str = mappingiter.next().ok_or(ErrorKind::InvalidInput)?; - let guest_str = mappingiter.next().ok_or(ErrorKind::InvalidInput)?; + + let (inside_archive_str, guest_str) = { + let tmp2 = mappingiter.next().ok_or(ErrorKind::InvalidInput)?; + if let Some(tmp3) = mappingiter.next() { + (Some(clean(tmp2).to_str().unwrap().to_string()), tmp3) + } else { + (None, tmp2) + } + }; // TODO: Replace clean-path in favor of Path::normalize_lexically, which has not // been implemented yet. See: https://github.com/rust-lang/libs-team/issues/396 @@ -27,17 +41,16 @@ fn split_guest_and_host_path(mapping: &str) -> Result<(PathBuf, PathBuf), ErrorK let guest_path = clean(guest_str); - Ok((guest_path, host_path)) + Ok((guest_path, inside_archive_str, host_path)) } #[test] fn test_split_guest_and_host_path() { - use std::path::PathBuf; - let host_guest_strings = [ "./host_string.txt:guest_string.txt", "/home/user/host_string.txt:guest_string.md.txt", - "host_string.txt:this_does_exist.txt:should_not_exist.txt", + "host_string.txt:this_does_exist_in_archive.txt:this_does_exist.txt", + "host_string.txt:this_does_exist_in_archive.txt:this_does_exist.txt:this_is_ignored.txt", "host_string.txt:test/..//guest_string.txt", ]; @@ -47,13 +60,27 @@ fn test_split_guest_and_host_path() { // Mind the inverted order. let results = [ - (PathBuf::from("guest_string.txt"), fixture_path.clone()), + ( + PathBuf::from("guest_string.txt"), + None, + fixture_path.clone(), + ), ( PathBuf::from("guest_string.md.txt"), + None, PathBuf::from("/home/user/host_string.txt"), ), - (PathBuf::from("this_does_exist.txt"), fixture_path.clone()), - (PathBuf::from("guest_string.txt"), fixture_path), + ( + PathBuf::from("this_does_exist.txt"), + Some("this_does_exist_in_archive.txt".to_string()), + fixture_path.clone(), + ), + ( + PathBuf::from("this_does_exist.txt"), + Some("this_does_exist_in_archive.txt".to_string()), + fixture_path.clone(), + ), + (PathBuf::from("guest_string.txt"), None, fixture_path), ]; for (i, host_and_guest_string) in host_guest_strings diff --git a/src/lib.rs b/src/lib.rs index 87ec25e0..6daaf471 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,9 @@ pub enum HypervisorError { #[error("Invalid kernel path ({0})")] InvalidKernelPath(PathBuf), + #[error("Image configuration error: {0}")] + ImageConfig(#[from] hermit_entry::config::HandleConfigError), + #[error("Kernel Loading Error: {0}")] LoadedKernelError(#[from] vm::LoadKernelError), } diff --git a/src/params.rs b/src/params.rs index a9f88c84..8a3ed418 100644 --- a/src/params.rs +++ b/src/params.rs @@ -38,6 +38,7 @@ pub struct Params { pub kernel_args: Vec, /// Mapped paths between the guest and host OS + // TODO: use more fine-grained format (see also `EnvVars`) pub file_mapping: Vec, /// Path to create temporary directory on diff --git a/src/vm.rs b/src/vm.rs index a050aaef..9895cc6b 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -10,8 +10,9 @@ use std::{ use core_affinity::CoreId; use hermit_entry::{ - HermitVersion, + Format, HermitVersion, boot_info::{BootInfo, HardwareInfo, LoadInfo, PlatformInfo, RawBootInfo, SerialPortBase}, + config, detect_format, elf::{KernelObject, LoadedKernel, ParseKernelError}, }; use internal::VirtualizationBackendInternal; @@ -24,7 +25,7 @@ use crate::{ consts::*, fdt::Fdt, generate_address, - isolation::filemap::UhyveFileMap, + isolation::{filemap::UhyveFileMap, image::Cache as HermitImageCache}, mem::MmapMemory, os::KickSignal, params::{EnvVars, Params}, @@ -137,11 +138,99 @@ pub struct UhyveVm { pub(crate) kernel_info: Arc, } impl UhyveVm { - pub fn new(kernel_path: PathBuf, params: Params) -> HypervisorResult> { - let memory_size = params.memory_size.get(); + pub fn new(kernel_path: PathBuf, mut params: Params) -> HypervisorResult> { + let mut hermit_image_cache = HermitImageCache::default(); - let elf = fs::read(&kernel_path) + let mut kernel_data = fs::read(&kernel_path) .map_err(|_e| HypervisorError::InvalidKernelPath(kernel_path.clone()))?; + + // `kernel_data` might be an Hermit image + let elf = match detect_format(&kernel_data[..]) { + None => return Err(HypervisorError::InvalidKernelPath(kernel_path.clone())), + Some(Format::Elf) => kernel_data, + Some(Format::Gzip) => { + let image_tree = hermit_image_cache + .register_with_data(&kernel_path, |_| core::mem::take(&mut kernel_data)); + + // find + parse image configuration + let (config, kernel_slice) = match image_tree.get().handle_config() { + Ok((config, kernel_slice)) => (config, kernel_slice), + Err(e) => { + error!("Hermit image configuration error: {e}"); + return Err(HypervisorError::ImageConfig(e)); + } + }; + + match config { + config::Config::V1 { + mut input, + requirements: _, + kernel, + } => { + // handle image configuration + + // .input + if params.kernel_args.is_empty() { + params.kernel_args.append(&mut input.kernel_args); + if !input.app_args.is_empty() { + params.kernel_args.push("--".to_string()); + params.kernel_args.append(&mut input.app_args); + } + } + + // don't pass privileged env-var commands through + input.env_vars.retain(|i| i.contains('=')); + + if let EnvVars::Set(env) = &mut params.env { + if let Ok(EnvVars::Set(prev_env_vars)) = + EnvVars::try_from(&input.env_vars[..]) + { + // env vars from params take precedence + let new_env_vars = core::mem::take(env); + *env = prev_env_vars.into_iter().chain(new_env_vars).collect(); + } else { + warn!("Unable to parse env vars from Hermit image configuration"); + } + } else if !input.env_vars.is_empty() { + warn!("Ignoring Hermit image env vars due to `-e host`"); + } + + // .requirements + // TODO: implement this part + + // Limitation of current implementation: + // because we need to put the image path into the file map, + // it needs to be valid UTF-8 + let image_path_str = kernel_path + .to_str() + .expect("path to image must be valid UTF-8"); + + if let hermit_entry::ThinTree::Directory(iroot) = image_tree.get() { + for (k, v) in iroot { + if let hermit_entry::ThinTree::Directory(_) = v { + let k = core::str::from_utf8(k) + .expect("subdir path has to be valid UTF-8"); + params + .file_mapping + .insert(0, format!("{}:{}:/{}", image_path_str, k, k)); + } + } + } else { + return Err(HypervisorError::InvalidKernelPath(PathBuf::from( + format!("{}:{}", image_path_str, &kernel), + ))); + } + } + } + + // This copy is necessary to avoid a borrowing conflict + // with `hermit_image_cache`. + kernel_slice.to_vec() + } + }; + + let memory_size = params.memory_size.get(); + let object: KernelObject<'_> = KernelObject::parse(&elf).map_err(LoadKernelError::ParseKernelError)?; @@ -207,6 +296,7 @@ impl UhyveVm { let file_mapping = Mutex::new(UhyveFileMap::new( ¶ms.file_mapping, params.tempdir.clone(), + &mut hermit_image_cache, )); let mut mounts: Vec<_> = file_mapping .lock() diff --git a/tests/data/fixtures/fs/hello_world.hermit b/tests/data/fixtures/fs/hello_world.hermit new file mode 100644 index 00000000..429f4beb Binary files /dev/null and b/tests/data/fixtures/fs/hello_world.hermit differ diff --git a/tests/fs-test.rs b/tests/fs-test.rs index 097d9e96..a3bf4598 100644 --- a/tests/fs-test.rs +++ b/tests/fs-test.rs @@ -353,6 +353,28 @@ fn open_read_only_write() { .expect_err("Uhyve should've crashed on write"); } +/// Hermit Image test: Opens+reads a file inside of a hermit image +#[test] +fn image_open_read() { + env_logger::try_init().ok(); + + let guest_file_path = get_testname_derived_guest_path("image_open_read"); + let params = generate_params( + Some(vec![format!( + "{}/hello_world.hermit:hello_world.txt:{}", + get_fs_fixture_path().to_str().unwrap(), + guest_file_path.to_str().unwrap(), + )]), + "open_read", + &guest_file_path, + ); + + let bin_path: PathBuf = build_hermit_bin("fs_tests"); + + let res = run_vm_in_thread(bin_path, params); + assert_eq!(res.code, 0); +} + /// Tests file descriptor sandbox, particularly whether... /// - the guest can make a File out of fd 1 (stdout) and write to it. /// - the guest can make a File out of fd 2 (stderr) and write to it. diff --git a/tests/test-kernels/src/bin/fs_tests.rs b/tests/test-kernels/src/bin/fs_tests.rs index be681ade..07dc403f 100644 --- a/tests/test-kernels/src/bin/fs_tests.rs +++ b/tests/test-kernels/src/bin/fs_tests.rs @@ -99,6 +99,13 @@ fn open_read_only_write(filename: &str) { } } +fn open_read(filename: &str) { + let mut file = File::open(filename).unwrap(); + let mut buf = Vec::new(); + file.read_to_end(&mut buf).unwrap(); + assert_eq!(buf, b"Hello, world!\n"); +} + fn lseek_file(filename: &str) { let mut buf: [u8; 10] = [0; 10]; println!("Initial Buffer: {buf:?}"); @@ -174,6 +181,7 @@ fn main() { "fd_open_remove_before_and_after_closing" => open_remove_before_and_after_closing(filename), "fd_remove_twice_before_closing" => remove_twice_before_closing(filename), "open_read_only_write" => open_read_only_write(filename), + "open_read" => open_read(filename), "lseek_file" => lseek_file(filename), "mounts_test" => mount_test(), _ => panic!("test not found"), diff --git a/uhyve-interface/src/parameters.rs b/uhyve-interface/src/parameters.rs index 3fb00d16..6ea792d0 100644 --- a/uhyve-interface/src/parameters.rs +++ b/uhyve-interface/src/parameters.rs @@ -148,7 +148,7 @@ pub struct SerialWriteBufferParams { pub use hermit_abi::{ O_APPEND, O_CREAT, O_DIRECTORY, O_EXCL, O_RDONLY, O_RDWR, O_TRUNC, O_WRONLY, SEEK_CUR, SEEK_END, SEEK_SET, - errno::{EBADF, EFAULT, EINVAL, ENOENT, EROFS}, + errno::{EBADF, EFAULT, EINVAL, EISDIR, ENOENT, EROFS}, }; // File operations supported by Hermit and Uhyve