diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e6c7aa172..2fd7feb65c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -699,6 +699,22 @@ jobs: container_image: "" limit_to_owner: rcore-os main_pr_only: false + - name: Test axvisor self-hosted board asus-nuc15crh-linux + use_container: false + runs_on: '["self-hosted","linux","board"]' + timeout_minutes: 20 + command: | + mkdir -p tmp + cargo xtask image pull qemu-x86_64 --output-dir tmp + cargo xtask image pull initramfs-x86_64-busybox.cpio.gz --output-dir tmp + gzip -dc \ + tmp/initramfs-x86_64-busybox.cpio.gz/initramfs-x86_64-busybox.cpio.gz \ + > tmp/initramfs-x86_64-busybox.cpio + cargo xtask axvisor test board --board asus-nuc15crh-linux + cache_key: "" + container_image: "" + limit_to_owner: rcore-os + main_pr_only: false - name: Test starry self-hosted board orangepi-5-plus use_container: false runs_on: '["self-hosted","linux","board"]' diff --git a/apps/starry/nix/build-aarch64-unknown-none-softfloat.toml b/apps/starry/nix/build-aarch64-unknown-none-softfloat.toml index f535bc06b6..bfbc1edaf1 100644 --- a/apps/starry/nix/build-aarch64-unknown-none-softfloat.toml +++ b/apps/starry/nix/build-aarch64-unknown-none-softfloat.toml @@ -5,4 +5,3 @@ features = [ "ax-driver/virtio-blk", "ax-driver/virtio-net", ] -plat_dyn = true diff --git a/apps/starry/nix/build-x86_64-unknown-none.toml b/apps/starry/nix/build-x86_64-unknown-none.toml index 3173417cb3..b29523b215 100644 --- a/apps/starry/nix/build-x86_64-unknown-none.toml +++ b/apps/starry/nix/build-x86_64-unknown-none.toml @@ -5,4 +5,3 @@ features = [ "ax-driver/virtio-blk", "ax-driver/virtio-net", ] -plat_dyn = true diff --git a/bootloader/axloader/src/loader/control.rs b/bootloader/axloader/src/loader/control.rs index 3c62be4521..36b5b4fd8f 100644 --- a/bootloader/axloader/src/loader/control.rs +++ b/bootloader/axloader/src/loader/control.rs @@ -39,7 +39,15 @@ pub fn fetch_boot_offer() -> Result { crate::logln!("serial_control_wait: waiting for AXLOADER BOOT"); announce_ready(); loop { - let line = read_boot_line()?; + let line = match read_boot_line() { + Ok(line) => line, + Err(ControlError::Timeout) => return Err(ControlError::Timeout), + Err(err) => { + crate::logln!("serial_control_ignored: read_error={err:?}"); + announce_ready(); + continue; + } + }; match parse_boot_offer(&line) { Ok(offer) if valid_kernel_url(&offer.kernel_url) => return Ok(offer), Ok(offer) => { @@ -47,8 +55,12 @@ pub fn fetch_boot_offer() -> Result { "serial_control_ignored: invalid kernel_url {}", offer.kernel_url ); + announce_ready(); + } + Err(err) => { + crate::logln!("serial_control_ignored: parse_error={err:?}"); + announce_ready(); } - Err(err) => return Err(err), } } } @@ -133,7 +145,22 @@ fn parse_boot_offer(input: &str) -> Result { } fn valid_kernel_url(url: &str) -> bool { - url.starts_with("http://") && url.contains("/kernel.elf") + let Some(rest) = url.strip_prefix("http://") else { + return false; + }; + if rest + .bytes() + .any(|byte| matches!(byte, b'\0' | b'\r' | b'\n' | b' ' | b'\t')) + { + return false; + } + + let Some(path_start) = rest.find('/') else { + return false; + }; + let authority = &rest[..path_start]; + let path = &rest[path_start..]; + !authority.is_empty() && path.ends_with("/kernel.elf") } fn json_string_field<'a>(input: &'a str, key: &str) -> Option<&'a str> { diff --git a/bootloader/axloader/src/loader/elf_loader.rs b/bootloader/axloader/src/loader/elf_loader.rs index 30928d1c3a..bca0c1c9d3 100644 --- a/bootloader/axloader/src/loader/elf_loader.rs +++ b/bootloader/axloader/src/loader/elf_loader.rs @@ -44,6 +44,13 @@ pub struct LoadedElf { pub load_addr: u64, pub load_end: u64, pub page_count: usize, + pub handoff: EntryHandoff, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EntryHandoff { + BootInfo, + Uefi, } #[repr(C)] @@ -126,12 +133,7 @@ fn load_elf(image: &[u8], entry_symbol: Option<&str>) -> Result) -> Result find_symbol(image, &header, "httpboot_entry") - .and_then(|symbol| virtual_to_physical(symbol, &segments)) - .ok_or(ElfLoadError::EntryNotInLoadSegment)?, + let (entry, handoff) = match entry_symbol { + Some("httpboot_entry") => { + if let Some(entry) = find_symbol(image, &header, "httpboot_entry") + .and_then(|symbol| virtual_to_physical(symbol, &segments)) + { + (entry, EntryHandoff::BootInfo) + } else if let Some(entry) = find_symbol(image, &header, "__x86_64_efi_pe_entry") + .and_then(|symbol| virtual_to_physical(symbol, &segments)) + { + (entry, EntryHandoff::Uefi) + } else { + ( + virtual_to_physical(header.e_entry, &segments) + .ok_or(ElfLoadError::EntryNotInLoadSegment)?, + EntryHandoff::Uefi, + ) + } + } Some(_) => return Err(ElfLoadError::UnsupportedEntrySymbol), - None => virtual_to_physical(header.e_entry, &segments) - .ok_or(ElfLoadError::EntryNotInLoadSegment)?, + None => ( + virtual_to_physical(header.e_entry, &segments) + .ok_or(ElfLoadError::EntryNotInLoadSegment)?, + EntryHandoff::BootInfo, + ), }; + let entry = biased_addr(entry, load_addr, actual_load_addr)?; + let actual_load_end = actual_load_addr + .checked_add(page_count as u64 * UEFI_PAGE_SIZE) + .ok_or(ElfLoadError::SegmentAddressOverflow)?; Ok(LoadedElf { entry_point: entry, - load_addr, - load_end, + load_addr: actual_load_addr, + load_end: actual_load_end, page_count, + handoff, }) } +fn biased_addr(addr: u64, original_base: u64, actual_base: u64) -> Result { + let addr = addr as i128 + actual_base as i128 - original_base as i128; + if !(0..=u64::MAX as i128).contains(&addr) { + return Err(ElfLoadError::SegmentAddressOverflow); + } + Ok(addr as u64) +} + +fn allocate_load_region( + preferred_load_addr: u64, + page_count: usize, +) -> Result<(NonNull, u64), ElfLoadError> { + match boot::allocate_pages( + AllocateType::Address(preferred_load_addr), + MemoryType::LOADER_DATA, + page_count, + ) { + Ok(target) => Ok((target, preferred_load_addr)), + Err(_) => { + // This is only a load-bias fallback: axloader moves PT_LOAD segments as a + // whole and adjusts the handoff entry point, but it does not process ELF + // relocation records or rewrite absolute references embedded in the image. + // It is therefore valid only for images that are already known to tolerate + // this placement model, such as the current AxVisor UEFI stub. A generic + // non-PIE ET_EXEC kernel may still dereference its original physical + // addresses and fail early after being loaded at AnyPages. + crate::logln!( + "elf_load_relocate: preferred={:#x} pages={}", + preferred_load_addr, + page_count + ); + let target = + boot::allocate_pages(AllocateType::AnyPages, MemoryType::LOADER_DATA, page_count) + .map_err(|_| ElfLoadError::AllocateFailed)?; + Ok((target, target.as_ptr() as u64)) + } + } +} + fn validate_header(header: &Elf64Header) -> Result<(), ElfLoadError> { if &header.ident[..4] != ELF_MAGIC { return Err(ElfLoadError::BadMagic); diff --git a/bootloader/axloader/src/loader/entry.rs b/bootloader/axloader/src/loader/entry.rs index ed0dc7f54c..5518312734 100644 --- a/bootloader/axloader/src/loader/entry.rs +++ b/bootloader/axloader/src/loader/entry.rs @@ -1,7 +1,7 @@ use core::{mem, ptr::NonNull}; use uefi::{ - boot, + Status, boot, mem::memory_map::{MemoryMap, MemoryType}, }; @@ -14,6 +14,7 @@ const OSTOOL_BOOT_INFO_MAX_RAM_REGIONS: usize = 32; pub enum JumpError { EntryAddressTooLarge, BootInfoAllocateFailed, + SystemTableUnavailable, } #[repr(C)] @@ -71,6 +72,27 @@ pub fn exit_boot_services_and_jump(entry_point: u64) -> Result<(), JumpError> { unsafe { call_entry_point(entry_point, boot_info_ptr) } } +#[cfg(target_arch = "x86_64")] +pub fn jump_to_uefi_entry(entry_point: u64) -> Result<(), JumpError> { + let entry_point = usize::try_from(entry_point).map_err(|_| JumpError::EntryAddressTooLarge)?; + let system_table = uefi::table::system_table_raw().ok_or(JumpError::SystemTableUnavailable)?; + // This handoff intentionally reuses axloader's image handle. It is only + // suitable for loaded entries that use the UEFI system table but do not + // query LoadedImage or other image-handle-specific protocols. + unsafe { + call_uefi_entry_point( + entry_point, + boot::image_handle(), + system_table.as_ptr().cast(), + ) + } +} + +#[cfg(not(target_arch = "x86_64"))] +pub fn jump_to_uefi_entry(_entry_point: u64) -> Result<(), JumpError> { + Err(JumpError::SystemTableUnavailable) +} + fn allocate_boot_info() -> Result, JumpError> { let ptr = boot::allocate_pool(MemoryType::LOADER_DATA, mem::size_of::()) .map_err(|_| JumpError::BootInfoAllocateFailed)?; @@ -102,3 +124,18 @@ unsafe fn call_entry_point(entry_point: usize, boot_info: usize) -> ! { let entry: extern "C" fn(usize) -> ! = unsafe { core::mem::transmute(entry_point) }; entry(boot_info) } + +#[cfg(target_arch = "x86_64")] +unsafe fn call_uefi_entry_point( + entry_point: usize, + image_handle: uefi::Handle, + system_table: *const core::ffi::c_void, +) -> Result<(), JumpError> { + // SAFETY: the address comes from the loaded kernel ELF symbol + // `__x86_64_efi_pe_entry`, whose ABI matches a UEFI PE entry point. + let entry: extern "efiapi" fn(uefi::Handle, *const core::ffi::c_void) -> Status = + unsafe { core::mem::transmute(entry_point) }; + let status = entry(image_handle, system_table); + crate::logln!("uefi_entry_returned: {status:?}"); + Ok(()) +} diff --git a/bootloader/axloader/src/loader/mod.rs b/bootloader/axloader/src/loader/mod.rs index 8b3cca0b49..eb4ad6f8de 100644 --- a/bootloader/axloader/src/loader/mod.rs +++ b/bootloader/axloader/src/loader/mod.rs @@ -57,13 +57,22 @@ fn fetch_control_offer() -> bool { ) { Ok(elf) => { logln!( - "elf_loaded: load={:#x} end={:#x} pages={} entry={:#x}", + "elf_loaded: load={:#x} end={:#x} pages={} entry={:#x} handoff={:?}", elf.load_addr, elf.load_end, elf.page_count, - elf.entry_point + elf.entry_point, + elf.handoff ); - match entry::exit_boot_services_and_jump(elf.entry_point) { + let jump_result = match elf.handoff { + elf_loader::EntryHandoff::BootInfo => { + entry::exit_boot_services_and_jump(elf.entry_point) + } + elf_loader::EntryHandoff::Uefi => { + entry::jump_to_uefi_entry(elf.entry_point) + } + }; + match jump_result { Ok(()) => logln!("jump_error: entry returned unexpectedly"), Err(err) => logln!("jump_error: {err:?}"), } diff --git a/drivers/rdrive/src/lib.rs b/drivers/rdrive/src/lib.rs index 1c279ae4f1..5aac11b826 100644 --- a/drivers/rdrive/src/lib.rs +++ b/drivers/rdrive/src/lib.rs @@ -43,6 +43,7 @@ pub enum Platform { Static, Fdt { addr: NonNull }, Acpi(probe::acpi::AcpiRoot), + AcpiWithoutAml(probe::acpi::AcpiRoot), } unsafe impl Send for Platform {} @@ -52,6 +53,7 @@ pub enum PlatformSource { Static, Fdt(NonNull), Acpi(probe::acpi::AcpiRoot), + AcpiWithoutAml(probe::acpi::AcpiRoot), } unsafe impl Send for PlatformSource {} @@ -69,6 +71,7 @@ pub fn init(platform: Platform) -> Result<(), DriverError> { Platform::Static => init_sources(&[PlatformSource::Static])?, Platform::Fdt { addr } => init_sources(&[PlatformSource::Fdt(addr)])?, Platform::Acpi(root) => init_sources(&[PlatformSource::Acpi(root)])?, + Platform::AcpiWithoutAml(root) => init_sources(&[PlatformSource::AcpiWithoutAml(root)])?, } Ok(()) } @@ -78,7 +81,9 @@ pub fn init_sources(sources: &[PlatformSource]) -> Result<(), DriverError> { match source { PlatformSource::Static => {} PlatformSource::Fdt(addr) => probe::fdt::check_addr(*addr)?, - PlatformSource::Acpi(root) => probe::acpi::check_root(*root)?, + PlatformSource::Acpi(root) | PlatformSource::AcpiWithoutAml(root) => { + probe::acpi::check_root(*root)? + } } } @@ -87,6 +92,7 @@ pub fn init_sources(sources: &[PlatformSource]) -> Result<(), DriverError> { PlatformSource::Static => probe::static_::init()?, PlatformSource::Fdt(addr) => probe::fdt::init(*addr)?, PlatformSource::Acpi(root) => probe::acpi::init(*root)?, + PlatformSource::AcpiWithoutAml(root) => probe::acpi::init_without_aml(*root)?, } } diff --git a/drivers/rdrive/src/probe/acpi.rs b/drivers/rdrive/src/probe/acpi.rs index f720fba593..33b80e3950 100644 --- a/drivers/rdrive/src/probe/acpi.rs +++ b/drivers/rdrive/src/probe/acpi.rs @@ -891,6 +891,15 @@ pub fn check_root(root: AcpiRoot) -> Result<(), DriverError> { pub fn init(root: AcpiRoot) -> Result<(), DriverError> { let system = System::new(root)?; + init_system(system) +} + +pub fn init_without_aml(root: AcpiRoot) -> Result<(), DriverError> { + let system = System::new_without_aml(root)?; + init_system(system) +} + +fn init_system(system: System) -> Result<(), DriverError> { info!( "ACPI initialized: {} PCI ECAM region(s), {} IOAPIC(s), {} PCH-PIC(s)", system.pci_ecam_regions().len(), @@ -1134,7 +1143,7 @@ impl AcpiRoot { pub struct System { ecam_regions: Vec, routing: AcpiRouting, - interpreter: Interpreter, + interpreter: Option>, handler: AcpiHandler, pci: Option, probed_names: Mutex>, @@ -1160,21 +1169,35 @@ struct AcpiPciRoot { impl System { pub fn new(root: AcpiRoot) -> Result { + Self::new_with_options(root, true) + } + + pub fn new_without_aml(root: AcpiRoot) -> Result { + Self::new_with_options(root, false) + } + + fn new_with_options(root: AcpiRoot, load_aml: bool) -> Result { let handler = root.handler(); let tables = unsafe { AcpiTables::from_rsdp(handler.clone(), root.rsdp) }.map_err(acpi_error)?; let ecam_regions = read_pci_ecam_regions(&tables)?; let routing = read_interrupt_routing(&tables)?; let namespace_handler = root.handler_with_pci_ecam(ecam_regions.clone()); - let platform = AcpiPlatform::new(tables, namespace_handler.clone()).map_err(acpi_error)?; - let interpreter = Interpreter::new_from_platform(&platform).map_err(acpi_error)?; - interpreter.initialize_namespace(); - let pci = match read_pci_namespace(&interpreter) { - Ok(pci) => Some(pci), - Err(err) => { - warn!("failed to discover ACPI PCI namespace: {err:?}"); - None - } + let (interpreter, pci) = if load_aml { + let platform = + AcpiPlatform::new(tables, namespace_handler.clone()).map_err(acpi_error)?; + let interpreter = Interpreter::new_from_platform(&platform).map_err(acpi_error)?; + interpreter.initialize_namespace(); + let pci = match read_pci_namespace(&interpreter) { + Ok(pci) => Some(pci), + Err(err) => { + warn!("failed to discover ACPI PCI namespace: {err:?}"); + None + } + }; + (Some(interpreter), pci) + } else { + (None, None) }; Ok(Self { @@ -1302,7 +1325,9 @@ impl System { u16::from(route.root_device), u16::from(route.root_function), pin, - &self.interpreter, + self.interpreter + .as_ref() + .expect("ACPI PCI routing requires an AML interpreter"), &self.handler, &mut pci.link_allocator.lock(), ) @@ -1315,7 +1340,9 @@ impl System { u16::from(route.root_device), u16::from(route.root_function), pin, - &self.interpreter, + self.interpreter + .as_ref() + .expect("ACPI PCI routing requires an AML interpreter"), ) { Ok(route) => return Ok(Some(route)), Err(AmlError::PrtNoEntry) => {} @@ -1355,20 +1382,23 @@ impl System { } fn device_infos_for_ids(&self, ids: &[AcpiId]) -> Result, ProbeError> { + let Some(interpreter) = &self.interpreter else { + return Ok(Vec::new()); + }; let mut devices = Vec::new(); - let mut namespace = self.interpreter.namespace.lock().clone(); + let mut namespace = interpreter.namespace.lock().clone(); namespace .traverse(|path, level| { if level.kind != NamespaceLevelKind::Device { return Ok(true); } - let Some((hid, cids)) = acpi_device_ids(&self.interpreter, path)? else { + let Some((hid, cids)) = acpi_device_ids(interpreter, path)? else { return Ok(true); }; if !acpi_ids_match(&hid, &cids, ids) { return Ok(true); } - let resources = read_device_resources(&self.interpreter, path, &self.routing)?; + let resources = read_device_resources(interpreter, path, &self.routing)?; devices.push(AcpiDeviceInfo { path: path.as_string(), hid: Some(hid), @@ -1384,17 +1414,20 @@ impl System { } fn device_infos(&self) -> Result, ProbeError> { + let Some(interpreter) = &self.interpreter else { + return Ok(Vec::new()); + }; let mut devices = Vec::new(); - let mut namespace = self.interpreter.namespace.lock().clone(); + let mut namespace = interpreter.namespace.lock().clone(); namespace .traverse(|path, level| { if level.kind != NamespaceLevelKind::Device { return Ok(true); } - let Some((hid, cids)) = acpi_device_ids(&self.interpreter, path)? else { + let Some((hid, cids)) = acpi_device_ids(interpreter, path)? else { return Ok(true); }; - let resources = read_device_resources(&self.interpreter, path, &self.routing)?; + let resources = read_device_resources(interpreter, path, &self.routing)?; devices.push(AcpiDeviceInfo { path: path.as_string(), hid: Some(hid), diff --git a/os/axvisor/configs/board/asus-nuc15crh-x86_64.toml b/os/axvisor/configs/board/asus-nuc15crh-x86_64.toml new file mode 100644 index 0000000000..2c9409c2fa --- /dev/null +++ b/os/axvisor/configs/board/asus-nuc15crh-x86_64.toml @@ -0,0 +1,5 @@ +env = { AX_IP = "10.0.2.15", AX_GW = "10.0.2.2", RDRIVE_ACPI_LOAD_AML = "0" } +features = [] +log = "Info" +target = "x86_64-unknown-none" +vm_configs = [] diff --git a/os/axvisor/configs/vms/asus-nuc15crh/linux-smp1.toml b/os/axvisor/configs/vms/asus-nuc15crh/linux-smp1.toml new file mode 100644 index 0000000000..e7594d58fa --- /dev/null +++ b/os/axvisor/configs/vms/asus-nuc15crh/linux-smp1.toml @@ -0,0 +1,78 @@ +# Vm base info configs +# +[base] +# Guest vm id. +id = 1 +# Guest vm name. +name = "linux-asus-nuc15crh" +# Virtualization type. +vm_type = 1 +# The number of virtual CPUs. +cpu_num = 1 +# Guest vm physical cpu sets. +phys_cpu_sets = [1] + +# +# Vm kernel configs +# +[kernel] +# The entry point of the kernel image. +# Linux direct boot uses the Linux-specific boot stub at this GPA. +entry_point = 0x8000 +# The location of image: "memory" | "fs". +# The ASUS NUC15CRH HTTP Boot flow embeds the Linux guest images into AxVisor. +image_location = "memory" +# The file path of the kernel image. +kernel_path = "../../../../../tmp/qemu-x86_64/linux/linux-qemu" +# The load address of the kernel image. +kernel_load_addr = 0x20_0000 +# Whether to enable BIOS boot flow. +enable_bios = false +# The file path of the initramfs image. +ramdisk_path = "../../../../../tmp/initramfs-x86_64-busybox.cpio" +# The load address of the initramfs image. +ramdisk_load_addr = 0x0600_0000 +# Linux kernel command line. +cmdline = "console=ttyS0,115200n8 earlycon=uart8250,io,0x3f8,115200 ignore_loglevel loglevel=8 rdinit=/init devtmpfs.mount=1 acpi=off pci=off nox2apic tsc=unstable no_timer_check initcall_blacklist=ahci_pci_driver_init,i8042_init" + +# Memory regions with format (`base_paddr`, `size`, `flags`, `map_type`). +# For `map_type`, 0 means `MAP_ALLOC`, 1 means `MAP_IDENTICAL`, 2 means `MAP_RESERVED`. +memory_regions = [ + [0x0000_0000, 0x0800_0000, 0x7, 0], # System RAM 128M +] + +# +# Device specifications +# +[devices] +# The interrupt mode. +interrupt_mode = "passthrough" + +# Emu_devices. +# Name Base-Ipa Ipa_len Alloc-Irq Emu-Type EmuConfig. +emu_devices = [ + ["x86-com1", 0x3f8, 0x8, 0x0, 0x2, []], + ["x86-ioapic", 0xfec0_0000, 0x1000, 0x0, 0x23, []], + ["x86-pit", 0x40, 0x22, 0x0, 0x24, []], +] + +# Pass-through devices. +# Name Base-Ipa Base-Pa Length Alloc-Irq. +passthrough_devices = [ + [ + "HPET", + 0xfed0_0000, + 0xfed0_0000, + 0x1000, + 0x1, + ], +] + +# Passthrough addresses. +# Base-GPA Length. +passthrough_addresses = [ + [0xfe00_0000, 0x00c0_0000], +] + +# Devices that are not desired to be passed through to the guest +excluded_devices = [] diff --git a/platforms/somehal/src/driver.rs b/platforms/somehal/src/driver.rs index b8bc8fd3ba..01219d68f2 100644 --- a/platforms/somehal/src/driver.rs +++ b/platforms/somehal/src/driver.rs @@ -9,10 +9,14 @@ pub fn rdrive_setup() { .unwrap(); } else if let Some(rsdp) = someboot::rsdp_addr_phys() { info!("Initializing rdrive with ACPI RSDP at {:#x}", rsdp); - if let Err(err) = rdrive::init(rdrive::Platform::Acpi(rdrive::probe::acpi::AcpiRoot::new( - rsdp, - someboot::mem::phys_to_virt, - ))) { + let root = rdrive::probe::acpi::AcpiRoot::new(rsdp, someboot::mem::phys_to_virt); + let platform = if option_env!("RDRIVE_ACPI_LOAD_AML") == Some("0") { + info!("Initializing rdrive ACPI without AML loading"); + rdrive::Platform::AcpiWithoutAml(root) + } else { + rdrive::Platform::Acpi(root) + }; + if let Err(err) = rdrive::init(platform) { warn!( "failed to initialize rdrive with ACPI RSDP {:#x}: {:?}", rsdp, err diff --git a/scripts/axbuild/src/image/config.rs b/scripts/axbuild/src/image/config.rs index 719c261526..ed8348c7ec 100644 --- a/scripts/axbuild/src/image/config.rs +++ b/scripts/axbuild/src/image/config.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; pub const DEFAULT_REGISTRY_URL: &str = "https://raw.githubusercontent.com/rcore-os/tgosimages/refs/heads/main/registry/default.toml"; pub const DEFAULT_FALLBACK_REGISTRY_URL: &str = - "https://raw.githubusercontent.com/rcore-os/tgosimages/refs/heads/main/registry/v0.0.6.toml"; + "https://raw.githubusercontent.com/rcore-os/tgosimages/refs/heads/main/registry/v0.0.8.toml"; pub const IMAGE_CONFIG_FILENAME: &str = ".image.toml"; const DEFAULT_AUTO_SYNC_THRESHOLD: u64 = 60 * 60 * 24 * 7; const LOCAL_STORAGE_ENV: &str = "TGOS_IMAGE_LOCAL_STORAGE"; diff --git a/test-suit/axvisor/normal/board-asus-nuc15crh/build-x86_64-unknown-none.toml b/test-suit/axvisor/normal/board-asus-nuc15crh/build-x86_64-unknown-none.toml new file mode 100644 index 0000000000..d2652d2b45 --- /dev/null +++ b/test-suit/axvisor/normal/board-asus-nuc15crh/build-x86_64-unknown-none.toml @@ -0,0 +1,5 @@ +env = { AX_IP = "10.0.2.15", AX_GW = "10.0.2.2", RDRIVE_ACPI_LOAD_AML = "0" } +features = [] +log = "Info" +target = "x86_64-unknown-none" +vm_configs = ["os/axvisor/configs/vms/asus-nuc15crh/linux-smp1.toml"] diff --git a/test-suit/axvisor/normal/board-asus-nuc15crh/smoke/board-asus-nuc15crh-linux.toml b/test-suit/axvisor/normal/board-asus-nuc15crh/smoke/board-asus-nuc15crh-linux.toml new file mode 100644 index 0000000000..033c028350 --- /dev/null +++ b/test-suit/axvisor/normal/board-asus-nuc15crh/smoke/board-asus-nuc15crh-linux.toml @@ -0,0 +1,13 @@ +board_type = "Asus-nuc15-x86_64-vmx" +fail_regex = [ + "(?i)\\bpanic(?:ked)?\\b", + "(?i)kernel panic", + "(?i)login incorrect", + "(?i)permission denied", +] +shell_prefix = "~ #" +shell_init_cmd = "echo 'hello'" +success_regex = [ + "(?m)^hello\\s*$", +] +timeout = 300 diff --git a/test-suit/axvisor/normal/qemu/smoke/qemu-x86_64-svm.toml b/test-suit/axvisor/normal/qemu/smoke/qemu-x86_64-svm.toml index 1b7c3e32e7..85527cd0f1 100644 --- a/test-suit/axvisor/normal/qemu/smoke/qemu-x86_64-svm.toml +++ b/test-suit/axvisor/normal/qemu/smoke/qemu-x86_64-svm.toml @@ -22,7 +22,7 @@ args = [ "-m", "512M", ] -timeout = 180 +timeout = 300 fail_regex = [ "(?i)\\bpanic(?:ked)?\\b", "(?i)kernel panic", diff --git a/virtualization/axvm/src/arch/mod.rs b/virtualization/axvm/src/arch/mod.rs index 0999e820d3..6ddb3bb8cb 100644 --- a/virtualization/axvm/src/arch/mod.rs +++ b/virtualization/axvm/src/arch/mod.rs @@ -195,6 +195,7 @@ pub(crate) struct VcpuSetupContext<'a> { pub(crate) interrupt_mode: VMInterruptMode, pub(crate) emulates_console: bool, pub(crate) passthrough_ports: &'a [PassThroughPortConfig], + pub(crate) memory_regions: &'a [crate::vm::VMMemoryRegion], pub(crate) firmware_boot: bool, } diff --git a/virtualization/axvm/src/arch/x86_64/mod.rs b/virtualization/axvm/src/arch/x86_64/mod.rs index 86a70e4421..5318398bf0 100644 --- a/virtualization/axvm/src/arch/x86_64/mod.rs +++ b/virtualization/axvm/src/arch/x86_64/mod.rs @@ -73,6 +73,15 @@ impl ArchOps for X86_64Arch { ) -> AxResult<::SetupConfig> { let mut config = X86VCpuSetupConfig { emulate_com1: ctx.emulates_console, + guest_memory_regions: ctx + .memory_regions + .iter() + .map(|region| x86_vcpu::X86GuestMemoryRegion { + gpa: X86GuestPhysAddr::from_usize(region.gpa.as_usize()), + hva: X86HostVirtAddr::from_usize(region.hva.as_usize()), + size: region.size(), + }) + .collect(), ..Default::default() }; for port in ctx.passthrough_ports { diff --git a/virtualization/axvm/src/vm/prepare/vcpus.rs b/virtualization/axvm/src/vm/prepare/vcpus.rs index b449cfd22e..d3081e4641 100644 --- a/virtualization/axvm/src/vm/prepare/vcpus.rs +++ b/virtualization/axvm/src/vm/prepare/vcpus.rs @@ -65,6 +65,7 @@ impl PreparedVcpus { .iter() .any(|dev| dev.emu_type == EmulatedDeviceType::Console), passthrough_ports: resources.config.pass_through_ports(), + memory_regions: &resources.memory_regions, firmware_boot: guest_uses_firmware_boot(resources), })?; diff --git a/virtualization/x86_vcpu/src/lib.rs b/virtualization/x86_vcpu/src/lib.rs index fa09ff3e5c..01e108dbaa 100644 --- a/virtualization/x86_vcpu/src/lib.rs +++ b/virtualization/x86_vcpu/src/lib.rs @@ -25,6 +25,8 @@ extern crate alloc; #[cfg(test)] extern crate std; +use alloc::vec::Vec; + #[cfg(all(feature = "vmx", feature = "svm"))] compile_error!("features `vmx` and `svm` are mutually exclusive"); @@ -76,13 +78,26 @@ pub struct X86PassthroughPortRange { pub length: u16, } -/// x86 vCPU setup configuration. +/// Guest RAM region backing for x86 vCPU helpers. #[derive(Clone, Copy, Debug)] +pub struct X86GuestMemoryRegion { + /// Guest physical start address. + pub gpa: X86GuestPhysAddr, + /// Host virtual start address backing the guest memory. + pub hva: X86HostVirtAddr, + /// Region size in bytes. + pub size: usize, +} + +/// x86 vCPU setup configuration. +#[derive(Clone, Debug)] pub struct X86VCpuSetupConfig { /// Intercept COM1 PIO ports and route them to an emulated serial device. pub emulate_com1: bool, /// Host I/O port ranges routed through AxVM passthrough port devices. pub passthrough_ports: [Option; X86_MAX_PASSTHROUGH_PORT_RANGES], + /// Guest RAM regions used by the VMX instruction decoder to read guest bytes. + pub guest_memory_regions: Vec, } impl Default for X86VCpuSetupConfig { @@ -90,6 +105,7 @@ impl Default for X86VCpuSetupConfig { Self { emulate_com1: false, passthrough_ports: [None; X86_MAX_PASSTHROUGH_PORT_RANGES], + guest_memory_regions: Vec::new(), } } } diff --git a/virtualization/x86_vcpu/src/vmx/percpu.rs b/virtualization/x86_vcpu/src/vmx/percpu.rs index 735626b03e..6c224709bc 100644 --- a/virtualization/x86_vcpu/src/vmx/percpu.rs +++ b/virtualization/x86_vcpu/src/vmx/percpu.rs @@ -46,6 +46,10 @@ pub struct VmxPerCpuState { /// This region typically contains the VMCS and other state information /// required for managing virtual machines on this particular CPU. vmx_region: VmxRegion, + + /// Original host CR0/CR4 values before entering VMX operation. + host_cr0_cr4: Option<(u64, u64)>, + _host: PhantomData H>, } @@ -54,6 +58,7 @@ impl VmxPerCpuState { Ok(Self { vmcs_revision_id: 0, vmx_region: unsafe { VmxRegion::::uninit() }, + host_cr0_cr4: None, _host: PhantomData, }) } @@ -85,56 +90,65 @@ impl VmxPerCpuState { return x86_err!(Unsupported, "VMX disabled by BIOS"); } - // Check control registers are in a VMX-friendly state. (SDM Vol. 3C, Appendix A.7, A.8) - macro_rules! cr_is_valid { - ($value:expr, $crx:ident) => {{ - use Msr::*; - let value = $value; - paste::paste! { - let fixed0 = [].read(); - let fixed1 = [].read(); - } - (!fixed0 | value != 0) && (fixed1 | !value != 0) - }}; - } - if !cr_is_valid!(Cr0::read().bits(), CR0) { + let cr0 = Cr0::read_raw(); + let cr0_fixed0 = Msr::IA32_VMX_CR0_FIXED0.read(); + let cr0_fixed1 = Msr::IA32_VMX_CR0_FIXED1.read(); + let cr0_vmx = vmx_fixed_control_value(cr0, cr0_fixed0, cr0_fixed1); + let cr4 = Cr4::read_raw(); + let cr4_fixed0 = Msr::IA32_VMX_CR4_FIXED0.read(); + let cr4_fixed1 = Msr::IA32_VMX_CR4_FIXED1.read(); + let cr4_vmx = vmx_fixed_control_value(cr4, cr4_fixed0, cr4_fixed1); + + if !is_vmx_fixed_control_value_valid(cr0_vmx, cr0_fixed0, cr0_fixed1) { return x86_err!(BadState, "host CR0 is not valid in VMX operation"); } - if !cr_is_valid!(Cr4::read().bits(), CR4) { + if !is_vmx_fixed_control_value_valid(cr4_vmx, cr4_fixed0, cr4_fixed1) { return x86_err!(BadState, "host CR4 is not valid in VMX operation"); } // Get VMCS revision identifier in IA32_VMX_BASIC MSR. let vmx_basic = VmxBasic::read(); - if vmx_basic.region_size as usize != PAGE_SIZE { - return x86_err!(Unsupported); + if vmx_basic.region_size as usize > PAGE_SIZE { + return x86_err!( + Unsupported, + format_args!( + "unsupported VMX region size: {} bytes", + vmx_basic.region_size + ) + ); } if vmx_basic.mem_type != VmxBasic::VMX_MEMORY_TYPE_WRITE_BACK { - return x86_err!(Unsupported); + return x86_err!( + Unsupported, + format_args!("unsupported VMX memory type: {}", vmx_basic.mem_type) + ); } if vmx_basic.is_32bit_address { - return x86_err!(Unsupported); + return x86_err!(Unsupported, "unsupported 32-bit VMX physical address width"); } if !vmx_basic.io_exit_info { - return x86_err!(Unsupported); + return x86_err!(Unsupported, "VMX lacks I/O exit instruction info"); } if !vmx_basic.vmx_flex_controls { - return x86_err!(Unsupported); + return x86_err!(Unsupported, "VMX lacks flexible controls"); } self.vmcs_revision_id = vmx_basic.revision_id; self.vmx_region = VmxRegion::::new(self.vmcs_revision_id, false)?; unsafe { - // Enable VMX using the VMXE bit. - Cr4::write(Cr4::read() | Cr4Flags::VIRTUAL_MACHINE_EXTENSIONS); + Cr0::write_raw(cr0_vmx); + Cr4::write_raw(cr4_vmx); // Execute VMXON. - vmx::vmxon(self.vmx_region.phys_addr().as_usize() as _).map_err(|err| { - x86_err_type!( + if let Err(err) = vmx::vmxon(self.vmx_region.phys_addr().as_usize() as _) { + Cr4::write_raw(cr4); + Cr0::write_raw(cr0); + return Err(x86_err_type!( BadState, format_args!("VMX instruction vmxon failed: {:?}", err) - ) - })?; + )); + } } + self.host_cr0_cr4 = Some((cr0, cr4)); info!("[AxVM] succeeded to turn on VMX."); Ok(()) @@ -153,8 +167,14 @@ impl VmxPerCpuState { format_args!("VMX instruction vmxoff failed: {:?}", err) ) })?; - // Remove VMXE bit in CR4. - Cr4::update(|cr4| cr4.remove(Cr4Flags::VIRTUAL_MACHINE_EXTENSIONS)); + if let Some((cr0, cr4)) = self.host_cr0_cr4.take() { + Cr4::write_raw(cr4); + Cr0::write_raw(cr0); + } else { + // Fall back to the previous behavior for a partially initialized + // state where the original control registers were not recorded. + Cr4::update(|cr4| cr4.remove(Cr4Flags::VIRTUAL_MACHINE_EXTENSIONS)); + } }; info!("[AxVM] succeeded to turn off VMX."); @@ -163,6 +183,14 @@ impl VmxPerCpuState { } } +fn vmx_fixed_control_value(value: u64, fixed0: u64, fixed1: u64) -> u64 { + (value | fixed0) & fixed1 +} + +fn is_vmx_fixed_control_value_valid(value: u64, fixed0: u64, fixed1: u64) -> bool { + (value & fixed0) == fixed0 && (value & !fixed1) == 0 +} + #[cfg(test)] mod tests { use alloc::{format, vec::Vec}; diff --git a/virtualization/x86_vcpu/src/vmx/vcpu.rs b/virtualization/x86_vcpu/src/vmx/vcpu.rs index 07398f5db8..f6e37e6289 100644 --- a/virtualization/x86_vcpu/src/vmx/vcpu.rs +++ b/virtualization/x86_vcpu/src/vmx/vcpu.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use alloc::collections::VecDeque; +use alloc::{collections::VecDeque, vec::Vec}; use core::{ arch::naked_asm, fmt::{Debug, Formatter, Result}, @@ -40,9 +40,9 @@ use super::{ }, }; use crate::{ - X86AccessFlags, X86AccessWidth, X86GuestPhysAddr, X86GuestVirtAddr, X86HostOps, - X86HostPhysAddr, X86MsrAddr, X86NestedPageFaultInfo, X86NestedPagingConfig, X86Port, - X86VCpuCreateConfig, X86VCpuSetupConfig, X86VcpuError, X86VcpuResult, X86VmExit, + X86AccessFlags, X86AccessWidth, X86GuestMemoryRegion, X86GuestPhysAddr, X86GuestVirtAddr, + X86HostOps, X86HostPhysAddr, X86MsrAddr, X86NestedPageFaultInfo, X86NestedPagingConfig, + X86Port, X86VCpuCreateConfig, X86VCpuSetupConfig, X86VcpuError, X86VcpuResult, X86VmExit, ept::GuestPageWalkInfo, host, msr::Msr, regs::GeneralRegisters, restore_host_interrupt_flag, x86_real_mode_entry_state, xstate::XState, }; @@ -127,6 +127,8 @@ pub struct VmxVcpu { vlapic: EmulatedLocalApic, /// Guest CR2 is not saved or restored by VMX hardware. guest_cr2: usize, + /// Guest RAM regions used to read guest instructions and page tables. + guest_memory_regions: Vec, // Extra states /// The XState of the VCpu. Both host and guest. @@ -156,6 +158,7 @@ impl VmxVcpu { pending_events: VecDeque::with_capacity(8), vlapic: EmulatedLocalApic::::new(vm_id, vcpu_id), guest_cr2: 0, + guest_memory_regions: Vec::new(), xstate: XState::new(), #[cfg(feature = "tracing")] guest_regs_exiting: GeneralRegisters::default(), @@ -519,6 +522,7 @@ impl VmxVcpu { nested_page_table_root: X86HostPhysAddr, config: X86VCpuSetupConfig, ) -> X86VcpuResult { + self.guest_memory_regions = config.guest_memory_regions.clone(); let paddr = self.vmcs.phys_addr().as_usize() as u64; unsafe { vmx::vmclear(paddr).map_err(as_axerr)?; @@ -633,7 +637,7 @@ impl VmxVcpu { is_guest: bool, config: X86VCpuSetupConfig, ) -> X86VcpuResult { - // Intercept NMI and external interrupts. + // Intercept external interrupts and use the VMX preemption timer. use PinbasedControls as PinCtrl; use super::vmcs::controls::*; @@ -643,10 +647,7 @@ impl VmxVcpu { VmcsControl32::PINBASED_EXEC_CONTROLS, Msr::IA32_VMX_TRUE_PINBASED_CTLS, Msr::IA32_VMX_PINBASED_CTLS.read() as u32, - (PinCtrl::NMI_EXITING - | PinCtrl::EXTERNAL_INTERRUPT_EXITING - | PinCtrl::VMX_PREEMPTION_TIMER) - .bits(), + (PinCtrl::EXTERNAL_INTERRUPT_EXITING | PinCtrl::VMX_PREEMPTION_TIMER).bits(), 0, )?; @@ -751,8 +752,8 @@ impl VmxVcpu { // VmcsControlNW::CR4_GUEST_HOST_MASK.write(0)?; VmcsControl32::CR3_TARGET_COUNT.write(0)?; - // Pass-through exceptions (except #UD(6)), don't use I/O bitmap, set MSR bitmaps. - let exception_bitmap: u32 = 1 << 6; + // Pass through guest exceptions. Linux uses #UD (`ud2`) for BUG/WARN handling. + let exception_bitmap: u32 = 0; self.setup_io_bitmap(config)?; @@ -852,18 +853,6 @@ impl VmxVcpu { } } -// The current VMX APIC-access decode path is used only with Axvisor's -// identity-mapped guest RAM layout, so the guest physical address is also -// the host physical address. A non-identity guest memory backend should -// replace this helper with an explicit GPA-to-HVA translation. -fn read_guest_phys_u64(gpa: usize) -> X86VcpuResult { - let mut bytes = [0u8; 8]; - for (offset, byte) in bytes.iter_mut().enumerate() { - *byte = host::read_guest_u8::(X86GuestPhysAddr::from_usize(gpa + offset))?; - } - Ok(u64::from_le_bytes(bytes)) -} - /// Get ready then vmlaunch or vmresume. macro_rules! vmx_entry_with { ($instr:literal) => { @@ -1321,7 +1310,49 @@ impl VmxVcpu { fn read_guest_u8(&self, gva: X86GuestVirtAddr) -> X86VcpuResult { let gpa = self.translate_guest_linear(gva)?; - host::read_guest_u8::(gpa) + self.read_guest_phys_u8(gpa) + } + + fn guest_phys_to_host_virt(&self, gpa: X86GuestPhysAddr) -> X86VcpuResult { + let gpa = gpa.as_usize(); + self.guest_memory_regions + .iter() + .find_map(|region| { + let start = region.gpa.as_usize(); + let offset = gpa.checked_sub(start)?; + if offset < region.size { + region.hva.as_usize().checked_add(offset) + } else { + None + } + }) + .ok_or_else(|| { + x86_err_type!( + InvalidInput, + format_args!("guest physical address {gpa:#x} is not backed by RAM") + ) + }) + } + + fn read_guest_phys_u8(&self, gpa: X86GuestPhysAddr) -> X86VcpuResult { + if self.guest_memory_regions.is_empty() { + return host::read_guest_u8::(gpa); + } + + let hva = self.guest_phys_to_host_virt(gpa)?; + Ok(unsafe { core::ptr::read_volatile(hva as *const u8) }) + } + + fn read_guest_phys_u64(&self, gpa: X86GuestPhysAddr) -> X86VcpuResult { + let mut bytes = [0u8; 8]; + for (offset, byte) in bytes.iter_mut().enumerate() { + let addr = gpa + .as_usize() + .checked_add(offset) + .ok_or(X86VcpuError::InvalidInput)?; + *byte = self.read_guest_phys_u8(X86GuestPhysAddr::from_usize(addr))?; + } + Ok(u64::from_le_bytes(bytes)) } fn translate_guest_linear(&self, gva: X86GuestVirtAddr) -> X86VcpuResult { @@ -1353,7 +1384,8 @@ impl VmxVcpu { ]; for (level, index) in indexes.into_iter().enumerate() { - let entry = read_guest_phys_u64::(table + index * size_of::())?; + let entry = + self.read_guest_phys_u64(X86GuestPhysAddr::from(table + index * size_of::()))?; if entry & PRESENT == 0 { return x86_err!( InvalidInput,