use alloc::format;
use ax_errno::{AxResult, ax_err, ax_err_type};
use axvmconfig::AxVMCrateConfig;
#[cfg(target_arch = "x86_64")]
use axvmconfig::{EmulatedDeviceType, VMBootProtocol, VmMemMappingType};
use byte_unit::Byte;
use axvm::{AxVMRef, GuestPhysAddr, VMMemoryRegion};
use crate::config::{get_vm_dtb_arc, vmcfg};
mod linux;
#[cfg(target_arch = "x86_64")]
mod x86;
#[cfg(target_arch = "x86_64")]
use x86::boot_params as x86_boot_params;
#[cfg(target_arch = "x86_64")]
use x86::linux as x86_linux;
#[cfg(target_arch = "x86_64")]
use x86::linux_boot as x86_linux_boot;
#[cfg(target_arch = "x86_64")]
use x86::mptable as x86_mptable;
#[cfg(target_arch = "x86_64")]
use x86::multiboot as x86_boot;
#[cfg(target_arch = "x86_64")]
pub fn is_x86_linux_image_config(config: &AxVMCrateConfig) -> bool {
if !should_direct_boot_x86_linux(config) {
return false;
}
match config.kernel.image_location.as_deref() {
Some("memory") => with_memory_image(config, detect_x86_linux_image).is_some(),
#[cfg(feature = "fs")]
Some("fs") => fs::kernel_read(config, x86_linux::HEADER_READ_SIZE)
.ok()
.and_then(|data| detect_x86_linux_image(&data))
.is_some(),
_ => false,
}
}
pub fn get_image_header(config: &AxVMCrateConfig) -> Option<linux::Header> {
match config.kernel.image_location.as_deref() {
Some("memory") => with_memory_image(config, linux::Header::parse).flatten(),
#[cfg(feature = "fs")]
Some("fs") => {
let read_size = linux::Header::hdr_size();
let data = fs::kernel_read(config, read_size).ok()?;
linux::Header::parse(&data)
}
_ => None,
}
}
fn with_memory_image<F, R>(config: &AxVMCrateConfig, func: F) -> Option<R>
where
F: FnOnce(&[u8]) -> R,
{
let vm_imags = vmcfg::get_memory_images()
.iter()
.find(|&v| v.id == config.base.id)?;
Some(func(vm_imags.kernel))
}
fn memory_images_for_vm(config: &AxVMCrateConfig) -> AxResult<&'static vmcfg::MemoryImage> {
vmcfg::get_memory_images()
.iter()
.find(|&v| v.id == config.base.id)
.ok_or_else(|| {
ax_err_type!(
NotFound,
"VM images are missing; pass VM configs with AXVISOR_VM_CONFIGS"
)
})
}
pub struct ImageLoader {
main_memory: VMMemoryRegion,
vm: AxVMRef,
config: AxVMCrateConfig,
kernel_load_gpa: GuestPhysAddr,
bios_load_gpa: Option<GuestPhysAddr>,
dtb_load_gpa: Option<GuestPhysAddr>,
ramdisk_load_gpa: Option<GuestPhysAddr>,
}
impl ImageLoader {
pub fn new(main_memory: VMMemoryRegion, config: AxVMCrateConfig, vm: AxVMRef) -> Self {
Self {
main_memory,
vm,
config,
kernel_load_gpa: GuestPhysAddr::default(),
bios_load_gpa: None,
dtb_load_gpa: None,
ramdisk_load_gpa: None,
}
}
pub fn load(&mut self) -> AxResult {
self.config.kernel.validate_boot_config()?;
info!(
"Loading VM[{}] images into memory region: gpa={:#x}, hva={:#x}, size={:#}",
self.vm.id(),
self.main_memory.gpa,
self.main_memory.hva,
Byte::from(self.main_memory.size())
);
self.vm.with_config(|config| {
self.kernel_load_gpa = config.image_config.kernel_load_gpa;
self.dtb_load_gpa = config.image_config.dtb_load_gpa;
self.bios_load_gpa = config.image_config.bios_load_gpa;
self.ramdisk_load_gpa = config.image_config.ramdisk.as_ref().map(|r| r.load_gpa);
});
match self.config.kernel.image_location.as_deref() {
Some("memory") => self.load_vm_images_from_memory(),
#[cfg(feature = "fs")]
Some("fs") => fs::load_vm_images_from_filesystem(self),
_ => ax_err!(
InvalidInput,
"Unsupported image_location; use \"memory\" or enable fs feature for \"fs\""
),
}
}
fn load_vm_images_from_memory(&mut self) -> AxResult {
info!("Loading VM[{}] images from memory", self.config.base.id);
let vm_imags = memory_images_for_vm(&self.config)?;
#[cfg(target_arch = "x86_64")]
if should_direct_boot_x86_linux(&self.config)
&& let Some(header) = detect_x86_linux_image(vm_imags.kernel)
{
return self.load_x86_linux_images_from_memory(
header,
vm_imags.kernel,
vm_imags.ramdisk,
);
}
load_vm_image_from_memory(vm_imags.kernel, self.kernel_load_gpa, self.vm.clone())?;
if let Some(buffer) = vm_imags.ramdisk {
self.load_ramdisk_from_memory(buffer)?;
}
let vm_config = crate::config::build_axvm_config(&self.config);
if let Some(dtb_arc) = get_vm_dtb_arc(&vm_config) {
let _dtb_slice: &[u8] = &dtb_arc;
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
{
if let Some(dtb_src) = core::ptr::NonNull::new(_dtb_slice.as_ptr() as *mut u8) {
crate::fdt::update_fdt(
dtb_src,
_dtb_slice.len(),
self.vm.clone(),
&self.config,
)?;
} else {
return ax_err!(InvalidData, "Guest DTB pointer is null");
}
}
#[cfg(target_arch = "loongarch64")]
{
let dtb_load_gpa = self
.dtb_load_gpa
.ok_or_else(|| ax_err_type!(NotFound, "DTB load address is missing"))?;
load_vm_image_from_memory(_dtb_slice, dtb_load_gpa, self.vm.clone())?;
}
} else {
#[cfg(any(target_arch = "loongarch64", target_arch = "riscv64"))]
if let Some(buffer) = vm_imags.dtb {
let dtb_load_gpa = self
.dtb_load_gpa
.ok_or_else(|| ax_err_type!(NotFound, "DTB load address is missing"))?;
load_vm_image_from_memory(buffer, dtb_load_gpa, self.vm.clone())?;
} else {
info!("dtb_load_gpa not provided");
}
#[cfg(not(target_arch = "riscv64"))]
{
info!("dtb_load_gpa not provided");
}
}
self.load_boot_image_from_memory(vm_imags.bios)?;
Ok(())
}
#[cfg(target_arch = "x86_64")]
fn load_x86_linux_images_from_memory(
&mut self,
header: x86_linux::X86LinuxHeader,
kernel: &[u8],
ramdisk: Option<&[u8]>,
) -> AxResult {
self.adjust_x86_linux_dma_identity_layout()?;
let payload = x86_linux_payload(&header, kernel)?;
let initrd = if let Some(ramdisk) = ramdisk {
Some(x86_linux::X86LinuxRange::new(
self.ramdisk_load_gpa()?.as_usize(),
ramdisk.len(),
))
} else {
None
};
let layout = x86_linux::X86LinuxLoadLayout::new(
&header,
self.kernel_load_gpa.as_usize(),
payload.len(),
initrd,
)
.map_err(x86_linux_layout_error)?;
self.load_x86_linux_layout(header, layout, kernel)?;
load_vm_image_from_memory(payload, self.kernel_load_gpa, self.vm.clone())?;
if let Some(buffer) = ramdisk {
self.load_ramdisk_from_memory(buffer)?;
}
Ok(())
}
fn load_boot_image_from_memory(&self, bios: Option<&[u8]>) -> AxResult {
if !self.config.kernel.enable_bios {
return Ok(());
}
if let Some(buffer) = bios {
let load_gpa = self
.bios_load_gpa
.ok_or_else(|| ax_err_type!(NotFound, "boot firmware load address is missing"))?;
load_vm_image_from_memory(buffer, load_gpa, self.vm.clone())?;
#[cfg(target_arch = "x86_64")]
if should_patch_x86_multiboot_info(&self.config) {
self.load_x86_multiboot_info(buffer, load_gpa)?;
}
return Ok(());
}
#[cfg(target_arch = "x86_64")]
if self.config.kernel.effective_boot_protocol() == VMBootProtocol::Uefi {
let firmware_path = self.config.kernel.boot_firmware_path().ok_or_else(|| {
ax_errno::ax_err_type!(NotFound, "UEFI firmware image path is missed")
})?;
let load_gpa = self.bios_load_gpa.ok_or_else(|| {
ax_errno::ax_err_type!(NotFound, "UEFI firmware load addr is missed")
})?;
#[cfg(feature = "fs")]
{
info!(
"Loading UEFI firmware image {} at GPA {:#x}",
firmware_path,
load_gpa.as_usize()
);
return fs::load_vm_image(firmware_path, load_gpa, self.vm.clone());
}
#[cfg(not(feature = "fs"))]
{
return Err(ax_errno::ax_err_type!(
Unsupported,
"UEFI firmware path requires the fs feature when no firmware image buffer is available"
));
}
}
#[cfg(target_arch = "x86_64")]
if self.should_load_default_x86_boot_image() {
let bios_load_gpa = builtin_x86_bios_load_gpa(self.bios_load_gpa)?;
info!(
"Loading built-in x86 boot image at GPA {:#x}",
bios_load_gpa.as_usize()
);
load_vm_image_from_memory(
x86_boot::DEFAULT_BIOS_IMAGE,
bios_load_gpa,
self.vm.clone(),
)?;
#[cfg(target_arch = "x86_64")]
self.load_x86_multiboot_info(x86_boot::DEFAULT_BIOS_IMAGE, bios_load_gpa)?;
}
Ok(())
}
#[cfg(target_arch = "x86_64")]
fn should_load_default_x86_boot_image(&self) -> bool {
self.config.kernel.enable_bios
&& self.config.kernel.boot_firmware_path().is_none()
&& self.config.kernel.effective_boot_protocol() == VMBootProtocol::Multiboot
}
#[cfg(target_arch = "x86_64")]
fn load_x86_multiboot_info(&self, bios_image: &[u8], bios_load_gpa: GuestPhysAddr) -> AxResult {
const MULTIBOOT_INFO_GPA: usize = 0x6000;
const MULTIBOOT_MMAP_GPA: usize = 0x6040;
const MULTIBOOT_INFO_FLAGS: u32 = (1 << 0) | (1 << 6);
const MULTIBOOT_MEMORY_AVAILABLE: u32 = 1;
let mem_base = self.main_memory.gpa.as_usize() as u64;
let mem_size = self.main_memory.size() as u64;
let mem_upper_kb = mem_size.saturating_sub(0x100000) / 1024;
let mut mbi = [0u8; 52];
write_u32(&mut mbi, 0, MULTIBOOT_INFO_FLAGS);
write_u32(&mut mbi, 4, 639);
write_u32(&mut mbi, 8, mem_upper_kb as u32);
write_u32(&mut mbi, 44, 24);
write_u32(&mut mbi, 48, MULTIBOOT_MMAP_GPA as u32);
let mut mmap = [0u8; 24];
write_u32(&mut mmap, 0, 20);
write_u64(&mut mmap, 4, mem_base);
write_u64(&mut mmap, 12, mem_size);
write_u32(&mut mmap, 20, MULTIBOOT_MEMORY_AVAILABLE);
let mbi_gpa = (MULTIBOOT_INFO_GPA as u32).to_le_bytes();
validate_x86_bios_patch_region(bios_image)?;
load_vm_image_from_memory(&mbi, MULTIBOOT_INFO_GPA.into(), self.vm.clone())?;
load_vm_image_from_memory(&mmap, MULTIBOOT_MMAP_GPA.into(), self.vm.clone())?;
load_vm_image_from_memory(
&mbi_gpa,
(bios_load_gpa.as_usize() + x86_boot::AXVM_BIOS_EBX_IMM_OFFSET).into(),
self.vm.clone(),
)?;
Ok(())
}
fn load_ramdisk_from_memory(&self, ramdisk: &[u8]) -> AxResult {
let load_gpa = self.ramdisk_load_gpa()?;
let size = ramdisk.len();
self.vm.with_config(|config| {
if let Some(ref mut rd) = config.image_config.ramdisk {
rd.size = Some(size);
}
});
info!(
"Loading ramdisk image from memory ({} bytes) into GPA @{:#x}",
size,
load_gpa.as_usize()
);
load_vm_image_from_memory(ramdisk, load_gpa, self.vm.clone())
}
fn ramdisk_load_gpa(&self) -> AxResult<GuestPhysAddr> {
self.ramdisk_load_gpa
.ok_or_else(|| ax_errno::ax_err_type!(NotFound, "Ramdisk load addr is missed"))
}
#[cfg(target_arch = "x86_64")]
fn adjust_x86_linux_dma_identity_layout(&mut self) -> AxResult {
if !self.main_memory.is_identical() {
return Ok(());
}
let memory_base = self.main_memory.gpa.as_usize();
let configured_kernel = self.config.kernel.kernel_load_addr;
let configured_ramdisk = self.config.kernel.ramdisk_load_addr;
self.kernel_load_gpa = GuestPhysAddr::from(memory_base + configured_kernel);
if let Some(ramdisk_load_addr) = configured_ramdisk {
self.ramdisk_load_gpa = Some(GuestPhysAddr::from(memory_base + ramdisk_load_addr));
}
self.vm.with_config(|config| {
config.image_config.kernel_load_gpa = self.kernel_load_gpa;
if let Some(load_gpa) = self.ramdisk_load_gpa
&& let Some(ref mut ramdisk) = config.image_config.ramdisk
{
ramdisk.load_gpa = load_gpa;
}
});
info!(
"Adjusted x86 Linux identity DMA layout for VM[{}]: memory_base={:#x}, \
kernel_load_gpa={:#x}, ramdisk_load_gpa={:?}",
self.vm.id(),
memory_base,
self.kernel_load_gpa.as_usize(),
self.ramdisk_load_gpa
);
Ok(())
}
#[cfg(target_arch = "x86_64")]
fn load_x86_linux_layout(
&self,
header: x86_linux::X86LinuxHeader,
layout: x86_linux::X86LinuxLoadLayout,
kernel: &[u8],
) -> AxResult {
info!(
"x86 Linux layout for VM[{}]: header={:#x?}, payload_offset={:#x}, \
boot_params=[{:#x}..{:#x}), boot_stub=[{:#x}..{:#x}), kernel=[{:#x}..{:#x}), \
initrd={:?}",
self.config.base.id,
header,
header.payload_offset(),
layout.boot_params.start,
layout.boot_params.end().unwrap(),
layout.boot_stub.start,
layout.boot_stub.end().unwrap(),
layout.kernel.start,
layout.kernel.end().unwrap(),
layout.initrd
);
let boot_params = self.build_x86_boot_params(header, layout, kernel)?;
let boot_stub = self.build_x86_linux_boot_stub(&layout)?;
let mp_table = x86_mptable::build();
load_vm_image_from_memory(
&boot_params,
layout.boot_params.start.into(),
self.vm.clone(),
)?;
load_vm_image_from_memory(&boot_stub, layout.boot_stub.start.into(), self.vm.clone())?;
load_vm_image_from_memory(&mp_table, x86_mptable::MP_TABLE_GPA.into(), self.vm.clone())?;
self.install_x86_linux_boot_entry(&layout);
Ok(())
}
#[cfg(target_arch = "x86_64")]
fn build_x86_linux_boot_stub(
&self,
layout: &x86_linux::X86LinuxLoadLayout,
) -> AxResult<[u8; x86_linux::BOOT_STUB_SIZE]> {
x86_linux_boot::build_boot_image(layout).map_err(|err| {
ax_errno::ax_err_type!(
InvalidInput,
format!("failed to build x86 Linux boot stub: {err:?}")
)
})
}
#[cfg(target_arch = "x86_64")]
fn install_x86_linux_boot_entry(&self, layout: &x86_linux::X86LinuxLoadLayout) {
let entry = GuestPhysAddr::from(x86_linux_boot::DEFAULT_LINUX_BOOT_LOAD_GPA);
self.vm.with_config(|config| {
config.cpu_config.bsp_entry = entry;
config.cpu_config.ap_entry = entry;
});
info!(
"x86 Linux direct boot entry for VM[{}]: stub={:#x}, boot_params={:#x}, \
kernel_entry={:#x}, initrd={:?}",
self.config.base.id,
layout.boot_stub.start,
layout.boot_params.start,
layout.kernel.start,
layout.initrd
);
}
#[cfg(target_arch = "x86_64")]
fn build_x86_boot_params(
&self,
header: x86_linux::X86LinuxHeader,
layout: x86_linux::X86LinuxLoadLayout,
kernel: &[u8],
) -> AxResult<[u8; x86_linux::BOOT_PARAMS_SIZE]> {
let mut builder = x86_boot_params::BootParamsBuilder::new(
kernel,
header,
layout,
x86_linux::X86LinuxRange::new(self.main_memory.gpa.as_usize(), self.main_memory.size()),
);
let command_line = self.config.kernel.cmdline.as_deref().ok_or_else(|| {
ax_errno::ax_err_type!(
InvalidInput,
"x86 Linux direct boot requires kernel.cmdline in the VM config"
)
})?;
builder.set_command_line(command_line).map_err(|err| {
ax_errno::ax_err_type!(
InvalidInput,
format!("invalid x86 Linux command line: {err:?}")
)
})?;
for memory in &self.config.kernel.memory_regions {
if memory.map_type == VmMemMappingType::MapAlloc {
builder.add_ram_range(x86_linux::X86LinuxRange::new(memory.gpa, memory.size));
}
}
for device in &self.config.devices.passthrough_devices {
builder.add_reserved_range(x86_linux::X86LinuxRange::new(
device.base_gpa,
device.length,
));
}
for address in &self.config.devices.passthrough_addresses {
builder.add_reserved_range(x86_linux::X86LinuxRange::new(
address.base_gpa,
address.length,
));
}
for device in &self.config.devices.emu_devices {
if matches!(device.emu_type, EmulatedDeviceType::X86IoApic) {
builder.add_reserved_range(x86_linux::X86LinuxRange::new(
device.base_gpa,
device.length,
));
}
}
builder.add_reserved_range(x86_mptable::reserved_range());
builder.build().map_err(|err| {
ax_errno::ax_err_type!(
InvalidInput,
format!("failed to build x86 boot_params: {err:?}")
)
})
}
#[cfg(feature = "fs")]
fn load_ramdisk_from_filesystem(&self, ramdisk_path: &str) -> AxResult {
let load_gpa = self
.vm
.with_config(|config| config.image_config.ramdisk.as_ref().map(|r| r.load_gpa))
.ok_or_else(|| ax_errno::ax_err_type!(NotFound, "Ramdisk load addr is missed"))?;
let ramdisk_size = fs::image_size(ramdisk_path)?;
self.vm.with_config(|config| {
if let Some(ref mut rd) = config.image_config.ramdisk {
rd.size = Some(ramdisk_size);
}
});
info!(
"Loading ramdisk image from filesystem {} ({} bytes) into GPA @{:#x}",
ramdisk_path,
ramdisk_size,
load_gpa.as_usize()
);
fs::load_vm_image(ramdisk_path, load_gpa, self.vm.clone())
}
}
pub fn load_vm_image_from_memory(
image_buffer: &[u8],
load_addr: GuestPhysAddr,
vm: AxVMRef,
) -> AxResult {
let mut buffer_pos = 0;
let image_size = image_buffer.len();
debug!(
"loading VM image from memory {:?} {}",
load_addr,
image_buffer.len()
);
let image_load_regions = vm.get_image_load_region(load_addr, image_size)?;
for region in image_load_regions {
let region_len = region.len();
let bytes_to_write = region_len.min(image_size - buffer_pos);
unsafe {
core::ptr::copy_nonoverlapping(
image_buffer[buffer_pos..].as_ptr(),
region.as_mut_ptr().cast(),
bytes_to_write,
);
}
axvm::clean_dcache_range((region.as_ptr() as usize).into(), bytes_to_write);
buffer_pos += bytes_to_write;
if buffer_pos >= image_size {
debug!("copy size: {bytes_to_write}");
break;
}
}
if buffer_pos == image_size {
Ok(())
} else {
ax_err!(
InvalidData,
format!("VM image was only partially loaded: {buffer_pos}/{image_size} bytes")
)
}
}
#[cfg(feature = "fs")]
pub mod fs {
use alloc::vec::Vec;
use ax_errno::{AxResult, ax_err, ax_err_type};
use super::*;
pub fn kernel_read(config: &AxVMCrateConfig, read_size: usize) -> AxResult<Vec<u8>> {
let file_name = &config.kernel.kernel_path;
crate::manager::AxvmManager::read_file_exact(file_name, read_size)
}
pub(crate) fn load_vm_images_from_filesystem(loader: &mut ImageLoader) -> AxResult {
info!("Loading VM images from filesystem");
#[cfg(target_arch = "x86_64")]
{
if should_direct_boot_x86_linux(&loader.config) {
let kernel_probe = kernel_read(&loader.config, x86_linux::HEADER_READ_SIZE);
match kernel_probe {
Ok(data) => {
if let Some(header) = detect_x86_linux_image(&data) {
let kernel = read_image_file(&loader.config.kernel.kernel_path)?;
return loader.load_x86_linux_images_from_filesystem(header, &kernel);
}
}
Err(err) => debug!("Unable to probe x86 Linux bzImage header: {err:?}"),
}
}
}
load_vm_image(
&loader.config.kernel.kernel_path,
loader.kernel_load_gpa,
loader.vm.clone(),
)?;
if loader.config.kernel.enable_bios
&& let Some(bios_path) = loader.config.kernel.boot_firmware_path()
{
if let Some(bios_load_addr) = loader.bios_load_gpa {
#[cfg(target_arch = "x86_64")]
{
if should_patch_x86_multiboot_info(&loader.config) {
let bios_image = read_image_file(bios_path)?;
validate_x86_bios_patch_region(&bios_image)?;
load_vm_image_from_memory(&bios_image, bios_load_addr, loader.vm.clone())?;
loader.load_x86_multiboot_info(&bios_image, bios_load_addr)?;
} else {
load_vm_image(bios_path, bios_load_addr, loader.vm.clone())?;
}
}
#[cfg(not(target_arch = "x86_64"))]
load_vm_image(bios_path, bios_load_addr, loader.vm.clone())?;
} else {
return ax_err!(NotFound, "boot firmware load addr is missed");
}
};
#[cfg(target_arch = "x86_64")]
if loader.should_load_default_x86_boot_image() {
let bios_load_gpa = builtin_x86_bios_load_gpa(loader.bios_load_gpa)?;
info!(
"Loading built-in x86 boot image at GPA {:#x}",
bios_load_gpa.as_usize()
);
load_vm_image_from_memory(
x86_boot::DEFAULT_BIOS_IMAGE,
bios_load_gpa,
loader.vm.clone(),
)?;
#[cfg(target_arch = "x86_64")]
loader.load_x86_multiboot_info(x86_boot::DEFAULT_BIOS_IMAGE, bios_load_gpa)?;
}
if let Some(ramdisk_path) = &loader.config.kernel.ramdisk_path {
loader.load_ramdisk_from_filesystem(ramdisk_path)?;
};
let vm_config = crate::config::build_axvm_config(&loader.config);
if let Some(dtb_arc) = get_vm_dtb_arc(&vm_config) {
let _dtb_slice: &[u8] = &dtb_arc;
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
{
let dtb_src = core::ptr::NonNull::new(_dtb_slice.as_ptr() as *mut u8)
.ok_or_else(|| ax_err_type!(InvalidData, "Guest DTB pointer is null"))?;
crate::fdt::update_fdt(
dtb_src,
_dtb_slice.len(),
loader.vm.clone(),
&loader.config,
)?;
}
#[cfg(target_arch = "loongarch64")]
{
let dtb_load_gpa = loader
.dtb_load_gpa
.ok_or_else(|| ax_err_type!(NotFound, "DTB load address is missing"))?;
load_vm_image_from_memory(_dtb_slice, dtb_load_gpa, loader.vm.clone())?;
}
}
Ok(())
}
#[cfg(target_arch = "x86_64")]
impl ImageLoader {
fn load_x86_linux_images_from_filesystem(
&mut self,
header: x86_linux::X86LinuxHeader,
kernel: &[u8],
) -> AxResult {
self.adjust_x86_linux_dma_identity_layout()?;
let payload = x86_linux_payload(&header, kernel)?;
let initrd = if let Some(ramdisk_path) = &self.config.kernel.ramdisk_path {
let ramdisk_size = image_size(ramdisk_path)?;
Some(x86_linux::X86LinuxRange::new(
self.ramdisk_load_gpa()?.as_usize(),
ramdisk_size,
))
} else {
None
};
let layout = x86_linux::X86LinuxLoadLayout::new(
&header,
self.kernel_load_gpa.as_usize(),
payload.len(),
initrd,
)
.map_err(x86_linux_layout_error)?;
self.load_x86_linux_layout(header, layout, kernel)?;
load_vm_image_from_memory(payload, self.kernel_load_gpa, self.vm.clone())?;
if let Some(ramdisk_path) = &self.config.kernel.ramdisk_path {
self.load_ramdisk_from_filesystem(ramdisk_path)?;
}
Ok(())
}
}
pub(crate) fn load_vm_image(
image_path: &str,
image_load_gpa: GuestPhysAddr,
vm: AxVMRef,
) -> AxResult {
let image = crate::manager::AxvmManager::read_file(image_path)?;
let image_size = image.len();
let image_load_regions = vm.get_image_load_region(image_load_gpa, image_size)?;
let mut offset = 0;
for buffer in image_load_regions {
let end = offset + buffer.len();
let data = image.get(offset..end).ok_or_else(|| {
ax_err_type!(
InvalidData,
format!("Image {} has an invalid load region layout", image_path)
)
})?;
buffer.copy_from_slice(data);
offset = end;
axvm::clean_dcache_range((buffer.as_ptr() as usize).into(), buffer.len());
}
Ok(())
}
#[cfg(target_arch = "x86_64")]
fn read_image_file(image_path: &str) -> AxResult<Vec<u8>> {
crate::manager::AxvmManager::read_file(image_path)
}
pub fn image_size(file_name: &str) -> AxResult<usize> {
crate::manager::AxvmManager::file_size(file_name)
}
#[cfg(any(
target_arch = "aarch64",
target_arch = "loongarch64",
target_arch = "riscv64"
))]
pub fn read_full_image(file_name: &str) -> AxResult<Vec<u8>> {
crate::manager::AxvmManager::read_file(file_name)
}
}
#[cfg(target_arch = "x86_64")]
fn should_patch_x86_multiboot_info(config: &AxVMCrateConfig) -> bool {
config.kernel.effective_boot_protocol() == VMBootProtocol::Multiboot
}
#[cfg(target_arch = "x86_64")]
fn should_direct_boot_x86_linux(config: &AxVMCrateConfig) -> bool {
!config.kernel.enable_bios && config.kernel.effective_boot_protocol() == VMBootProtocol::Direct
}
#[cfg(target_arch = "x86_64")]
fn detect_x86_linux_image(image: &[u8]) -> Option<x86_linux::X86LinuxHeader> {
match x86_linux::X86LinuxHeader::parse(image) {
Ok(header) => Some(header),
Err(err) => {
debug!("Not an x86 Linux bzImage: {err:?}");
None
}
}
}
#[cfg(target_arch = "x86_64")]
fn x86_linux_payload<'a>(
header: &x86_linux::X86LinuxHeader,
image: &'a [u8],
) -> AxResult<&'a [u8]> {
let payload_offset = header.payload_offset();
image.get(payload_offset..).ok_or_else(|| {
ax_errno::ax_err_type!(
InvalidInput,
format!(
"x86 Linux bzImage payload offset {:#x} exceeds image size {:#x}",
payload_offset,
image.len()
)
)
})
}
#[cfg(target_arch = "x86_64")]
fn x86_linux_layout_error(err: x86_linux::X86LinuxLayoutError) -> ax_errno::AxError {
ax_errno::ax_err_type!(
InvalidInput,
format!("invalid x86 Linux memory layout: {err:?}")
)
}
#[cfg(target_arch = "x86_64")]
fn builtin_x86_bios_load_gpa(configured_gpa: Option<GuestPhysAddr>) -> AxResult<GuestPhysAddr> {
let default_gpa = GuestPhysAddr::from(x86_boot::DEFAULT_BIOS_LOAD_GPA);
match configured_gpa {
Some(gpa) if gpa != default_gpa => Err(ax_errno::ax_err_type!(
InvalidInput,
format!(
"built-in x86 BIOS must be loaded at GPA {:#x}, but bios_load_addr is {:#x}; set \
bios_path to use a relocatable external BIOS image",
default_gpa.as_usize(),
gpa.as_usize()
)
)),
Some(gpa) => Ok(gpa),
None => Ok(default_gpa),
}
}
#[cfg(target_arch = "x86_64")]
fn validate_x86_bios_patch_region(bios_image: &[u8]) -> AxResult {
let patch_end = x86_boot::AXVM_BIOS_EBX_IMM_OFFSET + core::mem::size_of::<u32>();
if bios_image.len() < patch_end {
return Err(ax_errno::ax_err_type!(
InvalidInput,
format!(
"x86 BIOS image is too small for multiboot info patch: size {}, need at least {} \
bytes for EBX immediate at offset {:#x}",
bios_image.len(),
patch_end,
x86_boot::AXVM_BIOS_EBX_IMM_OFFSET
)
));
}
if bios_image[x86_boot::AXVM_BIOS_EBX_IMM_OFFSET - 1] != x86_boot::MOV_EBX_IMM32_OPCODE {
return Err(ax_errno::ax_err_type!(
InvalidInput,
format!(
"x86 BIOS image does not match axvm-bios layout: expected mov ebx, imm32 opcode \
at offset {:#x}",
x86_boot::AXVM_BIOS_EBX_IMM_OFFSET - 1
)
));
}
Ok(())
}
#[cfg(target_arch = "x86_64")]
fn write_u32(buffer: &mut [u8], offset: usize, value: u32) {
buffer[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}
#[cfg(target_arch = "x86_64")]
fn write_u64(buffer: &mut [u8], offset: usize, value: u64) {
buffer[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
}
#[cfg(all(test, target_arch = "x86_64"))]
mod tests {
use super::*;
#[test]
fn built_in_x86_bios_uses_default_gpa_when_unspecified() {
assert_eq!(
builtin_x86_bios_load_gpa(None).unwrap(),
GuestPhysAddr::from(x86_boot::DEFAULT_BIOS_LOAD_GPA)
);
}
#[test]
fn built_in_x86_bios_accepts_explicit_default_gpa() {
let default_gpa = GuestPhysAddr::from(x86_boot::DEFAULT_BIOS_LOAD_GPA);
assert_eq!(
builtin_x86_bios_load_gpa(Some(default_gpa)).unwrap(),
default_gpa
);
}
#[test]
fn built_in_x86_bios_rejects_non_default_gpa() {
let invalid_gpa = GuestPhysAddr::from(x86_boot::DEFAULT_BIOS_LOAD_GPA + 0x1000);
assert!(builtin_x86_bios_load_gpa(Some(invalid_gpa)).is_err());
}
#[test]
fn legacy_x86_bios_config_uses_multiboot_patch() {
let mut cfg = AxVMCrateConfig::default();
cfg.kernel.enable_bios = true;
assert!(should_patch_x86_multiboot_info(&cfg));
}
#[test]
fn x86_uefi_config_skips_multiboot_patch() {
let mut cfg = AxVMCrateConfig::default();
cfg.kernel.enable_bios = true;
cfg.kernel.boot_protocol = Some(VMBootProtocol::Uefi);
assert!(!should_patch_x86_multiboot_info(&cfg));
}
#[test]
fn x86_linux_direct_boot_requires_direct_protocol() {
let mut cfg = AxVMCrateConfig::default();
assert!(should_direct_boot_x86_linux(&cfg));
cfg.kernel.enable_bios = true;
assert!(!should_direct_boot_x86_linux(&cfg));
cfg.kernel.boot_protocol = Some(VMBootProtocol::Uefi);
assert!(!should_direct_boot_x86_linux(&cfg));
cfg.kernel.boot_protocol = Some(VMBootProtocol::Direct);
assert!(!should_direct_boot_x86_linux(&cfg));
cfg.kernel.enable_bios = false;
assert!(should_direct_boot_x86_linux(&cfg));
}
}