Skip to content

Commit 582c866

Browse files
committed
feat: handle hermit images + config nested in image
1 parent 924ed3c commit 582c866

File tree

6 files changed

+523
-90
lines changed

6 files changed

+523
-90
lines changed

Cargo.lock

Lines changed: 41 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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ uuid = { version = "1.18.1", features = ["fast-rng", "v4"]}
7171
toml = "0.9.8"
7272
serde = { version = "1.0", features = ["derive"] }
7373
merge = { version = "0.2" }
74+
memmap2 = "0.9.8"
75+
76+
[dependencies.hermit-image-reader]
77+
git = "https://codeberg.org/hermit-os-contrib/hermit-image-reader.git"
7478

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

src/bin/uhyve.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::{fs, num::ParseIntError, path::PathBuf, process, str::FromStr};
55
use clap::{Command, CommandFactory, Parser, error::ErrorKind};
66
use core_affinity::CoreId;
77
use env_logger::Builder;
8-
use log::{LevelFilter, info};
8+
use log::{LevelFilter, info, warn};
99
use merge::Merge;
1010
use serde::Deserialize;
1111
use thiserror::Error;
@@ -146,6 +146,17 @@ struct UhyveArgs {
146146
#[serde(skip)]
147147
#[merge(strategy = merge::option::overwrite_none)]
148148
pub config: Option<PathBuf>,
149+
150+
/// Kernel image file
151+
///
152+
/// Reads configuration and associated data from a locally given path.
153+
///
154+
/// If `--config CONFIG_FILE` is given, the configurations are merged.
155+
/// Also keep in mind that the configuration formats differ.
156+
#[clap(long)]
157+
#[serde(skip)]
158+
#[merge(strategy = merge::option::overwrite_none)]
159+
pub image: Option<PathBuf>,
149160
}
150161

151162
/// Arguments for memory resources allocated to the guest (both guest and host).
@@ -382,6 +393,7 @@ impl From<Args> for Params {
382393
#[cfg(target_os = "linux")]
383394
gdb_port,
384395
config: _,
396+
image: _,
385397
},
386398
memory:
387399
MemoryArgs {
@@ -451,6 +463,59 @@ fn read_toml_contents(toml_path: &PathBuf) -> Result<Args, Box<dyn std::error::E
451463
///
452464
/// This overrides missing CLI configs (None) with configs obtained from config file.
453465
fn load_vm_config(args: &mut Args) {
466+
// If there is an image given, try to find a config file in it, and load that
467+
if let Some(image_file) = &args.uhyve.image {
468+
// Unfortunately, we have to read the image twice (unless we figure out a way to cache it...)
469+
let data = std::fs::read(image_file).expect("unable to read supplied hermit image file");
470+
let decompressed = hermit_image_reader::decompress_image(&data[..])
471+
.expect("unable to decompress supplied hermit image file");
472+
473+
let entry = hermit_image_reader::ImageParser::new(&decompressed[..])
474+
.filter_map(|i| i.ok())
475+
.filter(|i| {
476+
if let Ok(name) = str::from_utf8(&i.name) {
477+
name == hermit_image_reader::config::DEFAULT_CONFIG_NAME
478+
} else {
479+
false
480+
}
481+
})
482+
// for `tar` and similar formats, duplicate entries are allowed,
483+
// and the latest entry wins
484+
.last();
485+
if let Some(entry) = entry {
486+
let mut config = hermit_image_reader::config::parse(entry.value)
487+
.expect("unable to parse config of supplied hermit image file");
488+
489+
// .input
490+
args.guest.kernel_args.append(&mut config.input.kernel_args);
491+
if !config.input.app_args.is_empty() {
492+
args.guest.kernel_args.push("--".to_string());
493+
args.guest.kernel_args.append(&mut config.input.app_args);
494+
}
495+
args.guest.env_vars.append(&mut config.input.env_vars);
496+
// don't pass privileged env-var commands through
497+
args.guest.env_vars.retain(|i| i.contains('='));
498+
499+
// .requirements
500+
// TODO: currently no corresponding options exist
501+
502+
let image_file_str = image_file.to_str().unwrap();
503+
504+
// .kernel
505+
args.guest.kernel = format!("{}:{}", image_file_str, config.kernel).into();
506+
507+
// .file_mapping
508+
// TODO: improve this
509+
for (key, value) in config.file_mapping {
510+
args.uhyve
511+
.file_mapping
512+
.push(format!("{}:{}:{}", image_file_str, key, value));
513+
}
514+
} else {
515+
warn!("Supplied hermit image file doesn't contain a configuration file");
516+
}
517+
}
518+
454519
// Tries to read arguments from a configuration file. If it doesn't exist or if
455520
// parsing is not possible, panic.
456521
if let Some(config_file) = args.get_config_file() {
@@ -624,6 +689,7 @@ mod tests {
624689
#[cfg(target_os = "linux")]
625690
gdb_port: None,
626691
config: Some(PathBuf::from("config.txt")),
692+
image: None,
627693
},
628694
memory: MemoryArgs {
629695
memory_size: None,
@@ -683,6 +749,7 @@ mod tests {
683749
#[cfg(target_os = "linux")]
684750
gdb_port: Some(1),
685751
config: Some(PathBuf::from("config.txt")),
752+
image: None,
686753
},
687754
memory: MemoryArgs {
688755
memory_size: Some(GuestMemorySize::from_str("16MiB").unwrap()),

src/hypercall.rs

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
use std::{
22
ffi::{CStr, CString, OsStr},
3+
fs::File,
34
io::{self, Error, ErrorKind},
4-
os::{fd::IntoRawFd, unix::ffi::OsStrExt},
5+
os::{
6+
fd::IntoRawFd,
7+
unix::{ffi::OsStrExt, io::FromRawFd},
8+
},
9+
sync::Arc,
510
};
611

12+
use clean_path::clean;
713
use uhyve_interface::{GuestPhysAddr, Hypercall, HypercallAddress, MAX_ARGC_ENVC, parameters::*};
814

915
use crate::{
10-
isolation::filemap::UhyveFileMap,
16+
isolation::filemap::{HermitImageThinFile, MappedFileMutRef, UhyveFileMap},
1117
mem::{MemoryError, MmapMemory},
1218
params::EnvVars,
1319
virt_to_phys,
@@ -88,8 +94,44 @@ pub fn unlink(mem: &MmapMemory, sysunlink: &mut UnlinkParams, file_map: &mut Uhy
8894
sysunlink.ret = if let Some(host_path) = file_map.get_host_path(guest_path) {
8995
// We can safely unwrap here, as host_path.as_bytes will never contain internal \0 bytes
9096
// As host_path_c_string is a valid CString, this implementation is presumed to be safe.
91-
let host_path_c_string = CString::new(host_path.as_bytes()).unwrap();
92-
unsafe { libc::unlink(host_path_c_string.as_c_str().as_ptr()) }
97+
match host_path {
98+
MappedFileMutRef::OnHost(oh) => {
99+
let host_path_c_string = CString::new(oh.as_os_str().as_bytes()).unwrap();
100+
unsafe { libc::unlink(host_path_c_string.as_c_str().as_ptr()) }
101+
}
102+
MappedFileMutRef::InImage { .. } => {
103+
// we need to find the parent entry
104+
// TODO: reject if the parent entry points to a different entry / image
105+
let mut guest_pathbuf = clean(OsStr::from_bytes(guest_path.to_bytes()));
106+
(|| {
107+
let Some(file_name) = guest_pathbuf.file_name() else {
108+
return -ENOENT;
109+
};
110+
let Ok(file_name) = str::from_utf8(file_name.as_bytes()) else {
111+
return -ENOENT;
112+
};
113+
let file_name = file_name.to_string();
114+
if !guest_pathbuf.pop() {
115+
return -ENOENT;
116+
}
117+
let guest_path_c_string =
118+
CString::new(guest_pathbuf.as_os_str().as_bytes()).unwrap();
119+
if let Some(MappedFileMutRef::InImage { image: _, thin }) =
120+
file_map.get_host_path(&guest_path_c_string)
121+
{
122+
// TODO: reject unlinking of directories
123+
thin.unlink(&[&*file_name]);
124+
0
125+
} else {
126+
// TODO: unmount entire image if we unlink the root?
127+
error!(
128+
"The kernel requested to unlink() an unknown path ({guest_path:?}): Rejecting..."
129+
);
130+
-ENOENT
131+
}
132+
})()
133+
}
134+
}
93135
} else {
94136
error!("The kernel requested to unlink() an unknown path ({guest_path:?}): Rejecting...");
95137
-ENOENT
@@ -109,16 +151,63 @@ pub fn open(mem: &MmapMemory, sysopen: &mut OpenParams, file_map: &mut UhyveFile
109151
return;
110152
}
111153

112-
if let Some(host_path) = file_map.get_host_path(guest_path) {
154+
if let Some(guest_entry) = file_map.get_host_path(guest_path) {
113155
debug!("{guest_path:#?} found in file map.");
114-
// We can safely unwrap here, as host_path.as_bytes will never contain internal \0 bytes
115-
// As host_path_c_string is a valid CString, this implementation is presumed to be safe.
116-
let host_path_c_string = CString::new(host_path.as_bytes()).unwrap();
156+
match guest_entry {
157+
MappedFileMutRef::OnHost(host_path) => {
158+
// We can safely unwrap here, as host_path.as_bytes will never contain internal \0 bytes
159+
// As host_path_c_string is a valid CString, this implementation is presumed to be safe.
160+
let host_path_c_string = CString::new(host_path.as_os_str().as_bytes()).unwrap();
161+
162+
sysopen.ret = unsafe {
163+
libc::open(host_path_c_string.as_c_str().as_ptr(), flags, sysopen.mode)
164+
};
117165

118-
sysopen.ret =
119-
unsafe { libc::open(host_path_c_string.as_c_str().as_ptr(), flags, sysopen.mode) };
166+
file_map.fdmap.insert_fd(sysopen.ret);
167+
}
168+
MappedFileMutRef::InImage {
169+
image,
170+
thin: HermitImageThinFile::File(r),
171+
} => {
172+
// For simplicity, create a temporary files with these contents.
173+
debug!("Attempting to open a temp file for unpacked {guest_path:#?}...");
174+
// Existing files that already exist should be in the file map, not here.
175+
// If a supposed attacker can predict where we open a file and its filename,
176+
// this contigency, together with O_CREAT, will cause the write to fail.
177+
flags |= O_EXCL;
178+
let image = Arc::clone(image);
179+
let r = r.clone();
120180

121-
file_map.fdmap.insert_fd(sysopen.ret);
181+
let host_path_c_string = file_map.create_temporary_file(guest_path);
182+
let new_host_path = host_path_c_string.as_c_str().as_ptr();
183+
sysopen.ret = unsafe { libc::open(new_host_path, flags, sysopen.mode) };
184+
if sysopen.ret >= 0 {
185+
use std::io::Write;
186+
let raw_fd = sysopen.ret.into_raw_fd();
187+
let mut fh = unsafe { File::from_raw_fd(raw_fd) };
188+
// SAFETY (slice access): we assume that the image parsing was correct
189+
// and that the temporary file wasn't modified after creation.
190+
match fh.write_all(&image[r.clone()]) {
191+
Ok(_) => {
192+
// don't destroy the file descriptor
193+
core::mem::forget(fh);
194+
file_map.fdmap.insert_fd(sysopen.ret.into_raw_fd());
195+
}
196+
197+
Err(_) => {
198+
// TODO: what's the correct error code for this?
199+
sysopen.ret = -ENOENT;
200+
201+
unsafe { libc::unlink(new_host_path) };
202+
}
203+
}
204+
}
205+
}
206+
_ => {
207+
debug!("Returning -ENOENT for {guest_path:#?}");
208+
sysopen.ret = -ENOENT;
209+
}
210+
};
122211
} else {
123212
debug!("{guest_path:#?} not found in file map.");
124213
if (flags & O_CREAT) == O_CREAT {

0 commit comments

Comments
 (0)