Skip to content

Commit 08693f9

Browse files
committed
feat(isolation,vm): Implement support for Hermit images
This is the Hermit image implementation variant which uses virtual (in-memory) files on the hypervisor level (in contrast to the kernel level).
1 parent a0786c8 commit 08693f9

File tree

7 files changed

+224
-16
lines changed

7 files changed

+224
-16
lines changed

Cargo.lock

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ clap = { version = "4.5", features = ["derive", "env"] }
4141
clean-path = "0.2.1"
4242
core_affinity = "0.8"
4343
env_logger = "0.11"
44+
flate2 = "1.1"
4445
gdbstub = "0.7"
4546
gdbstub_arch = "0.3"
4647
hermit-abi = "0.5"
@@ -67,6 +68,7 @@ virtio-bindings = "~0.2.7"
6768
vm-fdt = "0.3"
6869
vm-memory = { version = "0.18", features = ["backend-mmap"] }
6970
uuid = { version = "1.21.0", features = ["fast-rng", "v4"]}
71+
tar-no-std = { version = "0.4", features = ["alloc"] }
7072

7173
[target.'cfg(target_os = "linux")'.dependencies]
7274
kvm-bindings = "0.14"

src/bin/uhyve.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ impl<'de> serde::de::Deserialize<'de> for Affinity {
400400
#[derive(Debug, Default, Deserialize, Merge, Parser)]
401401
#[cfg_attr(test, derive(PartialEq))]
402402
struct GuestArgs {
403-
/// The kernel to execute
403+
/// The kernel or Hermit image to execute
404404
#[serde(skip)]
405405
#[merge(skip)]
406406
kernel: PathBuf,

src/isolation/filemap/mod.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{ffi::CString, os::unix::ffi::OsStrExt, path::PathBuf};
1+
use std::{ffi::CString, os::unix::ffi::OsStrExt, path::PathBuf, sync::Arc};
22

33
#[cfg(target_os = "linux")]
44
use libc::{O_DIRECT, O_SYNC};
@@ -50,6 +50,19 @@ impl From<Option<String>> for UhyveIoMode {
5050
}
5151
}
5252

53+
#[derive(Clone, Debug, thiserror::Error)]
54+
pub enum HermitImageError {
55+
#[error("The Hermit image tar file is corrupted: {0}")]
56+
CorruptData(#[from] tar_no_std::CorruptDataError),
57+
58+
#[error("A file in the Hermit image doesn't have an UTF-8 file name: {0:?}")]
59+
// NOTE: `Box`ed to avoid too large size differences between enum entries.
60+
NonUtf8Filename(Box<tar_no_std::TarFormatString<256>>),
61+
62+
#[error("Unable to create a file from the Hermit image in the directory tree: {0:?}")]
63+
CreateLeaf(String),
64+
}
65+
5366
/// Wrapper around a `HashMap` to map guest paths to arbitrary host paths and track file descriptors.
5467
#[derive(Debug)]
5568
pub struct UhyveFileMap {
@@ -92,6 +105,28 @@ impl UhyveFileMap {
92105
fm
93106
}
94107

108+
/// Adds the contents of a decompressed hermit image to the file map.
109+
pub fn add_hermit_image(&mut self, tar_bytes: &[u8]) -> Result<(), HermitImageError> {
110+
if tar_bytes.is_empty() {
111+
return Ok(());
112+
}
113+
114+
for i in tar_no_std::TarArchiveRef::new(tar_bytes)?.entries() {
115+
let filename = i.filename();
116+
let filename = filename
117+
.as_str()
118+
.map_err(|_| HermitImageError::NonUtf8Filename(Box::new(filename)))?;
119+
let data: Arc<[u8]> = i.data().to_vec().into();
120+
121+
// UNWRAP: `tar_no_std::ArchiveEntry::filename` already truncates at the first null byte.
122+
if !self.create_leaf(filename, UhyveMapLeaf::Virtual(data)) {
123+
return Err(HermitImageError::CreateLeaf(filename.to_string()));
124+
}
125+
}
126+
127+
Ok(())
128+
}
129+
95130
/// Returns the host_path on the host filesystem given a requested guest_path, if it exists.
96131
///
97132
/// * `guest_path` - The guest path that is to be looked up in the map.

src/lib.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,24 @@ pub enum HypervisorError {
4949
#[error("Invalid kernel path ({0})")]
5050
InvalidKernelPath(PathBuf),
5151

52+
#[error(transparent)]
53+
HermitImageError(#[from] crate::isolation::filemap::HermitImageError),
54+
55+
#[error("Unable to find Hermit image config in archive")]
56+
HermitImageConfigNotFound,
57+
58+
#[error("Unable to parse Hermit image config: {0}")]
59+
HermitImageConfigParseError(#[from] toml::de::Error),
60+
61+
#[error("Insufficient guest memory size: got = {got}, wanted = {wanted}")]
62+
InsufficientGuestMemorySize {
63+
got: byte_unit::Byte,
64+
wanted: byte_unit::Byte,
65+
},
66+
67+
#[error("Insufficient guest CPU count: got = {got}, wanted = {wanted}")]
68+
InsufficientGuestCPUs { got: u32, wanted: u32 },
69+
5270
#[error("Kernel Loading Error: {0}")]
5371
LoadedKernelError(#[from] vm::LoadKernelError),
5472
}

src/params.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ impl FromStr for CpuCount {
150150
}
151151

152152
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
153-
pub struct GuestMemorySize(Byte);
153+
pub struct GuestMemorySize(pub(crate) Byte);
154154

155155
impl GuestMemorySize {
156156
const fn minimum() -> Byte {
@@ -257,13 +257,13 @@ impl Default for EnvVars {
257257
Self::Set(HashMap::new())
258258
}
259259
}
260-
impl<S: AsRef<str> + std::fmt::Debug + PartialEq<S> + From<&'static str>> TryFrom<&[S]>
260+
impl<S: AsRef<str> + core::fmt::Debug + PartialEq + PartialEq<&'static str>> TryFrom<&[S]>
261261
for EnvVars
262262
{
263263
type Error = &'static str;
264264

265265
fn try_from(v: &[S]) -> Result<Self, Self::Error> {
266-
if v.contains(&S::from("host")) {
266+
if v.iter().any(|i| *i == "host") {
267267
if v.len() != 1 {
268268
warn!(
269269
"Specifying -e host discards all other explicitly specified environment vars"

src/vm.rs

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::{
22
env, fmt, fs, io,
3+
mem::{drop, take},
34
num::NonZero,
45
os::unix::prelude::JoinHandleExt,
56
path::PathBuf,
@@ -10,8 +11,9 @@ use std::{
1011

1112
use core_affinity::CoreId;
1213
use hermit_entry::{
13-
HermitVersion, UhyveIfVersion,
14+
Format, HermitVersion, UhyveIfVersion,
1415
boot_info::{BootInfo, HardwareInfo, LoadInfo, PlatformInfo, RawBootInfo, SerialPortBase},
16+
config, detect_format,
1517
elf::{KernelObject, LoadedKernel, ParseKernelError},
1618
};
1719
use internal::VirtualizationBackendInternal;
@@ -24,7 +26,7 @@ use crate::{
2426
consts::*,
2527
fdt::Fdt,
2628
generate_address,
27-
isolation::filemap::UhyveFileMap,
29+
isolation::filemap::{UhyveFileMap, UhyveMapLeaf},
2830
mem::MmapMemory,
2931
os::KickSignal,
3032
params::{EnvVars, Params},
@@ -137,11 +139,126 @@ pub struct UhyveVm<VirtBackend: VirtualizationBackend> {
137139
pub(crate) kernel_info: Arc<KernelInfo>,
138140
}
139141
impl<VirtBackend: VirtualizationBackend> UhyveVm<VirtBackend> {
140-
pub fn new(kernel_path: PathBuf, params: Params) -> HypervisorResult<UhyveVm<VirtBackend>> {
142+
pub fn new(kernel_path: PathBuf, mut params: Params) -> HypervisorResult<UhyveVm<VirtBackend>> {
141143
let memory_size = params.memory_size.get();
142144

143-
let elf = fs::read(&kernel_path)
145+
let kernel_data = fs::read(&kernel_path)
144146
.map_err(|_e| HypervisorError::InvalidKernelPath(kernel_path.clone()))?;
147+
148+
// TODO: file_mapping not in kernel_info
149+
let mut file_mapping = UhyveFileMap::new(
150+
&params.file_mapping,
151+
params.tempdir.clone(),
152+
#[cfg(target_os = "linux")]
153+
params.io_mode,
154+
);
155+
156+
// `kernel_data` might be an Hermit image
157+
let elf = match detect_format(&kernel_data[..]) {
158+
None => return Err(HypervisorError::InvalidKernelPath(kernel_path.clone())),
159+
Some(Format::Elf) => kernel_data,
160+
Some(Format::Gzip) => {
161+
{
162+
use io::Read;
163+
164+
// decompress image
165+
let mut buf_decompressed = Vec::new();
166+
flate2::bufread::GzDecoder::new(&kernel_data[..])
167+
.read_to_end(&mut buf_decompressed)?;
168+
drop(kernel_data);
169+
170+
// insert Hermit image tree into file map
171+
file_mapping.add_hermit_image(&buf_decompressed[..])?;
172+
}
173+
174+
// TODO: make `hermit_entry::config::DEFAULT_CONFIG_NAME` public and use that here instead.
175+
let config_data = if let Some(UhyveMapLeaf::Virtual(data)) =
176+
file_mapping.get_host_path("/hermit.toml")
177+
{
178+
data
179+
} else {
180+
return Err(HypervisorError::HermitImageConfigNotFound);
181+
};
182+
183+
let config: config::Config<'_> = toml::from_slice(&config_data[..])?;
184+
185+
// handle Hermit image configuration
186+
match config {
187+
config::Config::V1 {
188+
mut input,
189+
requirements,
190+
kernel,
191+
} => {
192+
// .input
193+
if params.kernel_args.is_empty() {
194+
params.kernel_args.append(
195+
&mut take(&mut input.kernel_args)
196+
.into_iter()
197+
.map(|i| i.into_owned())
198+
.collect(),
199+
);
200+
if !input.app_args.is_empty() {
201+
params.kernel_args.push("--".to_string());
202+
params.kernel_args.append(
203+
&mut take(&mut input.app_args)
204+
.into_iter()
205+
.map(|i| i.into_owned())
206+
.collect(),
207+
)
208+
}
209+
}
210+
debug!("Passing kernel arguments: {:?}", &params.kernel_args);
211+
212+
// don't pass privileged env-var commands through
213+
input.env_vars.retain(|i| i.contains('='));
214+
215+
if let EnvVars::Set(env) = &mut params.env {
216+
if let Ok(EnvVars::Set(prev_env_vars)) =
217+
EnvVars::try_from(&input.env_vars[..])
218+
{
219+
// env vars from params take precedence
220+
let new_env_vars = take(env);
221+
*env = prev_env_vars.into_iter().chain(new_env_vars).collect();
222+
} else {
223+
warn!("Unable to parse env vars from Hermit image configuration");
224+
}
225+
} else if input.env_vars.is_empty() {
226+
info!("Ignoring Hermit image env vars due to `-e host`");
227+
}
228+
229+
// .requirements
230+
231+
// TODO: what about default memory size?
232+
if let Some(required_memory_size) = requirements.memory
233+
&& params.memory_size.0 < required_memory_size
234+
{
235+
return Err(HypervisorError::InsufficientGuestMemorySize {
236+
got: params.memory_size.0,
237+
wanted: required_memory_size,
238+
});
239+
}
240+
241+
if params.cpu_count.get() < requirements.cpus {
242+
return Err(HypervisorError::InsufficientGuestCPUs {
243+
got: params.cpu_count.get(),
244+
wanted: requirements.cpus,
245+
});
246+
}
247+
248+
// .kernel
249+
if let Some(UhyveMapLeaf::Virtual(data)) =
250+
file_mapping.get_host_path(&kernel)
251+
{
252+
data.to_vec()
253+
} else {
254+
error!("Unable to find kernel in Hermit image");
255+
return Err(HypervisorError::InvalidKernelPath(kernel_path.clone()));
256+
}
257+
}
258+
}
259+
}
260+
};
261+
145262
let object: KernelObject<'_> =
146263
KernelObject::parse(&elf).map_err(LoadKernelError::ParseKernelError)?;
147264

@@ -203,13 +320,6 @@ impl<VirtBackend: VirtualizationBackend> UhyveVm<VirtBackend> {
203320
#[cfg(not(target_os = "linux"))]
204321
let mut mem = MmapMemory::new(memory_size, guest_address, false, false);
205322

206-
// TODO: file_mapping not in kernel_info
207-
let file_mapping = UhyveFileMap::new(
208-
&params.file_mapping,
209-
params.tempdir.clone(),
210-
#[cfg(target_os = "linux")]
211-
params.io_mode,
212-
);
213323
let mounts: Vec<_> = file_mapping.get_all_guest_dirs().collect();
214324

215325
let serial = UhyveSerial::from_params(&params.output)?;

0 commit comments

Comments
 (0)