Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ semihosting = ["dep:semihosting"]
## _application processor_s (AP) from the _boot-strap processor_ (BSP).
smp = ["acpi"]

## Enables (internal) hermit image support.
##
## This allows packaging a kernel together with all its static environment files into a .tar.gz archive,
## whose decompressed version gets handled by the kernel
## (the decompression gets handled by the bootloader / hypervisor).
hermit-image = ["dep:tar-no-std"]

#! ### Network Features

## Enables TCP support.
Expand Down Expand Up @@ -326,6 +333,7 @@ simple-shell = { version = "0.0.1", optional = true }
smallvec = { version = "1", features = ["const_new"] }
take-static = "0.1"
talc = { version = "5" }
tar-no-std = { version = "0.4", optional = true, features = ["alloc"] }
thiserror = { version = "2", default-features = false }
time = { version = "0.3", default-features = false }
volatile = "0.6"
Expand Down
88 changes: 73 additions & 15 deletions src/fs/mem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,6 @@ pub(crate) struct RamFileInner {
pub attr: FileAttr,
}

impl RamFileInner {
pub fn new(attr: FileAttr) -> Self {
Self {
data: Vec::new(),
attr,
}
}
}

pub struct RamFileInterface {
/// Position within the file
pos: Mutex<usize>,
Expand Down Expand Up @@ -354,6 +345,10 @@ impl VfsNode for RamFile {

impl RamFile {
pub fn new(mode: AccessPermission) -> Self {
Self::new_with_data(Vec::new(), mode)
}

fn new_with_data(data: Vec<u8>, mode: AccessPermission) -> Self {
let microseconds = arch::kernel::systemtime::now_micros();
let t = timespec::from_usec(microseconds as i64);
let attr = FileAttr {
Expand All @@ -365,7 +360,7 @@ impl RamFile {
};

Self {
data: Arc::new(RwLock::new(RamFileInner::new(attr))),
data: Arc::new(RwLock::new(RamFileInner { data, attr })),
}
}
}
Expand Down Expand Up @@ -464,6 +459,64 @@ impl MemDirectory {
}
}

#[cfg(feature = "hermit-image")]
pub fn try_from_image(image: &'static [u8]) -> io::Result<Self> {
let this = Self::new(AccessPermission::S_IRUSR);

let tar_archive_ref = tar_no_std::TarArchiveRef::new(image).map_err(|e| {
error!("[Hermit image] Tar file has invalid format: {e:?}");
Errno::Inval
})?;

for entry in tar_archive_ref.entries() {
let filename = entry.filename();
let filename = filename.as_str().map_err(|e| {
error!(
"[Hermit image] Tar entry has not supported filename (non UTF-8): {filename:?}; {e}",
);
Errno::Inval
})?;
if filename.is_empty() {
continue;
}
debug!("[Hermit image] Processing tar entry: {filename}");

let mode = entry.posix_header().mode.to_flags().map_err(|e| {
error!(
"[Hermit image] Tar entry {filename:?} has invalid mode: {:?}; {e}",
entry.posix_header().mode,
);
Errno::Inval
})?;
let mode = AccessPermission::from_bits(mode.bits() as u32).ok_or_else(|| {
error!("[Hermit image] Tar entry {filename:?} has invalid mode: {mode:?}");
Errno::Inval
})?;

for (i, _) in filename.match_indices("/") {
let part = &filename[..i];
if this.traverse_lstat(part).is_err() {
this.traverse_mkdir(
part,
AccessPermission::S_IRUSR
| AccessPermission::S_IWUSR
| AccessPermission::S_IRGRP,
)
.inspect_err(|e| {
error!("[Hermit image] Unable to mkdir {part:?}: {e}");
})?;
}
}

this.traverse_create_file(filename, entry.data(), mode)
.inspect_err(|e| {
error!("[Hermit image] Unable to write entry {filename:?}: {e}");
})?;
}

Ok(this)
}

async fn async_traverse_open(
&self,
path: &str,
Expand Down Expand Up @@ -693,11 +746,16 @@ impl VfsNode for MemDirectory {
return directory.traverse_create_file(rest, data, mode);
}

let file = RomFile::new(data, mode);
self.inner
.write()
.await
.insert(component.to_owned(), Box::new(file));
let file: Box<dyn VfsNode> = if mode.contains(AccessPermission::S_IWUSR)
|| mode.contains(AccessPermission::S_IWGRP)
|| mode.contains(AccessPermission::S_IWOTH)
{
Box::new(RamFile::new_with_data(data.to_vec(), mode))
} else {
Box::new(RomFile::new(data, mode))
};

self.inner.write().await.insert(component.to_owned(), file);
Ok(())
},
None,
Expand Down
16 changes: 15 additions & 1 deletion src/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,21 @@ pub(crate) fn init() {
const VERSION: &str = env!("CARGO_PKG_VERSION");
const UTC_BUILT_TIME: &str = build_time::build_time_utc!();

let root_filesystem = Filesystem::new();
#[cfg_attr(not(feature = "hermit-image"), expect(unused_mut))]
let mut root_filesystem = Filesystem::new();

// Handle optional Hermit Image specified in FDT.
#[cfg(feature = "hermit-image")]
if let Some(tar_image) = crate::mm::hermit_tar_image() {
root_filesystem.root =
MemDirectory::try_from_image(tar_image).expect("Unable to parse Hermit Image");
Comment on lines +313 to +314
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we not print an error instead of panicking and continue?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't really make sense to boot a kernel that expects a very specific file system environment, and letting the kernel later run into errors when the image loading fails.

}

if crate::mm::hermit_tar_image().is_some() {
error!(
"Kernel built without Hermit image support, but a Hermit image was supplied: ignoring"
);
}

root_filesystem
.mkdir("/tmp", AccessPermission::from_bits(0o777).unwrap())
Expand Down
73 changes: 69 additions & 4 deletions src/mm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ mod page_range_alloc;
mod physicalmem;
mod virtualmem;

use core::cmp;
use core::ops::Range;

use align_address::Align;
Expand Down Expand Up @@ -90,22 +91,86 @@ pub(crate) fn kernel_end_address() -> VirtAddr {
KERNEL_ADDR_RANGE.end
}

/// Physical and virtual address range of the Hermit image, in case it is present
/// (indicated via FDT).
static HERMIT_IMAGE_START_AND_LEN: Lazy<Option<(VirtAddr, usize)>> = Lazy::new(|| {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is having a lazy static for this worth it? Or would a function suffice?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to avoid parsing this information multiple times;

Normally, we need it twice: once during the memory setup (to ensure that memory is reserved for it, even tho the FDT reserved memory regions should take care of that), and once during filesystem setup.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you measure any benefit here? I expect the parsing overhead to be minimal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably; but I prefer an implementation that makes it harder to find out when stuff can panic and when not.

And in general, it might be possible to "thread" this information through the call-stack via argument passing, but mm/mod.rs and fs/mod.rs are quite far apart from each other in terms of invocation sites.

let fdt = env::fdt()?;

// per FDT specification, /chosen always exists
let chosen = fdt.find_node("/chosen").unwrap();

let fdt::node::NodeProperty {
value: image_reg, ..
} = chosen.property("image_reg")?;
Comment on lines +102 to +104
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a spec on the design of this FDT node?

Why is it so much more complicated to parse than a Linux initrd (https://github.com/hermit-os/loader/blob/v0.5.6/src/arch/riscv64/mod.rs#L21-L31)?

Should we model it like a Linux initrd instead? We could do either straight up linux,initrd-start and linux,initrd-end as that is what we use with other VMMs or adapt Hermit-specific names just for Uhyve (hermit,... or uhyve,...).

Copy link
Copy Markdown
Contributor Author

@fogti fogti May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Doing it the way I described above would make sense, I think.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, hermit-specific naming would make sense.


let cell_sizes = fdt.root().cell_sizes();
let split_point = cell_sizes.address_cells * 4;
let end_point = split_point + cell_sizes.size_cells * 4;

if image_reg.len() != end_point {
return None;
}

let (addr, len) = image_reg.split_at(split_point);

if addr.len() == size_of::<*const u8>() && len.len() == size_of::<usize>() {
let addr = usize::from_be_bytes(addr.try_into().unwrap());
let len = usize::from_be_bytes(len.try_into().unwrap());
info!("Hermit image at {addr:x} with length {len:x}");
Some((
VirtAddr::from_ptr(core::ptr::with_exposed_provenance::<u8>(addr)),
len,
))
} else {
error!(
"Hermit image supplied with invalid address range (#addr = {}, #len = {})",
addr.len(),
len.len(),
);
None
}
});

pub(crate) fn hermit_tar_image() -> Option<&'static [u8]> {
// technically, the following is UB, because the kernel might be contained within...
HERMIT_IMAGE_START_AND_LEN
.map(|(addr, len)| unsafe { core::slice::from_raw_parts(addr.as_ptr(), len) })
Comment on lines +135 to +137
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the FDT node include the kernel? Why not only put the initrd into the node?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The kernel doesn't know if it is contained in it or not, it is simply possible that the ranges overlap (at least currently we don't outlaw that because it might provide memory usage savings in case of using the hermit-loader; uhyve always copies the kernel anyways).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand. How can they ever overlap? Sure, the image could contain the kernel ELF disk image, but not the loaded image, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For uhyve, the case is clear: https://github.com/fogti/uhyve/blob/278bfd24a8744adc762fd598bcd025495bcc1d9b/src/vm.rs#L365
For loader, I'm not sure. There is no final implementation yet, and idk how the loading (dyn-linking?) of the kernel works there.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loader works very similarly to Uhyve. It's just happening from inside the VM.

We get an ELF image and load the ELF image to memory by copying. The running executable is not running straight from the ELF image.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, then normally, no overlap should be possible (except by loader error).

}

#[cfg(target_os = "none")]
pub(crate) fn init() {
use crate::arch::mm::paging;
use arch::mm::paging;

Lazy::force(&KERNEL_ADDR_RANGE);

unsafe {
arch::mm::init();
}

Lazy::force(&HERMIT_IMAGE_START_AND_LEN);

let total_mem = physicalmem::total_memory_size();
let kernel_addr_range = KERNEL_ADDR_RANGE.clone();
let reserved_addr_range = if let Some((image_start, image_len)) = *HERMIT_IMAGE_START_AND_LEN {
cmp::min(
kernel_addr_range.start,
image_start.align_down(LargePageSize::SIZE),
)
..cmp::max(
kernel_addr_range.end,
(image_start + image_len).align_up(LargePageSize::SIZE),
)
} else {
kernel_addr_range.clone()
};
info!("Total memory size: {} MiB", total_mem >> 20);
info!(
"Kernel region: {:p}..{:p}",
kernel_addr_range.start, kernel_addr_range.end
kernel_addr_range.start, kernel_addr_range.end,
);
info!(
"Locally reserved region: {:p}..{:p}",
reserved_addr_range.start, reserved_addr_range.end,
);

// we reserve physical memory for the required page tables
Expand All @@ -126,7 +191,7 @@ pub(crate) fn init() {
// On UEFI, the given memory is guaranteed free memory and the kernel is located before the given memory
reserved_space
} else {
(kernel_addr_range.end.as_u64() - env::get_ram_address().as_u64() + reserved_space as u64)
(reserved_addr_range.end.as_u64() - env::get_ram_address().as_u64() + reserved_space as u64)
as usize
};
info!("Minimum memory size: {} MiB", min_mem >> 20);
Expand All @@ -146,7 +211,7 @@ pub(crate) fn init() {
// we reserve at least 75% of the memory for the user space
let reserve: usize = (avail_mem * 75) / 100;
// 64 MB is enough as kernel heap
let reserve = core::cmp::min(reserve, 0x0400_0000);
let reserve = cmp::min(reserve, 0x0400_0000);

let virt_size: usize = reserve.align_down(LargePageSize::SIZE as usize);
let layout = PageLayout::from_size_align(virt_size, LargePageSize::SIZE as usize).unwrap();
Expand Down
2 changes: 1 addition & 1 deletion xtask/src/clippy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ impl Clippy {
clippy().run()?;
clippy().arg("--features=common-os").run()?;
clippy()
.arg("--features=acpi,dns,fsgsbase,pci,smp,vga")
.arg("--features=acpi,dns,fsgsbase,pci,smp,vga,hermit-image")
.run()?;
clippy().arg("--no-default-features").run()?;
clippy().arg("--all-features").run()?;
Expand Down
Loading