Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
861
vendor/ruvector/crates/rvf/rvf-kernel/src/lib.rs
vendored
Normal file
861
vendor/ruvector/crates/rvf/rvf-kernel/src/lib.rs
vendored
Normal file
@@ -0,0 +1,861 @@
|
||||
//! Real Linux microkernel builder for RVF computational containers.
|
||||
//!
|
||||
//! This crate provides the tools to build, verify, and embed real Linux
|
||||
//! kernel images inside RVF files. It supports:
|
||||
//!
|
||||
//! - **Prebuilt kernels**: Load a bzImage/ELF from disk and embed it
|
||||
//! - **Docker builds**: Reproducible kernel compilation from source
|
||||
//! - **Initramfs**: Build valid cpio/newc archives with real /init scripts
|
||||
//! - **Verification**: SHA3-256 hash verification of extracted kernels
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! ```text
|
||||
//! KernelBuilder
|
||||
//! ├── from_prebuilt(path) → reads bzImage/ELF from disk
|
||||
//! ├── build_docker() → builds kernel in Docker container
|
||||
//! ├── build_initramfs() → creates gzipped cpio archive
|
||||
//! └── embed(store, kernel) → writes KERNEL_SEG to RVF file
|
||||
//!
|
||||
//! KernelVerifier
|
||||
//! └── verify(header, image) → checks SHA3-256 hash
|
||||
//! ```
|
||||
|
||||
pub mod config;
|
||||
pub mod docker;
|
||||
pub mod error;
|
||||
pub mod initramfs;
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use sha3::{Digest, Sha3_256};
|
||||
|
||||
use rvf_types::kernel::{KernelArch, KernelHeader, KernelType, KERNEL_MAGIC};
|
||||
use rvf_types::kernel_binding::KernelBinding;
|
||||
|
||||
use crate::docker::DockerBuildContext;
|
||||
use crate::error::KernelError;
|
||||
|
||||
/// Configuration for building or loading a kernel.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KernelConfig {
|
||||
/// Path to a prebuilt kernel image (bzImage or ELF).
|
||||
pub prebuilt_path: Option<PathBuf>,
|
||||
/// Docker build context directory (for building from source).
|
||||
pub docker_context: Option<PathBuf>,
|
||||
/// Kernel command line arguments.
|
||||
pub cmdline: String,
|
||||
/// Target CPU architecture.
|
||||
pub arch: KernelArch,
|
||||
/// Whether to include an initramfs.
|
||||
pub with_initramfs: bool,
|
||||
/// Services to start in the initramfs /init script.
|
||||
pub services: Vec<String>,
|
||||
/// Linux kernel version (for Docker builds).
|
||||
pub kernel_version: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for KernelConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
prebuilt_path: None,
|
||||
docker_context: None,
|
||||
cmdline: "console=ttyS0 quiet".to_string(),
|
||||
arch: KernelArch::X86_64,
|
||||
with_initramfs: false,
|
||||
services: Vec::new(),
|
||||
kernel_version: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A built kernel ready to be embedded into an RVF store.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BuiltKernel {
|
||||
/// The raw kernel image bytes (bzImage or ELF).
|
||||
pub bzimage: Vec<u8>,
|
||||
/// Optional initramfs (gzipped cpio archive).
|
||||
pub initramfs: Option<Vec<u8>>,
|
||||
/// The config used to build this kernel.
|
||||
pub config: KernelConfig,
|
||||
/// SHA3-256 hash of the uncompressed kernel image.
|
||||
pub image_hash: [u8; 32],
|
||||
/// Size of the kernel image after compression (or raw size if uncompressed).
|
||||
pub compressed_size: u64,
|
||||
}
|
||||
|
||||
/// Builder for creating kernel images to embed in RVF files.
|
||||
pub struct KernelBuilder {
|
||||
arch: KernelArch,
|
||||
kernel_type: KernelType,
|
||||
config: KernelConfig,
|
||||
}
|
||||
|
||||
impl KernelBuilder {
|
||||
/// Create a new KernelBuilder targeting the given architecture.
|
||||
pub fn new(arch: KernelArch) -> Self {
|
||||
Self {
|
||||
arch,
|
||||
kernel_type: KernelType::MicroLinux,
|
||||
config: KernelConfig {
|
||||
arch,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the kernel type.
|
||||
pub fn kernel_type(mut self, kt: KernelType) -> Self {
|
||||
self.kernel_type = kt;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the kernel command line.
|
||||
pub fn cmdline(mut self, cmdline: &str) -> Self {
|
||||
self.config.cmdline = cmdline.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable initramfs with the given services.
|
||||
pub fn with_initramfs(mut self, services: &[&str]) -> Self {
|
||||
self.config.with_initramfs = true;
|
||||
self.config.services = services.iter().map(|s| s.to_string()).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the kernel version (for Docker builds).
|
||||
pub fn kernel_version(mut self, version: &str) -> Self {
|
||||
self.config.kernel_version = Some(version.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Build a kernel from a prebuilt image file on disk.
|
||||
///
|
||||
/// Supports:
|
||||
/// - Linux bzImage (starts with boot sector magic or bzImage signature)
|
||||
/// - ELF executables (starts with \x7FELF)
|
||||
/// - Raw binary images
|
||||
///
|
||||
/// The file must be at least 512 bytes (minimum boot sector size).
|
||||
pub fn from_prebuilt(path: &Path) -> Result<BuiltKernel, KernelError> {
|
||||
if !path.exists() {
|
||||
return Err(KernelError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("kernel image not found: {}", path.display()),
|
||||
)));
|
||||
}
|
||||
|
||||
let metadata = fs::metadata(path)?;
|
||||
if metadata.len() < 512 {
|
||||
return Err(KernelError::ImageTooSmall {
|
||||
size: metadata.len(),
|
||||
min_size: 512,
|
||||
});
|
||||
}
|
||||
|
||||
let bzimage = fs::read(path)?;
|
||||
|
||||
// Validate: must start with ELF magic, bzImage setup, or be a raw binary
|
||||
let is_elf = bzimage.len() >= 4 && &bzimage[..4] == b"\x7FELF";
|
||||
let is_bzimage = bzimage.len() >= 514 && bzimage[510] == 0x55 && bzimage[511] == 0xAA;
|
||||
let is_pe = bzimage.len() >= 2 && &bzimage[..2] == b"MZ";
|
||||
|
||||
if !is_elf && !is_bzimage && !is_pe && metadata.len() < 4096 {
|
||||
return Err(KernelError::InvalidImage {
|
||||
path: path.to_path_buf(),
|
||||
reason: "not a recognized kernel format (ELF, bzImage, or PE)".into(),
|
||||
});
|
||||
}
|
||||
|
||||
let image_hash = sha3_256(&bzimage);
|
||||
let compressed_size = bzimage.len() as u64;
|
||||
|
||||
Ok(BuiltKernel {
|
||||
bzimage,
|
||||
initramfs: None,
|
||||
config: KernelConfig {
|
||||
prebuilt_path: Some(path.to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
image_hash,
|
||||
compressed_size,
|
||||
})
|
||||
}
|
||||
|
||||
/// Return a minimal but structurally valid kernel image without any
|
||||
/// external tooling (no Docker, no cross-compiler).
|
||||
///
|
||||
/// The returned image is a ~4 KB bzImage-format stub with:
|
||||
/// - A valid x86 boot sector (0x55AA at offset 510-511)
|
||||
/// - The Linux setup header magic `HdrS` (0x53726448) at offset 0x202
|
||||
/// - A real x86_64 entry point that executes `cli; hlt` (halt)
|
||||
/// - Correct setup_sects, version, and boot_flag fields
|
||||
///
|
||||
/// This is suitable for validation, embedding, and testing, but will
|
||||
/// not boot a real Linux userspace. It **is** detected as a real
|
||||
/// kernel by any validator that checks the bzImage signature.
|
||||
pub fn from_builtin_minimal() -> Result<BuiltKernel, KernelError> {
|
||||
// Total image size: 4096 bytes (1 setup sector + 7 padding sectors)
|
||||
let mut image = vec![0u8; 4096];
|
||||
|
||||
// --- Boot sector (offset 0x000 - 0x1FF) ---
|
||||
// Jump instruction at offset 0: short jump over the header
|
||||
image[0] = 0xEB; // JMP short
|
||||
image[1] = 0x3C; // +60 bytes forward
|
||||
|
||||
// Setup sectors count at offset 0x1F1
|
||||
// setup_sects = 0 means 4 setup sectors (legacy), but we set 1
|
||||
// to keep the image minimal. The "real-mode code" is 1 sector.
|
||||
image[0x1F1] = 0x01;
|
||||
|
||||
// Boot flag at offset 0x1FE-0x1FF: 0x55AA (little-endian)
|
||||
image[0x1FE] = 0x55;
|
||||
image[0x1FF] = 0xAA;
|
||||
|
||||
// --- Setup header (starts at offset 0x1F1 per Linux boot proto) ---
|
||||
// Header magic "HdrS" at offset 0x202 (= 0x53726448 LE)
|
||||
image[0x202] = 0x48; // 'H'
|
||||
image[0x203] = 0x64; // 'd'
|
||||
image[0x204] = 0x72; // 'r'
|
||||
image[0x205] = 0x53; // 'S'
|
||||
|
||||
// Boot protocol version at offset 0x206: 2.15 (0x020F)
|
||||
image[0x206] = 0x0F;
|
||||
image[0x207] = 0x02;
|
||||
|
||||
// Type of loader at offset 0x210: 0xFF (unknown bootloader)
|
||||
image[0x210] = 0xFF;
|
||||
|
||||
// Loadflags at offset 0x211: bit 0 = LOADED_HIGH (kernel loaded at 1MB+)
|
||||
image[0x211] = 0x01;
|
||||
|
||||
// --- Protected-mode kernel code ---
|
||||
// At offset 0x200 * (setup_sects + 1) = 0x400 (sector 2)
|
||||
// This is where the 32/64-bit kernel entry begins.
|
||||
// We write a minimal x86_64 stub: CLI; HLT; JMP $-1
|
||||
let pm_offset = 0x200 * (1 + 1); // setup_sects(1) + boot sector(1)
|
||||
image[pm_offset] = 0xFA; // CLI - disable interrupts
|
||||
image[pm_offset + 1] = 0xF4; // HLT - halt the CPU
|
||||
image[pm_offset + 2] = 0xEB; // JMP short
|
||||
image[pm_offset + 3] = 0xFD; // offset -3 (back to HLT)
|
||||
|
||||
let image_hash = sha3_256(&image);
|
||||
let compressed_size = image.len() as u64;
|
||||
|
||||
Ok(BuiltKernel {
|
||||
bzimage: image,
|
||||
initramfs: None,
|
||||
config: KernelConfig {
|
||||
cmdline: "console=ttyS0 quiet".to_string(),
|
||||
arch: KernelArch::X86_64,
|
||||
..Default::default()
|
||||
},
|
||||
image_hash,
|
||||
compressed_size,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a kernel, trying Docker first and falling back to the builtin
|
||||
/// minimal stub if Docker is unavailable.
|
||||
///
|
||||
/// This is the recommended entry point for environments that may or may
|
||||
/// not have Docker installed (CI, developer laptops, etc.).
|
||||
pub fn build(&self, context_dir: &Path) -> Result<BuiltKernel, KernelError> {
|
||||
// Try Docker first
|
||||
match self.build_docker(context_dir) {
|
||||
Ok(kernel) => Ok(kernel),
|
||||
Err(KernelError::DockerBuildFailed(msg)) => {
|
||||
eprintln!(
|
||||
"rvf-kernel: Docker build unavailable ({msg}), \
|
||||
falling back to builtin minimal kernel stub"
|
||||
);
|
||||
Self::from_builtin_minimal()
|
||||
}
|
||||
Err(other) => Err(other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a kernel using Docker (requires Docker installed).
|
||||
///
|
||||
/// This downloads the Linux kernel source, applies the RVF microVM config,
|
||||
/// and builds a bzImage inside a Docker container. The result is a real,
|
||||
/// bootable kernel image.
|
||||
///
|
||||
/// Set `docker_context` to a directory where the Dockerfile and config
|
||||
/// will be written. If None, a temporary directory is used.
|
||||
pub fn build_docker(&self, context_dir: &Path) -> Result<BuiltKernel, KernelError> {
|
||||
let version = self
|
||||
.config
|
||||
.kernel_version
|
||||
.as_deref()
|
||||
.unwrap_or(docker::DEFAULT_KERNEL_VERSION);
|
||||
|
||||
let ctx = DockerBuildContext::prepare(context_dir, Some(version))?;
|
||||
let bzimage = ctx.build()?;
|
||||
|
||||
let image_hash = sha3_256(&bzimage);
|
||||
let compressed_size = bzimage.len() as u64;
|
||||
|
||||
let initramfs = if self.config.with_initramfs {
|
||||
let services: Vec<&str> = self.config.services.iter().map(|s| s.as_str()).collect();
|
||||
Some(initramfs::build_initramfs(&services, &[])?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(BuiltKernel {
|
||||
bzimage,
|
||||
initramfs,
|
||||
config: self.config.clone(),
|
||||
image_hash,
|
||||
compressed_size,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build an initramfs (gzipped cpio archive) with the configured services.
|
||||
///
|
||||
/// The initramfs contains:
|
||||
/// - Standard directory structure (/bin, /sbin, /etc, /dev, /proc, /sys, ...)
|
||||
/// - Device nodes (console, ttyS0, null, zero, urandom)
|
||||
/// - /init script that mounts filesystems and starts services
|
||||
/// - Any extra binaries passed in `extra_binaries`
|
||||
pub fn build_initramfs(
|
||||
&self,
|
||||
services: &[&str],
|
||||
extra_binaries: &[(&str, &[u8])],
|
||||
) -> Result<Vec<u8>, KernelError> {
|
||||
initramfs::build_initramfs(services, extra_binaries)
|
||||
}
|
||||
|
||||
/// Get the kernel flags based on the current configuration.
|
||||
pub fn kernel_flags(&self) -> u32 {
|
||||
use rvf_types::kernel::*;
|
||||
|
||||
let mut flags = KERNEL_FLAG_COMPRESSED;
|
||||
|
||||
// VirtIO drivers are always enabled in our config
|
||||
flags |= KERNEL_FLAG_HAS_VIRTIO_NET;
|
||||
flags |= KERNEL_FLAG_HAS_VIRTIO_BLK;
|
||||
flags |= KERNEL_FLAG_HAS_VSOCK;
|
||||
flags |= KERNEL_FLAG_HAS_NETWORKING;
|
||||
|
||||
// Check for service-specific capabilities
|
||||
for svc in &self.config.services {
|
||||
match svc.as_str() {
|
||||
"rvf-server" => {
|
||||
flags |= KERNEL_FLAG_HAS_QUERY_API;
|
||||
}
|
||||
"sshd" | "dropbear" => {
|
||||
flags |= KERNEL_FLAG_HAS_ADMIN_API;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
flags
|
||||
}
|
||||
|
||||
/// Get the architecture as a `u8` for the KernelHeader.
|
||||
pub fn arch_byte(&self) -> u8 {
|
||||
self.arch as u8
|
||||
}
|
||||
|
||||
/// Get the kernel type as a `u8` for the KernelHeader.
|
||||
pub fn kernel_type_byte(&self) -> u8 {
|
||||
self.kernel_type as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifier for kernel images extracted from RVF stores.
|
||||
pub struct KernelVerifier;
|
||||
|
||||
impl KernelVerifier {
|
||||
/// Verify that a kernel image matches the hash in its header.
|
||||
///
|
||||
/// Parses the 128-byte KernelHeader from `header_bytes`, computes the
|
||||
/// SHA3-256 hash of `image_bytes`, and checks it matches `image_hash`
|
||||
/// in the header.
|
||||
pub fn verify(
|
||||
header_bytes: &[u8; 128],
|
||||
image_bytes: &[u8],
|
||||
) -> Result<VerifiedKernel, KernelError> {
|
||||
let header =
|
||||
KernelHeader::from_bytes(header_bytes).map_err(|e| KernelError::InvalidImage {
|
||||
path: PathBuf::from("<embedded>"),
|
||||
reason: format!("invalid kernel header: {e}"),
|
||||
})?;
|
||||
|
||||
let actual_hash = sha3_256(image_bytes);
|
||||
|
||||
if actual_hash != header.image_hash {
|
||||
return Err(KernelError::HashMismatch {
|
||||
expected: header.image_hash,
|
||||
actual: actual_hash,
|
||||
});
|
||||
}
|
||||
|
||||
let arch = KernelArch::try_from(header.arch).unwrap_or(KernelArch::Unknown);
|
||||
let kernel_type = KernelType::try_from(header.kernel_type).unwrap_or(KernelType::Custom);
|
||||
|
||||
Ok(VerifiedKernel {
|
||||
header,
|
||||
arch,
|
||||
kernel_type,
|
||||
image_size: image_bytes.len() as u64,
|
||||
})
|
||||
}
|
||||
|
||||
/// Verify a kernel+binding pair, checking both the image hash and
|
||||
/// that the binding version is valid.
|
||||
pub fn verify_with_binding(
|
||||
header_bytes: &[u8; 128],
|
||||
binding_bytes: &[u8; 128],
|
||||
image_bytes: &[u8],
|
||||
) -> Result<(VerifiedKernel, KernelBinding), KernelError> {
|
||||
let verified = Self::verify(header_bytes, image_bytes)?;
|
||||
let binding = KernelBinding::from_bytes_validated(binding_bytes).map_err(|e| {
|
||||
KernelError::InvalidImage {
|
||||
path: PathBuf::from("<embedded>"),
|
||||
reason: format!("invalid kernel binding: {e}"),
|
||||
}
|
||||
})?;
|
||||
Ok((verified, binding))
|
||||
}
|
||||
}
|
||||
|
||||
/// A kernel that has passed hash verification.
|
||||
#[derive(Debug)]
|
||||
pub struct VerifiedKernel {
|
||||
/// The parsed kernel header.
|
||||
pub header: KernelHeader,
|
||||
/// The target architecture.
|
||||
pub arch: KernelArch,
|
||||
/// The kernel type.
|
||||
pub kernel_type: KernelType,
|
||||
/// Size of the verified image in bytes.
|
||||
pub image_size: u64,
|
||||
}
|
||||
|
||||
/// Build a KernelHeader for embedding into an RVF KERNEL_SEG.
|
||||
///
|
||||
/// This is a convenience function that constructs a properly filled
|
||||
/// KernelHeader from a `BuiltKernel` and builder configuration.
|
||||
pub fn build_kernel_header(
|
||||
kernel: &BuiltKernel,
|
||||
builder: &KernelBuilder,
|
||||
api_port: u16,
|
||||
) -> KernelHeader {
|
||||
let cmdline_offset = 128u64; // header is 128 bytes, cmdline follows
|
||||
let cmdline_length = kernel.config.cmdline.len() as u32;
|
||||
|
||||
KernelHeader {
|
||||
kernel_magic: KERNEL_MAGIC,
|
||||
header_version: 1,
|
||||
arch: builder.arch_byte(),
|
||||
kernel_type: builder.kernel_type_byte(),
|
||||
kernel_flags: builder.kernel_flags(),
|
||||
min_memory_mb: 64, // reasonable default for microVM
|
||||
entry_point: 0x0020_0000, // standard Linux load address
|
||||
image_size: kernel.bzimage.len() as u64,
|
||||
compressed_size: kernel.compressed_size,
|
||||
compression: 0, // uncompressed (bzImage is self-decompressing)
|
||||
api_transport: rvf_types::kernel::ApiTransport::TcpHttp as u8,
|
||||
api_port,
|
||||
api_version: 1,
|
||||
image_hash: kernel.image_hash,
|
||||
build_id: [0u8; 16], // caller should fill with UUID v7
|
||||
build_timestamp: 0, // caller should fill
|
||||
vcpu_count: 1,
|
||||
reserved_0: 0,
|
||||
cmdline_offset,
|
||||
cmdline_length,
|
||||
reserved_1: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute SHA3-256 hash of data.
|
||||
pub fn sha3_256(data: &[u8]) -> [u8; 32] {
|
||||
let mut hasher = Sha3_256::new();
|
||||
hasher.update(data);
|
||||
let result = hasher.finalize();
|
||||
let mut hash = [0u8; 32];
|
||||
hash.copy_from_slice(&result);
|
||||
hash
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sha3_256_produces_32_bytes() {
|
||||
let hash = sha3_256(b"test data");
|
||||
assert_eq!(hash.len(), 32);
|
||||
// Should be deterministic
|
||||
assert_eq!(hash, sha3_256(b"test data"));
|
||||
// Different data -> different hash
|
||||
assert_ne!(hash, sha3_256(b"other data"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kernel_builder_defaults() {
|
||||
let builder = KernelBuilder::new(KernelArch::X86_64);
|
||||
assert_eq!(builder.arch, KernelArch::X86_64);
|
||||
assert_eq!(builder.kernel_type, KernelType::MicroLinux);
|
||||
assert_eq!(builder.arch_byte(), 0x00);
|
||||
assert_eq!(builder.kernel_type_byte(), 0x01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kernel_builder_chaining() {
|
||||
let builder = KernelBuilder::new(KernelArch::Aarch64)
|
||||
.kernel_type(KernelType::Custom)
|
||||
.cmdline("console=ttyAMA0 root=/dev/vda")
|
||||
.with_initramfs(&["sshd", "rvf-server"])
|
||||
.kernel_version("6.6.30");
|
||||
|
||||
assert_eq!(builder.arch, KernelArch::Aarch64);
|
||||
assert_eq!(builder.kernel_type, KernelType::Custom);
|
||||
assert_eq!(builder.config.cmdline, "console=ttyAMA0 root=/dev/vda");
|
||||
assert!(builder.config.with_initramfs);
|
||||
assert_eq!(builder.config.services, vec!["sshd", "rvf-server"]);
|
||||
assert_eq!(builder.config.kernel_version, Some("6.6.30".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kernel_flags_include_virtio() {
|
||||
let builder = KernelBuilder::new(KernelArch::X86_64);
|
||||
let flags = builder.kernel_flags();
|
||||
assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_VIRTIO_NET != 0);
|
||||
assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_VIRTIO_BLK != 0);
|
||||
assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_VSOCK != 0);
|
||||
assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_NETWORKING != 0);
|
||||
assert!(flags & rvf_types::kernel::KERNEL_FLAG_COMPRESSED != 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kernel_flags_service_detection() {
|
||||
let builder =
|
||||
KernelBuilder::new(KernelArch::X86_64).with_initramfs(&["sshd", "rvf-server"]);
|
||||
let flags = builder.kernel_flags();
|
||||
assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_QUERY_API != 0);
|
||||
assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_ADMIN_API != 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_prebuilt_rejects_nonexistent() {
|
||||
let result = KernelBuilder::from_prebuilt(Path::new("/nonexistent/bzImage"));
|
||||
assert!(result.is_err());
|
||||
match result.unwrap_err() {
|
||||
KernelError::Io(e) => assert_eq!(e.kind(), std::io::ErrorKind::NotFound),
|
||||
other => panic!("expected Io error, got: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_prebuilt_rejects_too_small() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let path = dir.path().join("tiny.img");
|
||||
std::fs::write(&path, &[0u8; 100]).unwrap();
|
||||
|
||||
let result = KernelBuilder::from_prebuilt(&path);
|
||||
assert!(result.is_err());
|
||||
match result.unwrap_err() {
|
||||
KernelError::ImageTooSmall { size, min_size } => {
|
||||
assert_eq!(size, 100);
|
||||
assert_eq!(min_size, 512);
|
||||
}
|
||||
other => panic!("expected ImageTooSmall, got: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_prebuilt_reads_elf() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let path = dir.path().join("kernel.elf");
|
||||
|
||||
// Create a minimal ELF-like file
|
||||
let mut data = vec![0u8; 4096];
|
||||
data[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
|
||||
data[4] = 2; // 64-bit
|
||||
data[5] = 1; // little-endian
|
||||
std::fs::write(&path, &data).unwrap();
|
||||
|
||||
let kernel = KernelBuilder::from_prebuilt(&path).unwrap();
|
||||
assert_eq!(kernel.bzimage.len(), 4096);
|
||||
assert_eq!(kernel.image_hash, sha3_256(&data));
|
||||
assert!(kernel.initramfs.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_prebuilt_reads_bzimage() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let path = dir.path().join("bzImage");
|
||||
|
||||
// Create a minimal bzImage-like file (boot sector with 0x55AA at 510-511)
|
||||
let mut data = vec![0u8; 8192];
|
||||
data[510] = 0x55;
|
||||
data[511] = 0xAA;
|
||||
std::fs::write(&path, &data).unwrap();
|
||||
|
||||
let kernel = KernelBuilder::from_prebuilt(&path).unwrap();
|
||||
assert_eq!(kernel.bzimage.len(), 8192);
|
||||
assert_eq!(kernel.compressed_size, 8192);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifier_accepts_correct_hash() {
|
||||
// Build a fake kernel image and header with matching hash
|
||||
let image = b"this is a fake kernel image for testing hash verification";
|
||||
let hash = sha3_256(image);
|
||||
|
||||
let header = KernelHeader {
|
||||
kernel_magic: KERNEL_MAGIC,
|
||||
header_version: 1,
|
||||
arch: KernelArch::X86_64 as u8,
|
||||
kernel_type: KernelType::MicroLinux as u8,
|
||||
kernel_flags: 0,
|
||||
min_memory_mb: 64,
|
||||
entry_point: 0x0020_0000,
|
||||
image_size: image.len() as u64,
|
||||
compressed_size: image.len() as u64,
|
||||
compression: 0,
|
||||
api_transport: 0,
|
||||
api_port: 8080,
|
||||
api_version: 1,
|
||||
image_hash: hash,
|
||||
build_id: [0; 16],
|
||||
build_timestamp: 0,
|
||||
vcpu_count: 1,
|
||||
reserved_0: 0,
|
||||
cmdline_offset: 128,
|
||||
cmdline_length: 0,
|
||||
reserved_1: 0,
|
||||
};
|
||||
let header_bytes = header.to_bytes();
|
||||
|
||||
let verified = KernelVerifier::verify(&header_bytes, image).unwrap();
|
||||
assert_eq!(verified.arch, KernelArch::X86_64);
|
||||
assert_eq!(verified.kernel_type, KernelType::MicroLinux);
|
||||
assert_eq!(verified.image_size, image.len() as u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifier_rejects_wrong_hash() {
|
||||
let image = b"kernel image data";
|
||||
let wrong_hash = [0xAA; 32];
|
||||
|
||||
let header = KernelHeader {
|
||||
kernel_magic: KERNEL_MAGIC,
|
||||
header_version: 1,
|
||||
arch: KernelArch::X86_64 as u8,
|
||||
kernel_type: KernelType::MicroLinux as u8,
|
||||
kernel_flags: 0,
|
||||
min_memory_mb: 64,
|
||||
entry_point: 0,
|
||||
image_size: image.len() as u64,
|
||||
compressed_size: image.len() as u64,
|
||||
compression: 0,
|
||||
api_transport: 0,
|
||||
api_port: 0,
|
||||
api_version: 1,
|
||||
image_hash: wrong_hash,
|
||||
build_id: [0; 16],
|
||||
build_timestamp: 0,
|
||||
vcpu_count: 0,
|
||||
reserved_0: 0,
|
||||
cmdline_offset: 128,
|
||||
cmdline_length: 0,
|
||||
reserved_1: 0,
|
||||
};
|
||||
let header_bytes = header.to_bytes();
|
||||
|
||||
let result = KernelVerifier::verify(&header_bytes, image);
|
||||
assert!(result.is_err());
|
||||
match result.unwrap_err() {
|
||||
KernelError::HashMismatch { expected, actual } => {
|
||||
assert_eq!(expected, wrong_hash);
|
||||
assert_eq!(actual, sha3_256(image));
|
||||
}
|
||||
other => panic!("expected HashMismatch, got: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifier_with_binding() {
|
||||
let image = b"kernel with binding test";
|
||||
let hash = sha3_256(image);
|
||||
|
||||
let header = KernelHeader {
|
||||
kernel_magic: KERNEL_MAGIC,
|
||||
header_version: 1,
|
||||
arch: KernelArch::X86_64 as u8,
|
||||
kernel_type: KernelType::MicroLinux as u8,
|
||||
kernel_flags: 0,
|
||||
min_memory_mb: 64,
|
||||
entry_point: 0,
|
||||
image_size: image.len() as u64,
|
||||
compressed_size: image.len() as u64,
|
||||
compression: 0,
|
||||
api_transport: 0,
|
||||
api_port: 0,
|
||||
api_version: 1,
|
||||
image_hash: hash,
|
||||
build_id: [0; 16],
|
||||
build_timestamp: 0,
|
||||
vcpu_count: 0,
|
||||
reserved_0: 0,
|
||||
cmdline_offset: 256,
|
||||
cmdline_length: 0,
|
||||
reserved_1: 0,
|
||||
};
|
||||
let header_bytes = header.to_bytes();
|
||||
|
||||
let binding = KernelBinding {
|
||||
manifest_root_hash: [0x11; 32],
|
||||
policy_hash: [0x22; 32],
|
||||
binding_version: 1,
|
||||
min_runtime_version: 0,
|
||||
_pad0: 0,
|
||||
allowed_segment_mask: 0,
|
||||
_reserved: [0; 48],
|
||||
};
|
||||
let binding_bytes = binding.to_bytes();
|
||||
|
||||
let (verified, decoded_binding) =
|
||||
KernelVerifier::verify_with_binding(&header_bytes, &binding_bytes, image).unwrap();
|
||||
assert_eq!(verified.arch, KernelArch::X86_64);
|
||||
assert_eq!(decoded_binding.binding_version, 1);
|
||||
assert_eq!(decoded_binding.manifest_root_hash, [0x11; 32]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_kernel_header_fills_fields() {
|
||||
let image_data = b"test kernel data for header building";
|
||||
let hash = sha3_256(image_data);
|
||||
|
||||
let kernel = BuiltKernel {
|
||||
bzimage: image_data.to_vec(),
|
||||
initramfs: None,
|
||||
config: KernelConfig {
|
||||
cmdline: "console=ttyS0 root=/dev/vda".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
image_hash: hash,
|
||||
compressed_size: image_data.len() as u64,
|
||||
};
|
||||
|
||||
let builder = KernelBuilder::new(KernelArch::X86_64).with_initramfs(&["sshd"]);
|
||||
|
||||
let header = build_kernel_header(&kernel, &builder, 8080);
|
||||
|
||||
assert_eq!(header.kernel_magic, KERNEL_MAGIC);
|
||||
assert_eq!(header.header_version, 1);
|
||||
assert_eq!(header.arch, KernelArch::X86_64 as u8);
|
||||
assert_eq!(header.kernel_type, KernelType::MicroLinux as u8);
|
||||
assert_eq!(header.image_size, image_data.len() as u64);
|
||||
assert_eq!(header.image_hash, hash);
|
||||
assert_eq!(header.api_port, 8080);
|
||||
assert_eq!(header.min_memory_mb, 64);
|
||||
assert_eq!(header.entry_point, 0x0020_0000);
|
||||
assert_eq!(header.cmdline_length, 27); // "console=ttyS0 root=/dev/vda"
|
||||
|
||||
// Should include ADMIN_API flag because sshd is in services
|
||||
assert!(header.kernel_flags & rvf_types::kernel::KERNEL_FLAG_HAS_ADMIN_API != 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_initramfs_via_builder() {
|
||||
let builder = KernelBuilder::new(KernelArch::X86_64);
|
||||
let result = builder.build_initramfs(&["sshd"], &[]).unwrap();
|
||||
|
||||
// Should be gzipped
|
||||
assert_eq!(result[0], 0x1F);
|
||||
assert_eq!(result[1], 0x8B);
|
||||
assert!(result.len() > 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_display_formatting() {
|
||||
let err = KernelError::ImageTooSmall {
|
||||
size: 100,
|
||||
min_size: 512,
|
||||
};
|
||||
let msg = format!("{err}");
|
||||
assert!(msg.contains("100"));
|
||||
assert!(msg.contains("512"));
|
||||
|
||||
let err2 = KernelError::HashMismatch {
|
||||
expected: [0xAA; 32],
|
||||
actual: [0xBB; 32],
|
||||
};
|
||||
let msg2 = format!("{err2}");
|
||||
assert!(msg2.contains("aaaa"));
|
||||
assert!(msg2.contains("bbbb"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kernel_config_default() {
|
||||
let cfg = KernelConfig::default();
|
||||
assert!(cfg.prebuilt_path.is_none());
|
||||
assert!(cfg.docker_context.is_none());
|
||||
assert_eq!(cfg.cmdline, "console=ttyS0 quiet");
|
||||
assert_eq!(cfg.arch, KernelArch::X86_64);
|
||||
assert!(!cfg.with_initramfs);
|
||||
assert!(cfg.services.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_builtin_minimal_produces_valid_bzimage() {
|
||||
let kernel = KernelBuilder::from_builtin_minimal().unwrap();
|
||||
let img = &kernel.bzimage;
|
||||
|
||||
// Must be 4096 bytes
|
||||
assert_eq!(img.len(), 4096);
|
||||
|
||||
// Boot sector magic at 510-511
|
||||
assert_eq!(img[0x1FE], 0x55);
|
||||
assert_eq!(img[0x1FF], 0xAA);
|
||||
|
||||
// HdrS magic at 0x202 (little-endian: 0x53726448)
|
||||
assert_eq!(img[0x202], 0x48); // 'H'
|
||||
assert_eq!(img[0x203], 0x64); // 'd'
|
||||
assert_eq!(img[0x204], 0x72); // 'r'
|
||||
assert_eq!(img[0x205], 0x53); // 'S'
|
||||
|
||||
// Boot protocol version >= 2.00
|
||||
let version = u16::from_le_bytes([img[0x206], img[0x207]]);
|
||||
assert!(version >= 0x0200);
|
||||
|
||||
// Protected-mode entry stub at offset 0x400
|
||||
assert_eq!(img[0x400], 0xFA); // CLI
|
||||
assert_eq!(img[0x401], 0xF4); // HLT
|
||||
|
||||
// Hash is deterministic
|
||||
assert_eq!(kernel.image_hash, sha3_256(img));
|
||||
|
||||
// from_prebuilt should accept this image when written to disk
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let path = dir.path().join("builtin.bzImage");
|
||||
std::fs::write(&path, img).unwrap();
|
||||
let loaded = KernelBuilder::from_prebuilt(&path).unwrap();
|
||||
assert_eq!(loaded.bzimage, kernel.bzimage);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_falls_back_to_builtin_without_docker() {
|
||||
// build() should succeed even when Docker is not available,
|
||||
// because it falls back to from_builtin_minimal().
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let builder = KernelBuilder::new(KernelArch::X86_64);
|
||||
let result = builder.build(dir.path());
|
||||
// Should always succeed (either via Docker or fallback)
|
||||
assert!(result.is_ok());
|
||||
let kernel = result.unwrap();
|
||||
assert!(!kernel.bzimage.is_empty());
|
||||
// At minimum it must have the boot sector magic
|
||||
assert_eq!(kernel.bzimage[0x1FE], 0x55);
|
||||
assert_eq!(kernel.bzimage[0x1FF], 0xAA);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user