201 lines
6.7 KiB
Rust
201 lines
6.7 KiB
Rust
//! Kernel and initramfs extraction from RVF files.
|
|
//!
|
|
//! Opens the RVF store read-only, locates the KERNEL_SEG, parses the
|
|
//! KernelHeader, and writes the kernel image and optional initramfs to
|
|
//! temporary files that persist until the returned handles are dropped.
|
|
|
|
use std::io::Write;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use rvf_runtime::RvfStore;
|
|
use rvf_types::kernel::KernelHeader;
|
|
|
|
use crate::error::LaunchError;
|
|
|
|
/// Extracted kernel artifacts ready for QEMU consumption.
|
|
pub struct ExtractedKernel {
|
|
/// Path to the extracted kernel image (bzImage or equivalent).
|
|
pub kernel_path: PathBuf,
|
|
/// Path to the extracted initramfs, if present in the KERNEL_SEG.
|
|
pub initramfs_path: Option<PathBuf>,
|
|
/// The parsed KernelHeader.
|
|
pub header: KernelHeader,
|
|
/// The kernel command line string.
|
|
pub cmdline: String,
|
|
/// Temp directory holding the extracted files (kept alive via ownership).
|
|
_tempdir: tempfile::TempDir,
|
|
}
|
|
|
|
impl std::fmt::Debug for ExtractedKernel {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("ExtractedKernel")
|
|
.field("kernel_path", &self.kernel_path)
|
|
.field("initramfs_path", &self.initramfs_path)
|
|
.field("cmdline", &self.cmdline)
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
/// Extract kernel and initramfs from an RVF file to temporary files.
|
|
///
|
|
/// The returned `ExtractedKernel` owns a `TempDir`; the files are cleaned
|
|
/// up when it is dropped.
|
|
pub fn extract_kernel(rvf_path: &Path) -> Result<ExtractedKernel, LaunchError> {
|
|
let store = RvfStore::open_readonly(rvf_path)
|
|
.map_err(|e| LaunchError::KernelExtraction(format!("failed to open store: {e:?}")))?;
|
|
|
|
let (header_bytes, remainder) = store
|
|
.extract_kernel()
|
|
.map_err(|e| LaunchError::KernelExtraction(format!("segment read error: {e:?}")))?
|
|
.ok_or_else(|| LaunchError::NoKernelSegment {
|
|
path: rvf_path.to_path_buf(),
|
|
})?;
|
|
|
|
if header_bytes.len() < 128 {
|
|
return Err(LaunchError::KernelExtraction(
|
|
"KernelHeader too short".into(),
|
|
));
|
|
}
|
|
|
|
let mut hdr_array = [0u8; 128];
|
|
hdr_array.copy_from_slice(&header_bytes[..128]);
|
|
let header = KernelHeader::from_bytes(&hdr_array)
|
|
.map_err(|e| LaunchError::KernelExtraction(format!("bad KernelHeader: {e:?}")))?;
|
|
|
|
// The wire format after the 128-byte KernelHeader (which is already
|
|
// split off into `header_bytes`) is:
|
|
//
|
|
// For simple embed_kernel (no binding):
|
|
// kernel_image || cmdline
|
|
//
|
|
// For embed_kernel_with_binding:
|
|
// KernelBinding(128) || cmdline || kernel_image
|
|
//
|
|
// We determine the layout from header.image_size which tells us the
|
|
// kernel image length. The cmdline is header.cmdline_length bytes.
|
|
|
|
let image_size = header.image_size as usize;
|
|
let cmdline_length = header.cmdline_length as usize;
|
|
|
|
// Simple format: image comes first in the remainder, then cmdline
|
|
let (kernel_image, cmdline) = if image_size > 0 && image_size <= remainder.len() {
|
|
let img = &remainder[..image_size];
|
|
let cmd = if cmdline_length > 0 && image_size + cmdline_length <= remainder.len() {
|
|
String::from_utf8_lossy(&remainder[image_size..image_size + cmdline_length])
|
|
.into_owned()
|
|
} else {
|
|
String::new()
|
|
};
|
|
(img, cmd)
|
|
} else {
|
|
// Fallback: treat entire remainder as kernel image
|
|
(&remainder[..], String::new())
|
|
};
|
|
|
|
// Write to temp files
|
|
let tempdir = tempfile::tempdir().map_err(LaunchError::TempFile)?;
|
|
|
|
let kernel_file_path = tempdir.path().join("vmlinuz");
|
|
{
|
|
let mut f = std::fs::File::create(&kernel_file_path).map_err(LaunchError::TempFile)?;
|
|
f.write_all(kernel_image).map_err(LaunchError::TempFile)?;
|
|
f.sync_all().map_err(LaunchError::TempFile)?;
|
|
}
|
|
|
|
// For now we do not split out a separate initramfs from the kernel
|
|
// image. A future version could detect an appended initramfs using
|
|
// the standard Linux trailer magic (0x6d65736800000000).
|
|
let initramfs_path = None;
|
|
|
|
Ok(ExtractedKernel {
|
|
kernel_path: kernel_file_path,
|
|
initramfs_path,
|
|
header,
|
|
cmdline,
|
|
_tempdir: tempdir,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use rvf_runtime::options::RvfOptions;
|
|
use rvf_types::kernel::KernelArch;
|
|
|
|
#[test]
|
|
fn extract_from_store_with_kernel() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let rvf_path = dir.path().join("test.rvf");
|
|
|
|
let opts = RvfOptions {
|
|
dimension: 4,
|
|
..Default::default()
|
|
};
|
|
let mut store = RvfStore::create(&rvf_path, opts).unwrap();
|
|
|
|
let image = b"MZ\x00fake-kernel-image-for-testing";
|
|
store
|
|
.embed_kernel(
|
|
KernelArch::X86_64 as u8,
|
|
0x01,
|
|
0,
|
|
image,
|
|
8080,
|
|
Some("console=ttyS0"),
|
|
)
|
|
.unwrap();
|
|
store.close().unwrap();
|
|
|
|
let extracted = extract_kernel(&rvf_path).unwrap();
|
|
assert!(extracted.kernel_path.exists());
|
|
|
|
let on_disk = std::fs::read(&extracted.kernel_path).unwrap();
|
|
assert_eq!(on_disk, image);
|
|
assert_eq!(extracted.header.api_port, 8080);
|
|
assert_eq!(extracted.cmdline, "console=ttyS0");
|
|
}
|
|
|
|
#[test]
|
|
fn extract_kernel_no_cmdline() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let rvf_path = dir.path().join("no_cmd.rvf");
|
|
|
|
let opts = RvfOptions {
|
|
dimension: 4,
|
|
..Default::default()
|
|
};
|
|
let mut store = RvfStore::create(&rvf_path, opts).unwrap();
|
|
|
|
let image = b"fake-kernel";
|
|
store
|
|
.embed_kernel(KernelArch::X86_64 as u8, 0x01, 0, image, 9090, None)
|
|
.unwrap();
|
|
store.close().unwrap();
|
|
|
|
let extracted = extract_kernel(&rvf_path).unwrap();
|
|
let on_disk = std::fs::read(&extracted.kernel_path).unwrap();
|
|
assert_eq!(on_disk, image);
|
|
assert!(extracted.cmdline.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn extract_returns_error_when_no_kernel() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let rvf_path = dir.path().join("no_kernel.rvf");
|
|
|
|
let opts = RvfOptions {
|
|
dimension: 4,
|
|
..Default::default()
|
|
};
|
|
let store = RvfStore::create(&rvf_path, opts).unwrap();
|
|
store.close().unwrap();
|
|
|
|
let result = extract_kernel(&rvf_path);
|
|
assert!(result.is_err());
|
|
match result.unwrap_err() {
|
|
LaunchError::NoKernelSegment { .. } => {}
|
|
other => panic!("expected NoKernelSegment, got: {other}"),
|
|
}
|
|
}
|
|
}
|