From 88d8d37d9796c0fd176e0619e88377d599933229 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Wed, 29 Apr 2026 19:04:41 +0200 Subject: [PATCH 1/6] arch/vmm: add PVH boot protocol support for FreeBSD x86_64 Implement PVH (Para-Virtualized Hardware) boot protocol support, enabling FreeBSD x86_64 kernels to boot via the Xen PVH ABI. Key changes: - arch/Cargo.toml: add linux-loader (elf feature) as x86_64 dep - layout.rs: add PVH_INFO_START, MODLIST_START, MEMMAP_START, RSDP_ADDR - mptable.rs: export MPTABLE_START as pub - gdt.rs: fix get_limit() to apply granularity (G) bit expansion - mod.rs: add configure_pvh(), add_memmap_entry(); refactor configure_system() to dispatch to configure_pvh() or configure_64bit_boot() based on pvh flag; add pvh param - regs.rs: add pvh param to setup_regs/setup_sregs/ configure_segments_and_sregs; set 32-bit protected mode GDT and CR0=PE|ET for PVH; set rbx=PVH_INFO_START per PVH ABI - vstate.rs: thread pvh through configure_x86_64() - builder.rs: detect PvhEntryPresent in ELF load result; propagate pvh through PayloadConfig, load_payload, create_vcpus_x86_64 - lib.rs: add pvh param to Vmm::configure_system() Co-authored-by: Dmitrii Sharshakov Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jan Noha --- src/arch/Cargo.toml | 3 + src/arch/src/x86_64/gdt.rs | 11 ++- src/arch/src/x86_64/layout.rs | 15 ++++ src/arch/src/x86_64/mod.rs | 140 ++++++++++++++++++++++++++++++--- src/arch/src/x86_64/mptable.rs | 2 +- src/arch/src/x86_64/regs.rs | 91 +++++++++++++-------- src/vmm/src/builder.rs | 37 ++++++--- src/vmm/src/lib.rs | 2 + src/vmm/src/linux/vstate.rs | 11 +-- 9 files changed, 254 insertions(+), 58 deletions(-) diff --git a/src/arch/Cargo.toml b/src/arch/Cargo.toml index a60963abb..13f7a453d 100644 --- a/src/arch/Cargo.toml +++ b/src/arch/Cargo.toml @@ -26,5 +26,8 @@ kvm-bindings = { version = "0.12", features = ["fam-wrappers"] } kvm-ioctls = "0.22" tdx = { version = "0.1.0", optional = true } +[target.'cfg(target_arch = "x86_64")'.dependencies] +linux-loader = { version = "0.13.2", features = ["elf"] } + [dev-dependencies] utils = { package = "krun-utils", version = "=0.1.0-1.18.0", path = "../utils" } diff --git a/src/arch/src/x86_64/gdt.rs b/src/arch/src/x86_64/gdt.rs index c7fcbf31b..9ba1be436 100644 --- a/src/arch/src/x86_64/gdt.rs +++ b/src/arch/src/x86_64/gdt.rs @@ -25,7 +25,14 @@ fn get_base(entry: u64) -> u64 { } fn get_limit(entry: u64) -> u32 { - ((((entry) & 0x000F_0000_0000_0000) >> 32) | ((entry) & 0x0000_0000_0000_FFFF)) as u32 + let limit = + ((((entry) & 0x000F_0000_0000_0000) >> 32) | ((entry) & 0x0000_0000_0000_FFFF)) as u32; + + if get_g(entry) == 1 { + (limit << 12) | 0xFFF + } else { + limit + } } fn get_g(entry: u64) -> u8 { @@ -109,7 +116,7 @@ mod tests { assert_eq!(0xB, seg.type_); // base and limit assert_eq!(0x10_0000, seg.base); - assert_eq!(0xfffff, seg.limit); + assert_eq!(0xffffffff, seg.limit); assert_eq!(0x0, seg.unusable); } } diff --git a/src/arch/src/x86_64/layout.rs b/src/arch/src/x86_64/layout.rs index c626d68d7..fe3aad0a8 100644 --- a/src/arch/src/x86_64/layout.rs +++ b/src/arch/src/x86_64/layout.rs @@ -31,6 +31,21 @@ pub const IRQ_MAX: u32 = 15; /// Address for the TSS setup. pub const KVM_TSS_ADDRESS: u64 = 0xfffb_d000; +/// Address of the hvm_start_info struct used in PVH boot. +/// Mutually exclusive with SNP_CPUID_START (TEE only). +pub const PVH_INFO_START: u64 = 0x6000; + +/// Starting address of array of modules of hvm_modlist_entry type. +/// Used to enable initrd support using the PVH boot ABI. +pub const MODLIST_START: u64 = 0x6040; + +/// Address of memory map table used in PVH boot. Can overlap +/// with the zero page address since they are mutually exclusive. +pub const MEMMAP_START: u64 = 0x7000; + +/// Location of RSDP pointer in x86 machines. +pub const RSDP_ADDR: u64 = 0x000e_0000; + /// The 'zero page', a.k.a linux kernel bootparams. pub const ZERO_PAGE_START: u64 = 0x7000; diff --git a/src/arch/src/x86_64/mod.rs b/src/arch/src/x86_64/mod.rs index 7c4b6c83d..6598143e5 100644 --- a/src/arch/src/x86_64/mod.rs +++ b/src/arch/src/x86_64/mod.rs @@ -21,11 +21,18 @@ use crate::x86_64::layout::{EBDA_START, FIRST_ADDR_PAST_32BITS, MMIO_MEM_START}; #[cfg(feature = "tee")] use crate::x86_64::layout::{FIRMWARE_SIZE, FIRMWARE_START}; use crate::{ArchMemoryInfo, InitrdConfig}; -use arch_gen::x86::bootparam::{boot_params, E820_RAM}; +use arch_gen::x86::bootparam::{boot_params, E820_RAM, E820_RESERVED}; use vm_memory::Bytes; use vm_memory::{Address, ByteValued, GuestAddress, GuestMemoryMmap}; use vmm_sys_util::align_upwards; +#[cfg(not(feature = "tee"))] +use linux_loader::configurator::{pvh::PvhBootConfigurator, BootConfigurator, BootParams}; +#[cfg(not(feature = "tee"))] +use linux_loader::loader::elf::start_info::{ + hvm_memmap_table_entry, hvm_modlist_entry, hvm_start_info, +}; + // This is a workaround to the Rust enforcement specifying that any implementation of a foreign // trait (in this case `ByteValued`) where: // * the type that is implementing the trait is foreign or @@ -45,6 +52,9 @@ pub enum Error { /// Error writing MP table to memory. #[cfg(not(feature = "tee"))] MpTableSetup(mptable::Error), + /// Error writing hvm_start_info to guest memory. + #[cfg(not(feature = "tee"))] + StartInfoSetup, /// Error writing the zero page of guest memory. ZeroPageSetup, /// Failed to compute initrd address. @@ -245,6 +255,7 @@ pub fn arch_memory_regions( /// * `cmdline_size` - Size of the kernel command line in bytes including the null terminator. /// * `initrd` - Information about where the ramdisk image was loaded in the `guest_mem`. /// * `num_cpus` - Number of virtual CPUs the guest will have. +/// * `pvh` - Whether to use the PVH boot protocol. #[allow(unused_variables)] pub fn configure_system( guest_mem: &GuestMemoryMmap, @@ -253,6 +264,121 @@ pub fn configure_system( cmdline_size: usize, initrd: &Option, num_cpus: u8, + pvh: bool, +) -> super::Result<()> { + // Note that this puts the mptable at the last 1k of Linux's 640k base RAM + #[cfg(not(feature = "tee"))] + mptable::setup_mptable(guest_mem, num_cpus).map_err(Error::MpTableSetup)?; + + if pvh { + #[cfg(not(feature = "tee"))] + configure_pvh(guest_mem, arch_memory_info, cmdline_addr, initrd)?; + } else { + configure_64bit_boot( + guest_mem, + arch_memory_info, + cmdline_addr, + cmdline_size, + initrd, + num_cpus, + )?; + } + Ok(()) +} + +#[cfg(not(feature = "tee"))] +fn configure_pvh( + guest_mem: &GuestMemoryMmap, + arch_memory_info: &ArchMemoryInfo, + cmdline_addr: GuestAddress, + initrd: &Option, +) -> Result<(), Error> { + const XEN_HVM_START_MAGIC_VALUE: u32 = 0x336e_c578; + let first_addr_past_32bits = GuestAddress(FIRST_ADDR_PAST_32BITS); + let end_32bit_gap_start = GuestAddress(MMIO_MEM_START); + let himem_start = GuestAddress(layout::HIMEM_START); + let mut modules: Vec = Vec::new(); + if let Some(initrd_config) = initrd { + modules.push(hvm_modlist_entry { + paddr: initrd_config.address.raw_value(), + size: initrd_config.size as u64, + ..Default::default() + }); + } + let mut memmap: Vec = Vec::new(); + add_memmap_entry(&mut memmap, 0, mptable::MPTABLE_START, E820_RAM); + add_memmap_entry( + &mut memmap, + mptable::MPTABLE_START, + layout::RSDP_ADDR - mptable::MPTABLE_START, + E820_RESERVED, + ); + let last_addr = GuestAddress(arch_memory_info.ram_last_addr); + if last_addr < end_32bit_gap_start { + add_memmap_entry( + &mut memmap, + himem_start.raw_value(), + last_addr.unchecked_offset_from(himem_start) + 1, + E820_RAM, + ); + } else { + add_memmap_entry( + &mut memmap, + himem_start.raw_value(), + end_32bit_gap_start.unchecked_offset_from(himem_start), + E820_RAM, + ); + if last_addr > first_addr_past_32bits { + add_memmap_entry( + &mut memmap, + first_addr_past_32bits.raw_value(), + last_addr.unchecked_offset_from(first_addr_past_32bits) + 1, + E820_RAM, + ); + } + } + let mut start_info = hvm_start_info { + magic: XEN_HVM_START_MAGIC_VALUE, + version: 1, + cmdline_paddr: cmdline_addr.raw_value(), + memmap_paddr: layout::MEMMAP_START, + memmap_entries: memmap.len() as u32, + nr_modules: modules.len() as u32, + ..Default::default() + }; + if !modules.is_empty() { + start_info.modlist_paddr = layout::MODLIST_START; + } + let mut boot_params = + BootParams::new::(&start_info, GuestAddress(layout::PVH_INFO_START)); + boot_params.set_sections::(&memmap, GuestAddress(layout::MEMMAP_START)); + boot_params.set_modules::(&modules, GuestAddress(layout::MODLIST_START)); + PvhBootConfigurator::write_bootparams(&boot_params, guest_mem) + .map_err(|_| Error::StartInfoSetup) +} + +#[cfg(not(feature = "tee"))] +fn add_memmap_entry( + memmap: &mut Vec, + addr: u64, + size: u64, + mem_type: u32, +) { + memmap.push(hvm_memmap_table_entry { + addr, + size, + type_: mem_type, + reserved: 0, + }); +} + +fn configure_64bit_boot( + guest_mem: &GuestMemoryMmap, + arch_memory_info: &ArchMemoryInfo, + cmdline_addr: GuestAddress, + cmdline_size: usize, + initrd: &Option, + #[allow(unused_variables)] num_cpus: u8, ) -> super::Result<()> { const KERNEL_BOOT_FLAG_MAGIC: u16 = 0xaa55; const KERNEL_HDR_MAGIC: u32 = 0x5372_6448; @@ -263,10 +389,6 @@ pub fn configure_system( let himem_start = GuestAddress(layout::HIMEM_START); - // Note that this puts the mptable at the last 1k of Linux's 640k base RAM - #[cfg(not(feature = "tee"))] - mptable::setup_mptable(guest_mem, num_cpus).map_err(Error::MpTableSetup)?; - let mut params: BootParamsWrapper = BootParamsWrapper(boot_params::default()); params.0.hdr.type_of_loader = KERNEL_LOADER_OTHER; @@ -401,7 +523,7 @@ mod tests { let no_vcpus = 4; let gm = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x10000)]).unwrap(); let info = ArchMemoryInfo::default(); - let config_err = configure_system(&gm, &info, GuestAddress(0), 0, &None, 1); + let config_err = configure_system(&gm, &info, GuestAddress(0), 0, &None, 1, false); assert!(config_err.is_err()); #[cfg(not(feature = "tee"))] assert_eq!( @@ -414,21 +536,21 @@ mod tests { let (arch_mem_info, arch_mem_regions) = arch_memory_regions(mem_size, Some(KERNEL_LOAD_ADDR), KERNEL_SIZE, 0, None); let gm = GuestMemoryMmap::from_ranges(&arch_mem_regions).unwrap(); - configure_system(&gm, &arch_mem_info, GuestAddress(0), 0, &None, no_vcpus).unwrap(); + configure_system(&gm, &arch_mem_info, GuestAddress(0), 0, &None, no_vcpus, false).unwrap(); // Now assigning some memory that is equal to the start of the 32bit memory hole. let mem_size = 3328 << 20; let (arch_mem_info, arch_mem_regions) = arch_memory_regions(mem_size, Some(KERNEL_LOAD_ADDR), KERNEL_SIZE, 0, None); let gm = GuestMemoryMmap::from_ranges(&arch_mem_regions).unwrap(); - configure_system(&gm, &arch_mem_info, GuestAddress(0), 0, &None, no_vcpus).unwrap(); + configure_system(&gm, &arch_mem_info, GuestAddress(0), 0, &None, no_vcpus, false).unwrap(); // Now assigning some memory that falls after the 32bit memory hole. let mem_size = 3330 << 20; let (arch_mem_info, arch_mem_regions) = arch_memory_regions(mem_size, Some(KERNEL_LOAD_ADDR), KERNEL_SIZE, 0, None); let gm = GuestMemoryMmap::from_ranges(&arch_mem_regions).unwrap(); - configure_system(&gm, &arch_mem_info, GuestAddress(0), 0, &None, no_vcpus).unwrap(); + configure_system(&gm, &arch_mem_info, GuestAddress(0), 0, &None, no_vcpus, false).unwrap(); } #[test] diff --git a/src/arch/src/x86_64/mptable.rs b/src/arch/src/x86_64/mptable.rs index 1b4511453..bedda5a26 100644 --- a/src/arch/src/x86_64/mptable.rs +++ b/src/arch/src/x86_64/mptable.rs @@ -44,7 +44,7 @@ unsafe impl ByteValued for MpcLintsrcWrapper {} unsafe impl ByteValued for MpfIntelWrapper {} // MPTABLE, describing VCPUS. -const MPTABLE_START: u64 = 0x9fc00; +pub const MPTABLE_START: u64 = 0x9fc00; #[derive(Debug, Eq, PartialEq)] pub enum Error { diff --git a/src/arch/src/x86_64/regs.rs b/src/arch/src/x86_64/regs.rs index 06378c593..ffd293c1a 100644 --- a/src/arch/src/x86_64/regs.rs +++ b/src/arch/src/x86_64/regs.rs @@ -62,20 +62,31 @@ pub fn setup_fpu(vcpu: &VcpuFd) -> Result<()> { /// /// * `vcpu` - Structure for the VCPU that holds the VCPU's fd. /// * `boot_ip` - Starting instruction pointer. -pub fn setup_regs(vcpu: &VcpuFd, boot_ip: u64, id: u8) -> Result<()> { +/// * `pvh` - Whether to use the PVH boot protocol. +pub fn setup_regs(vcpu: &VcpuFd, boot_ip: u64, id: u8, pvh: bool) -> Result<()> { let regs: kvm_regs = if id == 0 || cfg!(not(feature = "tee")) { - kvm_regs { - rflags: 0x0000_0000_0000_0002u64, - rip: boot_ip, - // Frame pointer. It gets a snapshot of the stack pointer (rsp) so that when adjustments are - // made to rsp (i.e. reserving space for local variables or pushing values on to the stack), - // local variables and function parameters are still accessible from a constant offset from rbp. - rsp: super::layout::BOOT_STACK_POINTER, - // Starting stack pointer. - rbp: super::layout::BOOT_STACK_POINTER, - // Must point to zero page address per Linux ABI. This is x86_64 specific. - rsi: super::layout::ZERO_PAGE_START, - ..Default::default() + if pvh { + kvm_regs { + rflags: 0x0000_0000_0000_0002u64, + rip: boot_ip, + // PVH ABI: rbx points to hvm_start_info + rbx: super::layout::PVH_INFO_START, + ..Default::default() + } + } else { + kvm_regs { + rflags: 0x0000_0000_0000_0002u64, + rip: boot_ip, + // Frame pointer. It gets a snapshot of the stack pointer (rsp) so that when adjustments are + // made to rsp (i.e. reserving space for local variables or pushing values on to the stack), + // local variables and function parameters are still accessible from a constant offset from rbp. + rsp: super::layout::BOOT_STACK_POINTER, + // Starting stack pointer. + rbp: super::layout::BOOT_STACK_POINTER, + // Must point to zero page address per Linux ABI. This is x86_64 specific. + rsi: super::layout::ZERO_PAGE_START, + ..Default::default() + } } } else { kvm_regs { @@ -94,12 +105,15 @@ pub fn setup_regs(vcpu: &VcpuFd, boot_ip: u64, id: u8) -> Result<()> { /// /// * `mem` - The memory that will be passed to the guest. /// * `vcpu` - Structure for the VCPU that holds the VCPU's fd. -pub fn setup_sregs(mem: &GuestMemoryMmap, vcpu: &VcpuFd, id: u8) -> Result<()> { +/// * `pvh` - Whether to use the PVH boot protocol. +pub fn setup_sregs(mem: &GuestMemoryMmap, vcpu: &VcpuFd, id: u8, pvh: bool) -> Result<()> { let mut sregs: kvm_sregs = vcpu.get_sregs().map_err(Error::GetStatusRegisters)?; if cfg!(not(feature = "tee")) { - configure_segments_and_sregs(mem, &mut sregs)?; - setup_page_tables(mem, &mut sregs)?; // TODO(dgreid) - Can this be done once per system instead + configure_segments_and_sregs(mem, &mut sregs, pvh)?; + if !pvh { + setup_page_tables(mem, &mut sregs)?; // TODO(dgreid) - Can this be done once per system instead + } } else if id != 0 { //sregs.cs.selector = 0x9100; //sregs.cs.base = 0x91000; @@ -116,6 +130,7 @@ const BOOT_GDT_MAX: usize = 4; const EFER_LMA: u64 = 0x400; const EFER_LME: u64 = 0x100; +const X86_CR0_ET: u64 = 0x10; const X86_CR0_PE: u64 = 0x1; const X86_CR0_PG: u64 = 0x8000_0000; const X86_CR4_PAE: u64 = 0x20; @@ -140,13 +155,22 @@ fn write_idt_value(val: u64, guest_mem: &GuestMemoryMmap) -> Result<()> { .map_err(|_| Error::WriteIDT) } -fn configure_segments_and_sregs(mem: &GuestMemoryMmap, sregs: &mut kvm_sregs) -> Result<()> { - let gdt_table: [u64; BOOT_GDT_MAX] = [ - gdt_entry(0, 0, 0), // NULL - gdt_entry(0xa09b, 0, 0xfffff), // CODE - gdt_entry(0xc093, 0, 0xfffff), // DATA - gdt_entry(0x808b, 0, 0xfffff), // TSS - ]; +fn configure_segments_and_sregs(mem: &GuestMemoryMmap, sregs: &mut kvm_sregs, pvh: bool) -> Result<()> { + let gdt_table: [u64; BOOT_GDT_MAX] = if pvh { + [ + gdt_entry(0, 0, 0), // NULL + gdt_entry(0xc09b, 0, 0xffff_ffff), // CODE (32-bit protected mode) + gdt_entry(0xc093, 0, 0xffff_ffff), // DATA + gdt_entry(0x008b, 0, 0x67), // TSS + ] + } else { + [ + gdt_entry(0, 0, 0), // NULL + gdt_entry(0xa09b, 0, 0xfffff), // CODE (64-bit long mode) + gdt_entry(0xc093, 0, 0xfffff), // DATA + gdt_entry(0x808b, 0, 0xfffff), // TSS + ] + }; let code_seg = kvm_segment_from_gdt(gdt_table[1], 1); let data_seg = kvm_segment_from_gdt(gdt_table[2], 2); @@ -169,9 +193,14 @@ fn configure_segments_and_sregs(mem: &GuestMemoryMmap, sregs: &mut kvm_sregs) -> sregs.ss = data_seg; sregs.tr = tss_seg; - /* 64-bit protected mode */ - sregs.cr0 |= X86_CR0_PE; - sregs.efer |= EFER_LME | EFER_LMA; + if pvh { + sregs.cr0 = X86_CR0_PE | X86_CR0_ET; + sregs.cr4 = 0; + } else { + /* 64-bit protected mode */ + sregs.cr0 |= X86_CR0_PE; + sregs.efer |= EFER_LME | EFER_LMA; + } Ok(()) } @@ -225,13 +254,13 @@ mod tests { assert_eq!(0x0, read_u64(gm, BOOT_IDT_OFFSET)); assert_eq!(0, sregs.cs.base); - assert_eq!(0xfffff, sregs.ds.limit); + assert_eq!(0xffffffff, sregs.ds.limit); assert_eq!(0x10, sregs.es.selector); assert_eq!(1, sregs.fs.present); assert_eq!(1, sregs.gs.g); assert_eq!(0, sregs.ss.avl); assert_eq!(0, sregs.tr.base); - assert_eq!(0xfffff, sregs.tr.limit); + assert_eq!(0xffffffff, sregs.tr.limit); assert_eq!(0, sregs.tr.avl); assert!(sregs.cr0 & X86_CR0_PE != 0); assert!(sregs.efer & EFER_LME != 0 && sregs.efer & EFER_LMA != 0); @@ -241,7 +270,7 @@ mod tests { fn test_configure_segments_and_sregs() { let mut sregs: kvm_sregs = Default::default(); let gm = create_guest_mem(); - configure_segments_and_sregs(&gm, &mut sregs).unwrap(); + configure_segments_and_sregs(&gm, &mut sregs, false).unwrap(); validate_segments_and_sregs(&gm, &sregs); } @@ -304,7 +333,7 @@ mod tests { ..Default::default() }; - setup_regs(&vcpu, expected_regs.rip, 1).unwrap(); + setup_regs(&vcpu, expected_regs.rip, 1, false).unwrap(); let actual_regs: kvm_regs = vcpu.get_regs().unwrap(); assert_eq!(actual_regs, expected_regs); @@ -318,7 +347,7 @@ mod tests { let gm = create_guest_mem(); assert!(vcpu.set_sregs(&Default::default()).is_ok()); - setup_sregs(&gm, &vcpu, 1).unwrap(); + setup_sregs(&gm, &vcpu, 1, false).unwrap(); let mut sregs: kvm_sregs = vcpu.get_sregs().unwrap(); // for AMD KVM_GET_SREGS returns g = 0 for each kvm_segment. diff --git a/src/vmm/src/builder.rs b/src/vmm/src/builder.rs index b92b931d4..35a890a5b 100644 --- a/src/vmm/src/builder.rs +++ b/src/vmm/src/builder.rs @@ -834,6 +834,7 @@ pub fn build_microvm( &pio_device_manager.io_bus, &exit_evt, kernel_boot, + payload_config.pvh, #[cfg(feature = "tee")] _sender, ) @@ -1098,6 +1099,7 @@ pub fn build_microvm( &intc, &payload_config.initrd_config, &vm_resources.smbios_oem_strings, + payload_config.pvh, ) .map_err(StartMicrovmError::Internal)?; @@ -1153,7 +1155,9 @@ fn load_external_kernel( guest_mem: &GuestMemoryMmap, arch_mem_info: &ArchMemoryInfo, external_kernel: &ExternalKernel, -) -> std::result::Result<(GuestAddress, Option, Option), StartMicrovmError> { +) -> std::result::Result<(GuestAddress, Option, Option, bool), StartMicrovmError> { + #[allow(unused_mut)] + let mut pvh = false; let entry_addr = match external_kernel.format { // Raw images are treated as bundled kernels on x86_64 #[cfg(target_arch = "x86_64")] @@ -1174,7 +1178,13 @@ fn load_external_kernel( .map_err(StartMicrovmError::ElfOpenKernel)?; let load_result = loader::Elf::load(guest_mem, None, &mut file, None) .map_err(StartMicrovmError::ElfLoadKernel)?; - load_result.kernel_load + match load_result.pvh_boot_cap { + loader::PvhBootCapability::PvhEntryPresent(guest_address) => { + pvh = true; + guest_address + } + _ => load_result.kernel_load, + } } #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] KernelFormat::PeGz => { @@ -1292,7 +1302,7 @@ fn load_external_kernel( None }; - Ok((entry_addr, initrd_config, external_kernel.cmdline.clone())) + Ok((entry_addr, initrd_config, external_kernel.cmdline.clone(), pvh)) } fn load_payload( @@ -1306,6 +1316,7 @@ fn load_payload( GuestAddress, Option, Option, + bool, ), StartMicrovmError, > { @@ -1335,7 +1346,7 @@ fn load_payload( guest_mem .write(kernel_data, GuestAddress(kernel_guest_addr)) .unwrap(); - Ok((guest_mem, GuestAddress(kernel_entry_addr), None, None)) + Ok((guest_mem, GuestAddress(kernel_entry_addr), None, None, false)) } #[cfg(all(target_arch = "x86_64", not(feature = "tee")))] Payload::KernelMmap => { @@ -1437,15 +1448,16 @@ fn load_payload( GuestAddress(kernel_entry_addr), None, None, + false, )) } Payload::ExternalKernel(external_kernel) => { - let (entry_addr, initrd_config, cmdline) = + let (entry_addr, initrd_config, cmdline, pvh) = load_external_kernel(&guest_mem, _arch_mem_info, external_kernel)?; - Ok((guest_mem, entry_addr, initrd_config, cmdline)) + Ok((guest_mem, entry_addr, initrd_config, cmdline, pvh)) } #[cfg(test)] - Payload::Empty => Ok((guest_mem, GuestAddress(0), None, None)), + Payload::Empty => Ok((guest_mem, GuestAddress(0), None, None, false)), #[cfg(feature = "tee")] Payload::Tee => { let (kernel_host_addr, kernel_guest_addr, kernel_size) = @@ -1498,9 +1510,10 @@ fn load_payload( GuestAddress(arch::RESET_VECTOR), Some(initrd_config), None, + false, )) } - Payload::Firmware => Ok((guest_mem, GuestAddress(arch::RESET_VECTOR), None, None)), + Payload::Firmware => Ok((guest_mem, GuestAddress(arch::RESET_VECTOR), None, None, false)), } } @@ -1508,6 +1521,7 @@ pub struct PayloadConfig { entry_addr: GuestAddress, initrd_config: Option, kernel_cmdline: Option, + pvh: bool, } pub fn create_guest_memory( @@ -1652,7 +1666,7 @@ pub fn create_guest_memory( .map_err(|e| StartMicrovmError::GuestMemoryMmap(format!("{e:?}")))? }; - let (guest_mem, entry_addr, initrd_config, cmdline) = + let (guest_mem, entry_addr, initrd_config, cmdline, pvh) = load_payload(vm_resources, guest_mem, &arch_mem_info, payload)?; // Only write firmware if data exists AND this isn't an ExternalKernel payload @@ -1669,6 +1683,7 @@ pub fn create_guest_memory( entry_addr, initrd_config, kernel_cmdline: cmdline.clone(), + pvh, }; Ok((guest_mem, arch_mem_info, shm_manager, payload_config)) @@ -1875,6 +1890,7 @@ fn create_vcpus_x86_64( io_bus: &devices::Bus, exit_evt: &EventFd, kernel_boot: bool, + pvh: bool, #[cfg(feature = "tee")] pm_sender: Sender, ) -> super::Result> { let mut vcpus = Vec::with_capacity(vcpu_config.vcpu_count as usize); @@ -1891,7 +1907,7 @@ fn create_vcpus_x86_64( ) .map_err(Error::Vcpu)?; - vcpu.configure_x86_64(guest_mem, entry_addr, vcpu_config, kernel_boot) + vcpu.configure_x86_64(guest_mem, entry_addr, vcpu_config, kernel_boot, pvh) .map_err(Error::Vcpu)?; vcpus.push(vcpu); @@ -2556,6 +2572,7 @@ pub mod tests { &bus, &EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), true, + false, ) .unwrap(); assert_eq!(vcpu_vec.len(), vcpu_count as usize); diff --git a/src/vmm/src/lib.rs b/src/vmm/src/lib.rs index 0f0f8c258..754df2912 100644 --- a/src/vmm/src/lib.rs +++ b/src/vmm/src/lib.rs @@ -274,6 +274,7 @@ impl Vmm { _intc: &IrqChip, initrd: &Option, _smbios_oem_strings: &Option>, + _pvh: bool, ) -> Result<()> { #[cfg(target_arch = "x86_64")] { @@ -290,6 +291,7 @@ impl Vmm { cmdline_len, initrd, vcpus.len() as u8, + _pvh, ) .map_err(Error::ConfigureSystem)?; } diff --git a/src/vmm/src/linux/vstate.rs b/src/vmm/src/linux/vstate.rs index 05e58fbd7..85632a333 100644 --- a/src/vmm/src/linux/vstate.rs +++ b/src/vmm/src/linux/vstate.rs @@ -1170,6 +1170,7 @@ impl Vcpu { kernel_start_addr: GuestAddress, vcpu_config: &VcpuConfig, kernel_boot: bool, + pvh: bool, ) -> Result<()> { let cpuid_vm_spec = VmSpec::new( self.id, @@ -1201,10 +1202,10 @@ impl Vcpu { if kernel_boot { arch::x86_64::msr::setup_msrs(&self.fd).map_err(Error::MSRSConfiguration)?; - arch::x86_64::regs::setup_regs(&self.fd, kernel_start_addr.raw_value(), self.id) + arch::x86_64::regs::setup_regs(&self.fd, kernel_start_addr.raw_value(), self.id, pvh) .map_err(Error::REGSConfiguration)?; arch::x86_64::regs::setup_fpu(&self.fd).map_err(Error::FPUConfiguration)?; - arch::x86_64::regs::setup_sregs(guest_mem, &self.fd, self.id) + arch::x86_64::regs::setup_sregs(guest_mem, &self.fd, self.id, pvh) .map_err(Error::SREGSConfiguration)?; arch::x86_64::interrupts::set_lint(&self.fd).map_err(Error::LocalIntConfiguration)?; } @@ -1904,19 +1905,19 @@ mod tests { }; assert!(vcpu - .configure_x86_64(&vm_mem, GuestAddress(0), &vcpu_config, true) + .configure_x86_64(&vm_mem, GuestAddress(0), &vcpu_config, true, false) .is_ok()); // Test configure while using the T2 template. vcpu_config.cpu_template = Some(CpuFeaturesTemplate::T2); assert!(vcpu - .configure_x86_64(&vm_mem, GuestAddress(0), &vcpu_config, true) + .configure_x86_64(&vm_mem, GuestAddress(0), &vcpu_config, true, false) .is_ok()); // Test configure while using the C3 template. vcpu_config.cpu_template = Some(CpuFeaturesTemplate::C3); assert!(vcpu - .configure_x86_64(&vm_mem, GuestAddress(0), &vcpu_config, true) + .configure_x86_64(&vm_mem, GuestAddress(0), &vcpu_config, true, false) .is_ok()); } From c0da629b43ce83b8d5acfa9eec301ea792a893af Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Apr 2026 19:51:58 +0200 Subject: [PATCH 2/6] tests: add FreeBSD guest support - simple boot test, gvproxy connect and listen tests - fix timing issue in all network listen tests - replace the macOS-only `cfg` gate with a `gvproxy -version` check Signed-off-by: Jan Noha --- Cargo.lock | 1 + Makefile | 13 + tests/README.md | 43 +++ tests/guest-agent/src/main.rs | 8 + tests/run.sh | 152 +++++++++++ tests/test_cases/src/common_freebsd.rs | 248 +++++++++++++++++ tests/test_cases/src/freebsd_guest.rs | 17 ++ tests/test_cases/src/freebsd_network.rs | 256 ++++++++++++++++++ tests/test_cases/src/lib.rs | 26 ++ tests/test_cases/src/tcp_tester.rs | 24 +- tests/test_cases/src/test_freebsd_boot.rs | 57 ++++ .../test_freebsd_gvproxy_tcp_guest_connect.rs | 91 +++++++ .../test_freebsd_gvproxy_tcp_guest_listen.rs | 103 +++++++ tests/test_cases/src/test_net/gvproxy.rs | 161 +++++++---- .../src/test_tsi_tcp_guest_listen.rs | 2 - .../src/test_vsock_guest_connect.rs | 14 +- 16 files changed, 1159 insertions(+), 57 deletions(-) create mode 100644 tests/test_cases/src/common_freebsd.rs create mode 100644 tests/test_cases/src/freebsd_guest.rs create mode 100644 tests/test_cases/src/freebsd_network.rs create mode 100644 tests/test_cases/src/test_freebsd_boot.rs create mode 100644 tests/test_cases/src/test_freebsd_gvproxy_tcp_guest_connect.rs create mode 100644 tests/test_cases/src/test_freebsd_gvproxy_tcp_guest_listen.rs diff --git a/Cargo.lock b/Cargo.lock index ecb90d195..f25b4e5fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -623,6 +623,7 @@ dependencies = [ "kvm-bindings", "kvm-ioctls", "libc", + "linux-loader", "tdx", "vm-memory", "vmm-sys-util 0.14.0", diff --git a/Makefile b/Makefile index 1c16cb199..6f6a3c4fa 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,19 @@ endif ifeq ($(VIRGL_RESOURCE_MAP2),1) FEATURE_FLAGS += --features virgl_resource_map2 endif +# Test targets require the block device (BLK) feature for FreeBSD disk tests +# and the NET feature for gvproxy-based network tests. +# Enable automatically unless the user explicitly set them to a value. +ifeq ($(BLK),) + ifneq ($(filter test test-prefix,$(MAKECMDGOALS)),) + BLK := 1 + endif +endif +ifeq ($(NET),) + ifneq ($(filter test test-prefix,$(MAKECMDGOALS)),) + NET := 1 + endif +endif ifeq ($(BLK),1) FEATURE_FLAGS += --features blk endif diff --git a/tests/README.md b/tests/README.md index fc34374b7..1aca63971 100644 --- a/tests/README.md +++ b/tests/README.md @@ -44,3 +44,46 @@ make test ## Adding tests To add a test you need to add a new rust module in the `test_cases` directory, implement the required host and guest side methods (see existing tests) and register the test in the `test_cases/src/lib.rs` to be ran. + +## FreeBSD guest tests + +FreeBSD guest tests run on Linux (amd64, arm64) and macOS (arm64) hosts. They require two external assets that are not bundled in the repository. + +### Prerequisites + +1. Install required tools: + - **macOS**: `bsdtar` is built-in (`/usr/bin/bsdtar`) + - **Linux**: `sudo apt-get install libarchive-tools` (provides `bsdtar`) + - **Linux/macOS amd64**: add the Rust cross-compilation target: + ```bash + rustup target add x86_64-unknown-freebsd + ``` + - **Linux/macOS arm64**: `aarch64-unknown-freebsd` has no prebuilt stdlib in rustup, + so a nightly toolchain is used with `-Z build-std` to build it from source: + ```bash + rustup toolchain install nightly-2026-01-25 + ``` + +2. Build the FreeBSD sysroot and `init-freebsd` (from the libkrun root directory): + ```bash + make BUILD_BSD_INIT=1 + ``` + This downloads `freebsd-sysroot/base.txz`, extracts it to `freebsd-sysroot/`, and compiles `init/init-freebsd`. + +3. The FreeBSD kernel is downloaded and cached automatically by `run.sh` (from + `download.freebsd.org`). To use a locally-provided kernel instead, set + `KRUN_TEST_FREEBSD_KERNEL_PATH` before running: + ```bash + export KRUN_TEST_FREEBSD_KERNEL_PATH="/path/to/boot/kernel/kernel" # amd64 + export KRUN_TEST_FREEBSD_KERNEL_PATH="/path/to/boot/kernel/kernel.bin" # arm64 + ``` + +### Running FreeBSD tests + +With the sysroot/init assets built, `run.sh` (or `make test`) will automatically: +- Download and cache `target/freebsd-kernel/boot/kernel/kernel[.bin]` if not already present +- Cross-compile the `guest-agent` for FreeBSD +- Build `target/freebsd-test-rootfs.iso` from `init-freebsd` + the FreeBSD `guest-agent` +- Set `KRUN_TEST_FREEBSD_KERNEL_PATH` and `KRUN_TEST_FREEBSD_ISO_PATH` for the runner + +FreeBSD tests are **skipped** (not failed) when the kernel or ISO are unavailable, so the test suite still passes without FreeBSD assets. diff --git a/tests/guest-agent/src/main.rs b/tests/guest-agent/src/main.rs index 668eb9688..8d511f1be 100644 --- a/tests/guest-agent/src/main.rs +++ b/tests/guest-agent/src/main.rs @@ -10,6 +10,14 @@ fn run_guest_agent(test_name: &str) -> anyhow::Result<()> { .context("No such test!")?; let TestCase { test, .. } = test_case; test.in_guest(); + + #[cfg(target_os = "freebsd")] + { + use test_cases::freebsd_guest; + freebsd_guest::halt_vm(); + } + + #[allow(unreachable_code)] Ok(()) } diff --git a/tests/run.sh b/tests/run.sh index 3d7b1e6ef..b6e85e310 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -54,6 +54,100 @@ fi export KRUN_TEST_GUEST_AGENT_PATH="target/$GUEST_TARGET/debug/guest-agent" +# --- FreeBSD guest support --- +FREEBSD_SYSROOT="../freebsd-sysroot" +FREEBSD_INIT="../init/init-freebsd" + +RUST_NIGHTLY="nightly-2026-01-25" + +# Download FreeBSD kernel if KRUN_TEST_FREEBSD_KERNEL_PATH is not already set. +# The kernel binary is cached in target/freebsd-kernel/ and reused on subsequent runs. +if [ -z "${KRUN_TEST_FREEBSD_KERNEL_PATH}" ]; then + FREEBSD_KERNEL_DIR="target/freebsd-kernel" + mkdir -p "${FREEBSD_KERNEL_DIR}" + + if [ "$ARCH" = "x86_64" ]; then + # Use Firecracker-optimized FreeBSD kernel for x86_64 + FREEBSD_KERNEL_URL="https://github.com/acj/freebsd-firecracker/releases/download/v0.8.1/freebsd-kern.bin" + FREEBSD_KERNEL_PATH="${FREEBSD_KERNEL_DIR}/freebsd-kern.bin" + else + # Use upstream FreeBSD kernel for aarch64 + FREEBSD_KERNEL_URL="https://download.freebsd.org/releases/arm64/aarch64/14.4-RELEASE/kernel.txz" + FREEBSD_KERNEL_BIN="kernel.bin" + FREEBSD_KERNEL_PATH="${FREEBSD_KERNEL_DIR}/boot/kernel/${FREEBSD_KERNEL_BIN}" + fi + + if [ ! -f "${FREEBSD_KERNEL_PATH}" ]; then + echo "Downloading FreeBSD kernel..." + FREEBSD_KERNEL_TMP=$(mktemp) + if curl -fL -o "${FREEBSD_KERNEL_TMP}" "${FREEBSD_KERNEL_URL}"; then + if [ "$ARCH" = "x86_64" ]; then + mv "${FREEBSD_KERNEL_TMP}" "${FREEBSD_KERNEL_PATH}" + else + tar xJf "${FREEBSD_KERNEL_TMP}" -C "${FREEBSD_KERNEL_DIR}" \ + "./boot/kernel/${FREEBSD_KERNEL_BIN}" + rm -f "${FREEBSD_KERNEL_TMP}" + fi + else + echo "WARNING: Failed to download FreeBSD kernel; FreeBSD tests will be skipped." + rm -f "${FREEBSD_KERNEL_TMP}" + fi + fi + if [ -f "${FREEBSD_KERNEL_PATH}" ]; then + export KRUN_TEST_FREEBSD_KERNEL_PATH="${FREEBSD_KERNEL_PATH}" + echo "FreeBSD kernel: ${KRUN_TEST_FREEBSD_KERNEL_PATH}" + fi +fi + +if [ -f "${FREEBSD_SYSROOT}/.sysroot_ready" ] && [ -f "${FREEBSD_INIT}" ]; then + FREEBSD_TARGET="${ARCH}-unknown-freebsd" + FREEBSD_SYSROOT_ABS=$(cd "${FREEBSD_SYSROOT}" && pwd) + + # Common FreeBSD linker configuration + export CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_LINKER="clang" + export CARGO_TARGET_AARCH64_UNKNOWN_FREEBSD_LINKER="clang" + + # Common RUSTFLAGS for FreeBSD targets + FREEBSD_RUSTFLAGS_BASE="-C link-arg=-fuse-ld=lld -C link-arg=--sysroot=${FREEBSD_SYSROOT_ABS} -C target-feature=+crt-static" + + if [ "$ARCH" = "x86_64" ]; then + export CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_RUSTFLAGS="-C link-arg=-target -C link-arg=x86_64-unknown-freebsd ${FREEBSD_RUSTFLAGS_BASE}" + FREEBSD_CARGO_CMD="cargo build --target=${FREEBSD_TARGET} -p guest-agent" + else + # aarch64-unknown-freebsd has no prebuilt stdlib in rustup; build it from source with -Z build-std. + FREEBSD_RUSTFLAGS="-C link-arg=-target -C link-arg=aarch64-unknown-freebsd ${FREEBSD_RUSTFLAGS_BASE}" + [ "$OS" = "Darwin" ] && FREEBSD_RUSTFLAGS="${FREEBSD_RUSTFLAGS} -C link-arg=-stdlib=libc++" + export CARGO_TARGET_AARCH64_UNKNOWN_FREEBSD_RUSTFLAGS="${FREEBSD_RUSTFLAGS}" + FREEBSD_CARGO_CMD="cargo +${RUST_NIGHTLY} build -Z build-std=std,panic_abort --target=${FREEBSD_TARGET} -p guest-agent" + fi + + echo "Cross-compiling guest-agent for ${FREEBSD_TARGET}" + if $FREEBSD_CARGO_CMD; then + # Build the FreeBSD test rootfs ISO: init-freebsd + FreeBSD guest-agent at the root. + FREEBSD_ISO_STAGING=$(mktemp -d) + mkdir -p "${FREEBSD_ISO_STAGING}/dev" "${FREEBSD_ISO_STAGING}/tmp" "${FREEBSD_ISO_STAGING}/mnt" + cp "${FREEBSD_INIT}" "${FREEBSD_ISO_STAGING}/init-freebsd" + cp "target/${FREEBSD_TARGET}/debug/guest-agent" "${FREEBSD_ISO_STAGING}/guest-agent" + chmod +x "${FREEBSD_ISO_STAGING}/init-freebsd" "${FREEBSD_ISO_STAGING}/guest-agent" + FREEBSD_ISO_PATH="target/freebsd-test-rootfs.iso" + bsdtar cf "${FREEBSD_ISO_PATH}" --format=iso9660 -C "${FREEBSD_ISO_STAGING}" . + rm -rf "${FREEBSD_ISO_STAGING}" + echo "FreeBSD test rootfs ISO: ${FREEBSD_ISO_PATH}" + export KRUN_TEST_FREEBSD_ISO_PATH="${FREEBSD_ISO_PATH}" + else + if [ "$ARCH" = "x86_64" ]; then + echo "WARNING: guest-agent build for ${FREEBSD_TARGET} failed; FreeBSD tests will be skipped." + echo "(Run: rustup target add ${FREEBSD_TARGET})" + else + echo "WARNING: guest-agent build for ${FREEBSD_TARGET} failed; FreeBSD tests will be skipped." + echo "(Run: rustup +${RUST_NIGHTLY} component add rust-src)" + fi + fi +else + echo "FreeBSD sysroot or init/init-freebsd not found; FreeBSD tests will be skipped." + echo "(Run 'make' with BUILD_BSD_INIT=1 in the libkrun root to build FreeBSD assets.)" +fi + # Build runner args: pass through all arguments RUNNER_ARGS="$*" @@ -62,4 +156,62 @@ if [ -n "${KRUN_TEST_BASE_DIR}" ]; then RUNNER_ARGS="${RUNNER_ARGS} --base-dir ${KRUN_TEST_BASE_DIR}" fi +# Resolve gvproxy path: prefer explicit env var, then cached binary in +# target/, then PATH; finally fall back to downloading a cached copy. +GV_DIR="target" +GV_FILE="${GV_DIR}/gvproxy" +mkdir -p "${GV_DIR}" + +if [ -z "${KRUN_TEST_GVPROXY_PATH}" ]; then + # 1) cached copy in target/ + if [ -x "${GV_FILE}" ]; then + export KRUN_TEST_GVPROXY_PATH=$(realpath "${GV_FILE}") + echo "gvproxy (cached): ${KRUN_TEST_GVPROXY_PATH}" + else + # 2) search PATH + if [ "$OS" = "Darwin" ]; then + GV_NAMES="gvproxy gvproxy-darwin" + else + GV_NAMES="gvproxy gvproxy-linux-amd64 gvproxy-linux-arm64" + fi + + for name in $GV_NAMES; do + if which "$name" >/dev/null 2>&1; then + GV_PATH=$(which "$name") + if [ -x "$GV_PATH" ]; then + export KRUN_TEST_GVPROXY_PATH="$GV_PATH" + echo "gvproxy: ${KRUN_TEST_GVPROXY_PATH}" + break + fi + fi + done + + # 3) download into cached location if still unset + if [ -z "${KRUN_TEST_GVPROXY_PATH}" ]; then + GV_VERSION="0.8.9" + GV_URL_BASE="https://github.com/containers/gvisor-tap-vsock/releases/download/v${GV_VERSION}/gvproxy" + + if [ "$OS" = "Darwin" ]; then + GV_URL="${GV_URL_BASE}-darwin" + else + if [ "$ARCH" = "x86_64" ]; then + GV_URL="${GV_URL_BASE}-linux-amd64" + else + GV_URL="${GV_URL_BASE}-linux-arm64" + fi + fi + + echo "Downloading gvproxy to ${GV_FILE}..." + if curl -fL -o "${GV_FILE}" "${GV_URL}"; then + chmod +x "${GV_FILE}" + export KRUN_TEST_GVPROXY_PATH=$(realpath "${GV_FILE}") + echo "gvproxy: ${KRUN_TEST_GVPROXY_PATH}" + else + echo "WARNING: Failed to download gvproxy from ${GV_URL}; network tests may fail." + rm -f "${GV_FILE}" + fi + fi + fi +fi + target/debug/runner ${RUNNER_ARGS} diff --git a/tests/test_cases/src/common_freebsd.rs b/tests/test_cases/src/common_freebsd.rs new file mode 100644 index 000000000..ab08ae38d --- /dev/null +++ b/tests/test_cases/src/common_freebsd.rs @@ -0,0 +1,248 @@ +//! Host-side utilities for FreeBSD guest tests. + +use anyhow::Context; +use nix::libc; +use std::ffi::CString; +use std::os::unix::ffi::OsStrExt; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::test_net::gvproxy::{wait_for_socket, Gvproxy}; +use crate::{krun_call, TestSetup}; +use krun_sys::*; + +pub struct FreeBsdAssets { + pub kernel_path: PathBuf, + pub iso_path: PathBuf, +} + +/// Read FreeBSD asset paths from environment variables. +/// Returns `None` if either variable is unset or the referenced files don't exist. +pub fn freebsd_assets() -> Option { + let kernel_path = PathBuf::from(std::env::var_os("KRUN_TEST_FREEBSD_KERNEL_PATH")?); + let iso_path = PathBuf::from(std::env::var_os("KRUN_TEST_FREEBSD_ISO_PATH")?); + if !kernel_path.exists() || !iso_path.exists() { + return None; + } + Some(FreeBsdAssets { + kernel_path, + iso_path, + }) +} + +/// Create a `KRUN_CONFIG`-labelled ISO inside the test's tmp directory and return its path. +/// +/// `init-freebsd` identifies the config disk by its ISO volume label (`/dev/iso9660/KRUN_CONFIG`), +/// not by vtbd index, so the label is mandatory. +fn create_config_iso(test_case: &str, tmp_dir: &Path) -> anyhow::Result { + let staging = tmp_dir.join("krun_config"); + std::fs::create_dir(&staging).context("create krun_config staging dir")?; + + let json = format!(r#"{{"Cmd":["/guest-agent","{test_case}"]}}"#); + std::fs::write(staging.join("krun_config.json"), json).context("write krun_config.json")?; + + let iso_path = tmp_dir.join("krun_config.iso"); + let status = Command::new("bsdtar") + .args([ + "cf", + iso_path.to_str().context("config iso path is not UTF-8")?, + "--format=iso9660", + "--options", + "volume-id=KRUN_CONFIG", + "-C", + staging + .to_str() + .context("config staging dir is not UTF-8")?, + "krun_config.json", + ]) + .status() + .context( + "Failed to run bsdtar — on Linux install libarchive-tools; on macOS bsdtar is built-in", + )?; + + if !status.success() { + anyhow::bail!("bsdtar exited with {status}"); + } + Ok(iso_path) +} + +/// Normalize serial-console line endings for FreeBSD output assertions. +/// +/// FreeBSD's serial console emits CRLF (`\r\n`); strip the `\r` so that +/// test `check()` overrides can compare against plain `\n`-terminated strings. +pub fn normalize_serial_output(bytes: Vec) -> String { + String::from_utf8_lossy(&bytes) + .replace("\r\n", "\n") + .replace('\r', "\n") +} + +/// Generate a random MAC address for virtio-net device. +fn random_mac_address() -> [u8; 6] { + use std::collections::hash_map::RandomState; + use std::hash::{BuildHasher, Hasher}; + + let mut hasher = RandomState::new().build_hasher(); + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .subsec_nanos(); + hasher.write_u32(nanos); + let hash = hasher.finish(); + + [ + 0x52, // Xen OUI + 0x54, + 0x00, + ((hash >> 16) & 0xFF) as u8, + ((hash >> 8) & 0xFF) as u8, + (hash & 0xFF) as u8, + ] +} + +/// Start gvproxy and attach a virtio-net device for a FreeBSD guest. +/// +/// Mirrors `crate::test_net::gvproxy::setup_backend` but with FreeBSD-specific knobs: +/// passes `--listen unix://...` so callers can drive the HTTP API +/// (e.g. `setup_gvproxy_port_forward`), disables gvproxy's default :22 forwarder, and +/// uses a random MAC + `NET_FLAG_VFKIT` only (guest IP is assigned statically). +/// +/// Returns the net (HTTP-API) unix socket path so callers can call +/// `setup_gvproxy_port_forward` afterwards. +pub fn setup_gvproxy_backend(ctx: u32, test_setup: &TestSetup) -> anyhow::Result { + // Short relative names: macOS `sockaddr_un.sun_path` is 104 bytes (max 103 usable chars), + // so deep tmp paths plus long socket names can overflow. + let tmp_dir = test_setup + .tmp_dir + .canonicalize() + .unwrap_or_else(|_| test_setup.tmp_dir.clone()); + let net_sock = tmp_dir.join("gvproxy-net.sock"); + let vfkit_sock = tmp_dir.join("gvproxy-vfkit.sock"); + let gvproxy_log = tmp_dir.join("gvproxy.log"); + + let net_sock_str = net_sock + .to_str() + .context("gvproxy net-sock path is not valid UTF-8")? + .to_string(); + let vfkit_sock_str = vfkit_sock + .to_str() + .context("gvproxy vfkit-sock path is not valid UTF-8")?; + + let child = Gvproxy::new(vfkit_sock_str, &gvproxy_log) + .net_sock(&net_sock_str) + .ssh_port(-1) + .start()?; + test_setup.register_cleanup_pid(child.id()); + + anyhow::ensure!( + wait_for_socket(&vfkit_sock, 5000), + "gvproxy failed to create vfkit socket" + ); + + let vfkit_cstr = CString::new(vfkit_sock.as_os_str().as_bytes()) + .context("CString::new vfkit socket path")?; + let mut mac = random_mac_address(); + + unsafe { + krun_call!(krun_add_net_unixgram( + ctx, + vfkit_cstr.as_ptr(), + -1, + mac.as_mut_ptr(), + COMPAT_NET_FEATURES, + NET_FLAG_VFKIT, + ))?; + } + + Ok(net_sock_str) +} + +/// Boot a FreeBSD guest with `init-freebsd` and enter it. +/// +/// Parallel to [`crate::common::setup_fs_and_enter`] for Linux guests: +/// - boots from a pre-built rootfs ISO (`vtbd0`) containing `init-freebsd` + `guest-agent` +/// - passes the test-case name via a `KRUN_CONFIG` ISO (`vtbd1`) +/// - uses a serial console (required by FreeBSD; output reaches the runner via the stdout pipe) +pub fn setup_kernel_and_enter( + ctx: u32, + test_setup: TestSetup, + assets: FreeBsdAssets, +) -> anyhow::Result<()> { + let config_iso = create_config_iso(&test_setup.test_case, &test_setup.tmp_dir)?; + + unsafe { do_setup_and_enter(ctx, &assets.kernel_path, &assets.iso_path, &config_iso) } +} + +/// Shared implementation for entering the guest. Handles serial pipe + krun calls. +/// Networking, when needed, is added separately by the caller (e.g. via +/// [`setup_gvproxy_backend`]) before this function is invoked. +unsafe fn do_setup_and_enter( + ctx: u32, + kernel_path: &Path, + rootfs_path: &Path, + config_iso: &Path, +) -> anyhow::Result<()> { + // Create a pipe for serial console input to avoid a kqueue busy-spin on macOS. + // When the runner's check() calls wait_with_output(), it closes the subprocess's + // stdin (fd 0). On macOS/kqueue a closed-write-end pipe fires EVFILT_READ + // continuously, spinning the serial device at ~100% CPU. Using a fresh pipe + // whose write end stays open until _exit() is called prevents that. + // libkrun takes ownership of the read fd via File::from_raw_fd(); we only + // need to keep the write end alive, which _exit() will close for us. + let mut pipe_fds: [libc::c_int; 2] = [-1, -1]; + if libc::pipe(pipe_fds.as_mut_ptr()) != 0 { + anyhow::bail!( + "Failed to create serial input pipe: {}", + std::io::Error::last_os_error() + ); + } + let serial_read_fd = pipe_fds[0]; + + // Build CStrings for krun API. + let kernel_cstr = CString::new(kernel_path.as_os_str().as_bytes()).context("CString::new")?; + let rootfs_cstr = CString::new(rootfs_path.as_os_str().as_bytes()).context("CString::new")?; + let config_iso_cstr = + CString::new(config_iso.as_os_str().as_bytes()).context("CString::new")?; + + // FreeBSD requires a serial console; virtio console is not supported. + krun_call!(krun_disable_implicit_console(ctx))?; + krun_call!(krun_add_serial_console_default(ctx, serial_read_fd, 1))?; + + // Kernel cmdline: mount vtbd0 as root via cd9660 and hand off to init-freebsd. + #[cfg(target_arch = "x86_64")] + let (kernel_format, cmdline_prefix, flags) = (KRUN_KERNEL_FORMAT_ELF, "", "boot_mute=YES"); + #[cfg(not(target_arch = "x86_64"))] + let (kernel_format, cmdline_prefix, flags) = (KRUN_KERNEL_FORMAT_RAW, "FreeBSD:", "-mq"); + + let cmdline = format!( + "{cmdline_prefix}vfs.root.mountfrom=cd9660:/dev/vtbd0 {flags} init_path=/init-freebsd" + ); + let cmdline_cstr = CString::new(cmdline).context("CString::new")?; + + krun_call!(krun_set_kernel( + ctx, + kernel_cstr.as_ptr(), + kernel_format, + std::ptr::null(), + cmdline_cstr.as_ptr(), + ))?; + + // vtbd0: rootfs ISO (init-freebsd + guest-agent) + krun_call!(krun_add_disk( + ctx, + c"vtbd0".as_ptr(), + rootfs_cstr.as_ptr(), + true, + ))?; + + // vtbd1: config ISO (init-freebsd finds it by KRUN_CONFIG volume label, not vtbd index) + krun_call!(krun_add_disk( + ctx, + c"vtbd1".as_ptr(), + config_iso_cstr.as_ptr(), + true, + ))?; + + krun_call!(krun_start_enter(ctx))?; + unreachable!() +} diff --git a/tests/test_cases/src/freebsd_guest.rs b/tests/test_cases/src/freebsd_guest.rs new file mode 100644 index 000000000..ab0f49cc5 --- /dev/null +++ b/tests/test_cases/src/freebsd_guest.rs @@ -0,0 +1,17 @@ +//! FreeBSD guest-side utilities. + +#[cfg(target_os = "freebsd")] +use nix::libc::{reboot, RB_NOSYNC}; + +/// Clean shutdown for FreeBSD guest tests. +/// +/// After the guest test's `in_guest()` callback completes, call this to gracefully +/// halt the VM via `reboot(RB_NOSYNC)`. This avoids the init process panic +/// that would occur if PID 1's child simply exited. +#[cfg(target_os = "freebsd")] +pub fn halt_vm() -> ! { + unsafe { + reboot(RB_NOSYNC); // fast shutdown without syncing filesystems + } + loop {} +} diff --git a/tests/test_cases/src/freebsd_network.rs b/tests/test_cases/src/freebsd_network.rs new file mode 100644 index 000000000..046dbb341 --- /dev/null +++ b/tests/test_cases/src/freebsd_network.rs @@ -0,0 +1,256 @@ +#[cfg(target_os = "freebsd")] +pub fn configure_virtio_net_ip() { + use nix::libc; + use std::mem; + + // ioctl constants derived from freebsd-sysroot/usr/include/sys/sockio.h + // _IOW('i', 43, struct ifaliasreq{68}) = 0x80000000 | (68<<16) | ('i'<<8) | 43 + const SIOCAIFADDR: libc::c_ulong = 0x8044692b; + // _IOW('i', 16, struct ifreq{32}) = 0x80000000 | (32<<16) | ('i'<<8) | 16 + const SIOCSIFFLAGS: libc::c_ulong = 0x80206910; + + // Helper: convert dotted-decimal octets to a u32 in network byte order. + // sin_addr must be in network byte order (big-endian bytes in memory). + // On little-endian (aarch64) we must swap: .to_be() gives the right layout. + const fn nbo(a: u8, b: u8, c: u8, d: u8) -> u32 { + ((a as u32) << 24 | (b as u32) << 16 | (c as u32) << 8 | (d as u32)).to_be() + } + + // FreeBSD network structures (matching freebsd-sysroot/usr/include/net/if.h) + #[repr(C)] + struct sockaddr_in { + sin_len: u8, + sin_family: u8, + sin_port: u16, + sin_addr: u32, + sin_zero: [u8; 8], + } + + #[repr(C)] + struct ifaliasreq { + ifra_name: [u8; 16], + ifra_addr: sockaddr_in, + ifra_broadaddr: sockaddr_in, + ifra_mask: sockaddr_in, + ifra_ifa_vhid: i32, + } + + // Create socket + let sockfd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) }; + if sockfd < 0 { + eprintln!("Failed to create socket"); + return; + } + + // Interface name + let iface_name = c"vtnet0"; + let iface_bytes = iface_name.to_bytes(); + + // Build the ifaliasreq structure + let mut ifare: ifaliasreq = unsafe { mem::zeroed() }; + ifare.ifra_name[..iface_bytes.len()].copy_from_slice(iface_bytes); + + // Set up the address structure (192.168.127.2) + ifare.ifra_addr = sockaddr_in { + sin_len: mem::size_of::() as u8, + sin_family: libc::AF_INET as u8, + sin_port: 0, + sin_addr: nbo(192, 168, 127, 2), + sin_zero: [0u8; 8], + }; + + // Set up the netmask (255.255.255.0) + ifare.ifra_mask = sockaddr_in { + sin_len: mem::size_of::() as u8, + sin_family: libc::AF_INET as u8, + sin_port: 0, + sin_addr: nbo(255, 255, 255, 0), + sin_zero: [0u8; 8], + }; + + // Set up the broadcast address (192.168.127.255) + ifare.ifra_broadaddr = sockaddr_in { + sin_len: mem::size_of::() as u8, + sin_family: libc::AF_INET as u8, + sin_port: 0, + sin_addr: nbo(192, 168, 127, 255), + sin_zero: [0u8; 8], + }; + + // Set the interface address using ioctl + unsafe { + if libc::ioctl(sockfd, SIOCAIFADDR, &mut ifare as *mut _) < 0 { + eprintln!("Failed to set IP address"); + libc::close(sockfd); + return; + } + } + + // Bring the interface up + #[repr(C)] + struct ifreq { + ifr_name: [u8; 16], + ifr_union: [u8; 16], + } + + let mut ifr: ifreq = unsafe { mem::zeroed() }; + ifr.ifr_name[..iface_bytes.len()].copy_from_slice(iface_bytes); + + // Set flags to IFF_UP + let flags_ptr = &mut ifr.ifr_union as *mut _ as *mut u16; + unsafe { + *flags_ptr = libc::IFF_UP as u16; + } + + unsafe { + if libc::ioctl(sockfd, SIOCSIFFLAGS, &mut ifr as *mut _) < 0 { + eprintln!("Failed to bring interface up"); + libc::close(sockfd); + return; + } + libc::close(sockfd); + } + + // Add default route via 192.168.127.1 (gvproxy gateway). + // The rootfs has no /sbin/route binary, so we use the AF_ROUTE socket directly. + add_default_route(nbo(192, 168, 127, 1)); +} + +/// Add a default route (0.0.0.0/0) via the given gateway address (already in NBO). +/// +/// Sends an RTM_ADD message over an AF_ROUTE socket. The message layout is: +/// rt_msghdr (152 bytes) + sockaddr_in dst + sockaddr_in gw + sockaddr_in netmask +#[cfg(target_os = "freebsd")] +fn add_default_route(gateway_nbo: u32) { + use nix::libc; + use std::mem; + + // rt_msghdr field layout from freebsd-sysroot/usr/include/net/route.h, FreeBSD 14. + // Verified offsets: + // 0 rtm_msglen u16 + // 2 rtm_version u8 + // 3 rtm_type u8 + // 4 rtm_index u16 + // 6 _spare u16 + // 8 rtm_flags i32 + // 12 rtm_addrs i32 + // 16 rtm_pid i32 (pid_t = int on FreeBSD) + // 20 rtm_seq i32 + // 24 rtm_errno i32 + // 28 rtm_fmask i32 + // 32 rtm_inits u64 + // 40 rtm_rmx [u64; 14] (struct rt_metrics, 112 bytes) + // Total: 152 bytes + #[repr(C)] + struct RtMsghdr { + rtm_msglen: u16, + rtm_version: u8, + rtm_type: u8, + rtm_index: u16, + _spare: u16, + rtm_flags: i32, + rtm_addrs: i32, + rtm_pid: i32, + rtm_seq: i32, + rtm_errno: i32, + rtm_fmask: i32, + rtm_inits: u64, + rtm_rmx: [u64; 14], // struct rt_metrics + } + + #[repr(C)] + struct SockaddrIn { + sin_len: u8, + sin_family: u8, + sin_port: u16, + sin_addr: u32, + sin_zero: [u8; 8], + } + + // RTM_ADD = 0x1, RTM_VERSION = 5 + // RTF_UP = 0x1, RTF_GATEWAY = 0x2, RTF_STATIC = 0x800 + // RTA_DST = 0x1, RTA_GATEWAY = 0x2, RTA_NETMASK = 0x4 + const RTM_ADD: u8 = 0x1; + const RTM_VERSION: u8 = 5; + const RTF_UP: i32 = 0x1; + const RTF_GATEWAY: i32 = 0x2; + const RTF_STATIC: i32 = 0x800; + const RTA_DST: i32 = 0x1; + const RTA_GATEWAY: i32 = 0x2; + const RTA_NETMASK: i32 = 0x4; + + let sa_size = mem::size_of::() as u8; // 16 + + #[repr(C)] + struct RouteMsg { + hdr: RtMsghdr, + dst: SockaddrIn, // destination: 0.0.0.0 (default) + gateway: SockaddrIn, // gateway: 192.168.127.1 + netmask: SockaddrIn, // netmask: 0.0.0.0 (default route matches all) + } + + let msg_len = mem::size_of::() as u16; + + let msg = RouteMsg { + hdr: RtMsghdr { + rtm_msglen: msg_len, + rtm_version: RTM_VERSION, + rtm_type: RTM_ADD, + rtm_index: 0, + _spare: 0, + rtm_flags: RTF_UP | RTF_GATEWAY | RTF_STATIC, + rtm_addrs: RTA_DST | RTA_GATEWAY | RTA_NETMASK, + rtm_pid: 0, + rtm_seq: 1, + rtm_errno: 0, + rtm_fmask: 0, + rtm_inits: 0, + rtm_rmx: [0u64; 14], + }, + dst: SockaddrIn { + sin_len: mem::size_of::() as u8, + sin_family: libc::AF_INET as u8, + sin_port: 0, + sin_addr: 0, // 0.0.0.0 + sin_zero: [0u8; 8], + }, + gateway: SockaddrIn { + sin_len: sa_size, + sin_family: libc::AF_INET as u8, + sin_port: 0, + sin_addr: gateway_nbo, + sin_zero: [0u8; 8], + }, + netmask: SockaddrIn { + sin_len: sa_size, + sin_family: libc::AF_INET as u8, + sin_port: 0, + sin_addr: 0, // 0.0.0.0 mask = default route + sin_zero: [0u8; 8], + }, + }; + + unsafe { + let sockfd = libc::socket(libc::AF_ROUTE, libc::SOCK_RAW, 0); + if sockfd < 0 { + eprintln!("Failed to open AF_ROUTE socket"); + return; + } + + let ret = libc::write( + sockfd, + &msg as *const _ as *const libc::c_void, + mem::size_of::(), + ); + if ret < 0 { + eprintln!("Failed to add default route"); + } + + libc::close(sockfd); + } +} + +#[cfg(not(target_os = "freebsd"))] +pub fn configure_virtio_net_ip() { + // No-op on non-FreeBSD systems +} diff --git a/tests/test_cases/src/lib.rs b/tests/test_cases/src/lib.rs index 83f3b6b14..f78436dd4 100644 --- a/tests/test_cases/src/lib.rs +++ b/tests/test_cases/src/lib.rs @@ -36,6 +36,17 @@ pub enum TestOutcome { Report(Box), } +mod test_freebsd_boot; +use test_freebsd_boot::TestFreeBsdBoot; + +mod test_freebsd_gvproxy_tcp_guest_connect; +use test_freebsd_gvproxy_tcp_guest_connect::TestFreeBsdGvproxyTcpGuestConnect; + +mod test_freebsd_gvproxy_tcp_guest_listen; +use test_freebsd_gvproxy_tcp_guest_listen::TestFreeBsdGvproxyTcpGuestListen; + +pub mod freebsd_guest; + pub enum ShouldRun { Yes, No(&'static str), @@ -106,6 +117,15 @@ pub fn test_cases() -> Vec { "perf-net-vmnet-helper-rx", Box::new(TestNetPerf::new_vmnet_helper_rx()), ), + TestCase::new("freebsd-boot", Box::new(TestFreeBsdBoot)), + TestCase::new( + "freebsd-gvproxy-tcp-guest-connect", + Box::new(TestFreeBsdGvproxyTcpGuestConnect::new()), + ), + TestCase::new( + "freebsd-gvproxy-tcp-guest-listen", + Box::new(TestFreeBsdGvproxyTcpGuestListen::new()), + ), ] } @@ -156,6 +176,9 @@ compile_error!("Cannot enable both guest and host in the same binary!"); #[cfg(feature = "host")] mod common; +#[cfg(feature = "host")] +pub mod common_freebsd; + #[cfg(feature = "host")] mod krun; @@ -163,6 +186,9 @@ mod krun; pub mod rootfs; mod tcp_tester; +#[cfg(feature = "guest")] +pub mod freebsd_network; + #[host] #[derive(Clone, Debug)] pub struct TestSetup { diff --git a/tests/test_cases/src/tcp_tester.rs b/tests/test_cases/src/tcp_tester.rs index 941badbe0..e5ad7c511 100644 --- a/tests/test_cases/src/tcp_tester.rs +++ b/tests/test_cases/src/tcp_tester.rs @@ -71,9 +71,27 @@ impl TcpTester { } pub fn run_client(&self) { - let mut stream = connect(self.server_ip, self.port); - set_timeouts(&mut stream); - expect_msg(&mut stream, b"ping!"); + // Retry connect + first read until the proxy/guest bridge is fully wired up. + // gvproxy (and libkrun's TSI) accept the host-side connection immediately, + // but the upstream dial to the guest only succeeds once the guest has + // reached listen()/accept(). Until then the host either reads zero bytes + // (timeout/WouldBlock) or sees an EOF from a forced close. Once the first + // byte arrives, the bridge is live; any later error is a real bug. + let mut stream = loop { + let mut stream = connect(self.server_ip, self.port); + set_timeouts(&mut stream); + let mut buf = [0u8; 5]; + match stream.read_exact(&mut buf) { + Ok(()) => { + assert_eq!(&buf[..], b"ping!"); + break stream; + } + Err(_) => { + drop(stream); + thread::sleep(Duration::from_millis(200)); + } + } + }; expect_wouldblock(&mut stream); stream.write_all(b"pong!").unwrap(); expect_msg(&mut stream, b"bye!"); diff --git a/tests/test_cases/src/test_freebsd_boot.rs b/tests/test_cases/src/test_freebsd_boot.rs new file mode 100644 index 000000000..86eb96b19 --- /dev/null +++ b/tests/test_cases/src/test_freebsd_boot.rs @@ -0,0 +1,57 @@ +use macros::{guest, host}; + +pub struct TestFreeBsdBoot; + +#[host] +mod host { + use super::*; + + use crate::common_freebsd::{freebsd_assets, normalize_serial_output, setup_kernel_and_enter}; + use crate::{krun_call, krun_call_u32, ShouldRun, Test, TestOutcome, TestSetup}; + use krun_sys::*; + + impl Test for TestFreeBsdBoot { + fn check(self: Box, stdout: Vec) -> TestOutcome { + let output_str = normalize_serial_output(stdout); + if output_str == "OK\n" { + TestOutcome::Pass + } else { + TestOutcome::Fail(format!( + "expected exactly {:?}, got {:?}", + "OK\n", output_str + )) + } + } + + fn start_vm(self: Box, test_setup: TestSetup) -> anyhow::Result<()> { + let assets = freebsd_assets().expect("FreeBSD assets must be present when test runs"); + unsafe { + krun_call!(krun_set_log_level(KRUN_LOG_LEVEL_TRACE))?; + let ctx = krun_call_u32!(krun_create_ctx())?; + krun_call!(krun_set_vm_config(ctx, 1, 512))?; + setup_kernel_and_enter(ctx, test_setup, assets)?; + } + Ok(()) + } + + fn should_run(&self) -> ShouldRun { + match freebsd_assets() { + Some(_) => ShouldRun::Yes, + None => ShouldRun::No("freebsd assets missing"), + } + } + } +} + +#[guest] +mod guest { + use super::*; + + use crate::Test; + + impl Test for TestFreeBsdBoot { + fn in_guest(self: Box) { + println!("OK"); + } + } +} diff --git a/tests/test_cases/src/test_freebsd_gvproxy_tcp_guest_connect.rs b/tests/test_cases/src/test_freebsd_gvproxy_tcp_guest_connect.rs new file mode 100644 index 000000000..04c70462f --- /dev/null +++ b/tests/test_cases/src/test_freebsd_gvproxy_tcp_guest_connect.rs @@ -0,0 +1,91 @@ +use crate::tcp_tester::TcpTester; +use macros::{guest, host}; +use std::net::Ipv4Addr; + +const PORT: u16 = 8000; +// gvproxy's default NAT table maps HostIP (192.168.127.254) → 127.0.0.1 on the host. +// The gateway IP (192.168.127.1) is only virtual inside gvproxy's netstack and NOT +// reachable via net.Dial from the host — connecting to it gets a TCP RST. +const HOST_IP: Ipv4Addr = Ipv4Addr::new(192, 168, 127, 254); + +pub struct TestFreeBsdGvproxyTcpGuestConnect { + tcp_tester: TcpTester, +} + +impl TestFreeBsdGvproxyTcpGuestConnect { + pub fn new() -> TestFreeBsdGvproxyTcpGuestConnect { + Self { + tcp_tester: TcpTester::new(HOST_IP, PORT), + } + } +} + +#[host] +mod host { + use super::*; + + use crate::common_freebsd::{ + freebsd_assets, normalize_serial_output, setup_gvproxy_backend, setup_kernel_and_enter, + }; + use crate::test_net::gvproxy::gvproxy_path; + use crate::{krun_call, krun_call_u32}; + use crate::{ShouldRun, Test, TestOutcome, TestSetup}; + use krun_sys::*; + use std::thread; + + impl Test for TestFreeBsdGvproxyTcpGuestConnect { + fn should_run(&self) -> ShouldRun { + if freebsd_assets().is_none() { + return ShouldRun::No("freebsd assets missing"); + } + match gvproxy_path() { + Some(_) => ShouldRun::Yes, + None => ShouldRun::No("gvproxy not installed"), + } + } + + fn start_vm(self: Box, test_setup: TestSetup) -> anyhow::Result<()> { + let assets = freebsd_assets().expect("freebsd assets must be available"); + + // Spawn host-side TCP server. Guest connects to HOST_IP:PORT through gvproxy. + let listener = self.tcp_tester.create_server_socket(); + thread::spawn(move || self.tcp_tester.run_server(listener)); + + unsafe { + krun_call!(krun_set_log_level(KRUN_LOG_LEVEL_TRACE))?; + let ctx = krun_call_u32!(krun_create_ctx())?; + krun_call!(krun_set_vm_config(ctx, 1, 512))?; + setup_gvproxy_backend(ctx, &test_setup)?; + setup_kernel_and_enter(ctx, test_setup, assets)?; + } + Ok(()) + } + + fn check(self: Box, stdout: Vec) -> TestOutcome { + let output_str = normalize_serial_output(stdout); + if output_str == "OK\n" { + TestOutcome::Pass + } else { + TestOutcome::Fail(format!( + "expected exactly {:?}, got {:?}", + "OK\n", output_str + )) + } + } + } +} + +#[guest] +mod guest { + use super::*; + use crate::freebsd_network::configure_virtio_net_ip; + use crate::Test; + + impl Test for TestFreeBsdGvproxyTcpGuestConnect { + fn in_guest(self: Box) { + configure_virtio_net_ip(); + self.tcp_tester.run_client(); + println!("OK"); + } + } +} diff --git a/tests/test_cases/src/test_freebsd_gvproxy_tcp_guest_listen.rs b/tests/test_cases/src/test_freebsd_gvproxy_tcp_guest_listen.rs new file mode 100644 index 000000000..86f2701db --- /dev/null +++ b/tests/test_cases/src/test_freebsd_gvproxy_tcp_guest_listen.rs @@ -0,0 +1,103 @@ +use crate::tcp_tester::TcpTester; +use macros::{guest, host}; +use std::net::Ipv4Addr; + +const PORT: u16 = 8000; + +pub struct TestFreeBsdGvproxyTcpGuestListen { + tcp_tester: TcpTester, +} + +impl TestFreeBsdGvproxyTcpGuestListen { + pub fn new() -> TestFreeBsdGvproxyTcpGuestListen { + // The host-side client connects to 127.0.0.1:PORT — gvproxy's port-forward + // rule maps that to GUEST_IP:PORT inside the virtual network. + Self { + tcp_tester: TcpTester::new(Ipv4Addr::new(127, 0, 0, 1), PORT), + } + } +} + +#[host] +mod host { + use super::*; + + use crate::common_freebsd::{ + freebsd_assets, normalize_serial_output, setup_gvproxy_backend, setup_kernel_and_enter, + }; + use crate::test_net::gvproxy::{gvproxy_path, setup_gvproxy_port_forward}; + use crate::{krun_call, krun_call_u32}; + use crate::{ShouldRun, Test, TestOutcome, TestSetup}; + use krun_sys::*; + use std::net::Ipv4Addr; + use std::thread; + + // Virtual IP assigned to the guest inside gvproxy's network. + const GUEST_IP: Ipv4Addr = Ipv4Addr::new(192, 168, 127, 2); + + impl Test for TestFreeBsdGvproxyTcpGuestListen { + fn should_run(&self) -> ShouldRun { + if freebsd_assets().is_none() { + return ShouldRun::No("freebsd assets missing"); + } + match gvproxy_path() { + Some(_) => ShouldRun::Yes, + None => ShouldRun::No("gvproxy not installed"), + } + } + + fn start_vm(self: Box, test_setup: TestSetup) -> anyhow::Result<()> { + let assets = freebsd_assets().expect("freebsd assets must be available"); + + unsafe { + krun_call!(krun_set_log_level(KRUN_LOG_LEVEL_TRACE))?; + let ctx = krun_call_u32!(krun_create_ctx())?; + krun_call!(krun_set_vm_config(ctx, 1, 512))?; + let net_sock = setup_gvproxy_backend(ctx, &test_setup)?; + + // Set up port-forwarding: host :PORT → guest GUEST_IP:PORT. + // The guest IP (192.168.127.2) is virtual and not reachable directly from + // the host; gvproxy's forwarder bridges the gap. + setup_gvproxy_port_forward(&net_sock, PORT, GUEST_IP)?; + + // Spawn host-side client. Runs concurrently with the VM; retries + // connect+first-read until the guest is listening (see TcpTester::run_client). + let tcp_tester = self.tcp_tester; + thread::spawn(move || { + tcp_tester.run_client(); + }); + + setup_kernel_and_enter(ctx, test_setup, assets)?; + } + Ok(()) + } + + fn check(self: Box, stdout: Vec) -> TestOutcome { + let output_str = normalize_serial_output(stdout); + if output_str == "OK\n" { + TestOutcome::Pass + } else { + TestOutcome::Fail(format!( + "expected exactly {:?}, got {:?}", + "OK\n", output_str + )) + } + } + } +} + +#[guest] +mod guest { + use super::*; + use crate::freebsd_network::configure_virtio_net_ip; + use crate::Test; + + impl Test for TestFreeBsdGvproxyTcpGuestListen { + fn in_guest(self: Box) { + configure_virtio_net_ip(); + self.tcp_tester + .run_server(self.tcp_tester.create_server_socket()); + println!("OK"); + } + } +} diff --git a/tests/test_cases/src/test_net/gvproxy.rs b/tests/test_cases/src/test_net/gvproxy.rs index 0bd18ac02..2ed0cae74 100644 --- a/tests/test_cases/src/test_net/gvproxy.rs +++ b/tests/test_cases/src/test_net/gvproxy.rs @@ -1,58 +1,77 @@ -//! Gvproxy backend for virtio-net test (macOS only) +//! Gvproxy backend for virtio-net tests. +use crate::test_net::get_krun_add_net_unixgram; use crate::{krun_call, ShouldRun, TestSetup}; +use anyhow::Context; use krun_sys::{COMPAT_NET_FEATURES, NET_FLAG_DHCP_CLIENT, NET_FLAG_VFKIT}; -use nix::libc; use std::ffi::CString; +use std::io::{self, Read, Write}; +use std::net::Ipv4Addr; +use std::os::unix::net::UnixStream; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; + +/// Read gvproxy binary path from `KRUN_TEST_GVPROXY_PATH` (set by `tests/run.sh`). +/// Returns `None` if the variable is unset or the referenced file doesn't exist. +pub(crate) fn gvproxy_path() -> Option { + let path = std::env::var_os("KRUN_TEST_GVPROXY_PATH")?; + let p = PathBuf::from(path); + p.exists().then_some(p) +} -type KrunAddNetUnixgramFn = unsafe extern "C" fn( - ctx_id: u32, - c_path: *const std::ffi::c_char, - fd: i32, - c_mac: *mut u8, - features: u32, - flags: u32, -) -> i32; - -fn get_krun_add_net_unixgram() -> KrunAddNetUnixgramFn { - let symbol = CString::new("krun_add_net_unixgram").unwrap(); - let ptr = unsafe { libc::dlsym(libc::RTLD_DEFAULT, symbol.as_ptr()) }; - assert!(!ptr.is_null(), "krun_add_net_unixgram not found"); - unsafe { std::mem::transmute(ptr) } +pub(crate) struct Gvproxy<'a> { + vfkit_sock: &'a str, + log_path: &'a Path, + net_sock: Option<&'a str>, + ssh_port: Option, } -const GVPROXY_PATH: &str = match option_env!("GVPROXY_PATH") { - Some(path) => path, - None => "/opt/homebrew/opt/podman/libexec/podman/gvproxy", -}; +impl<'a> Gvproxy<'a> { + pub fn new(vfkit_sock: &'a str, log_path: &'a Path) -> Self { + Self { + vfkit_sock, + log_path, + net_sock: None, + ssh_port: None, + } + } -fn gvproxy_path() -> Option<&'static str> { - std::path::Path::new(GVPROXY_PATH) - .exists() - .then_some(GVPROXY_PATH) -} + /// Add `--listen unix://` so callers can hit the HTTP API + /// (e.g. [`setup_gvproxy_port_forward`]). + pub fn net_sock(&mut self, net_sock: &'a str) -> &mut Self { + self.net_sock = Some(net_sock); + self + } -fn start_gvproxy( - socket_path: &str, - log_path: &std::path::Path, -) -> std::io::Result { - use std::process::{Command, Stdio}; + /// Add `--ssh-port `. Pass `-1` to disable the default :22 forwarder. + pub fn ssh_port(&mut self, ssh_port: i32) -> &mut Self { + self.ssh_port = Some(ssh_port); + self + } - let gvproxy = gvproxy_path().expect("gvproxy not found"); + pub fn start(&mut self) -> io::Result { + let gvproxy = gvproxy_path().expect("gvproxy not found"); + let log_file = std::fs::File::create(self.log_path)?; - let log_file = std::fs::File::create(log_path)?; + let mut cmd = Command::new(gvproxy); + cmd.arg("--listen-vfkit") + .arg(format!("unixgram:{}", self.vfkit_sock)); + if let Some(net_sock) = self.net_sock { + cmd.arg("--listen").arg(format!("unix://{}", net_sock)); + } + if let Some(port) = self.ssh_port { + cmd.arg("--ssh-port").arg(port.to_string()); + } + cmd.arg("-debug"); - Command::new(gvproxy) - .arg("--listen-vfkit") - .arg(format!("unixgram:{}", socket_path)) - .arg("-debug") - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(log_file) - .spawn() + cmd.stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(log_file) + .spawn() + } } -fn wait_for_socket(path: &std::path::Path, timeout_ms: u64) -> bool { +pub(crate) fn wait_for_socket(path: &Path, timeout_ms: u64) -> bool { let start = std::time::Instant::now(); while start.elapsed().as_millis() < timeout_ms as u128 { if path.exists() { @@ -63,16 +82,56 @@ fn wait_for_socket(path: &std::path::Path, timeout_ms: u64) -> bool { false } -pub(crate) fn should_run() -> ShouldRun { - #[cfg(not(target_os = "macos"))] - return ShouldRun::No("gvproxy unixgram only supported on macOS"); - - #[cfg(target_os = "macos")] - { - if gvproxy_path().is_none() { - return ShouldRun::No("gvproxy not installed"); +/// Set up a gvproxy port-forwarding rule via its HTTP API. +/// +/// Sends `POST /services/forwarder/expose` with +/// `{"local":":","remote":":"}` to the net unix socket. +/// Retries until gvproxy is accepting connections (up to ~10 s). +pub(crate) fn setup_gvproxy_port_forward( + net_sock_path: &str, + port: u16, + remote_ip: Ipv4Addr, +) -> anyhow::Result<()> { + let mut stream = None; + for _ in 0..100 { + match UnixStream::connect(net_sock_path) { + Ok(s) => { + stream = Some(s); + break; + } + Err(_) => std::thread::sleep(std::time::Duration::from_millis(100)), } - ShouldRun::Yes + } + let mut stream = stream + .ok_or_else(|| anyhow::anyhow!("gvproxy HTTP socket not ready: {}", net_sock_path))?; + + let body = format!(r#"{{"local":":{port}","remote":"{remote_ip}:{port}"}}"#); + let request = format!( + "POST /services/forwarder/expose HTTP/1.0\r\nHost: unix\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}", + body.len(), + body, + ); + + stream + .write_all(request.as_bytes()) + .context("write port-forward request")?; + + let mut response = String::new(); + stream + .read_to_string(&mut response) + .context("read port-forward response")?; + + if !response.contains("200") { + anyhow::bail!("gvproxy port-forward expose failed: {}", response); + } + + Ok(()) +} + +pub(crate) fn should_run() -> ShouldRun { + match gvproxy_path() { + Some(_) => ShouldRun::Yes, + None => ShouldRun::No("gvproxy not installed"), } } @@ -87,7 +146,7 @@ pub(crate) fn setup_backend(ctx: u32, test_setup: &TestSetup) -> anyhow::Result< let socket_path_str = socket_path .to_str() .ok_or_else(|| anyhow::anyhow!("gvproxy socket path is not valid UTF-8"))?; - let gvproxy_child = start_gvproxy(socket_path_str, &gvproxy_log)?; + let gvproxy_child = Gvproxy::new(socket_path_str, &gvproxy_log).start()?; test_setup.register_cleanup_pid(gvproxy_child.id()); anyhow::ensure!( diff --git a/tests/test_cases/src/test_tsi_tcp_guest_listen.rs b/tests/test_cases/src/test_tsi_tcp_guest_listen.rs index 41e0ffc2d..09e15fa4a 100644 --- a/tests/test_cases/src/test_tsi_tcp_guest_listen.rs +++ b/tests/test_cases/src/test_tsi_tcp_guest_listen.rs @@ -25,13 +25,11 @@ mod host { use std::ffi::CString; use std::ptr::null; use std::thread; - use std::time::Duration; impl Test for TestTsiTcpGuestListen { fn start_vm(self: Box, test_setup: TestSetup) -> anyhow::Result<()> { unsafe { thread::spawn(move || { - thread::sleep(Duration::from_secs(1)); self.tcp_tester.run_client(); }); diff --git a/tests/test_cases/src/test_vsock_guest_connect.rs b/tests/test_cases/src/test_vsock_guest_connect.rs index bb0482f29..2a34257a6 100644 --- a/tests/test_cases/src/test_vsock_guest_connect.rs +++ b/tests/test_cases/src/test_vsock_guest_connect.rs @@ -1,16 +1,20 @@ -use macros::{guest, host}; +#[cfg(target_os = "linux")] +use macros::guest; +use macros::host; use std::io::{ErrorKind, Read}; use std::os::unix::net::UnixStream; use std::time::Duration; pub struct TestVsockGuestConnect; +#[allow(dead_code)] fn stream_expect_msg(stream: &mut UnixStream, expected: &[u8]) { let mut buf = vec![0; expected.len()]; stream.read_exact(&mut buf[..]).unwrap(); assert_eq!(&buf[..], expected); } +#[allow(dead_code)] fn stream_expect_wouldblock(stream: &mut UnixStream) { stream.set_nonblocking(true).unwrap(); let err = stream.read(&mut [0u8; 1]).unwrap_err(); @@ -18,6 +22,7 @@ fn stream_expect_wouldblock(stream: &mut UnixStream) { assert_eq!(err.kind(), ErrorKind::WouldBlock); } +#[allow(dead_code)] fn stream_set_timeouts(stream: &mut UnixStream) { stream .set_read_timeout(Some(Duration::from_secs(3))) @@ -27,6 +32,7 @@ fn stream_set_timeouts(stream: &mut UnixStream) { .unwrap(); } +#[allow(dead_code)] const VSOCK_PORT: u32 = 1234; #[host] @@ -78,6 +84,8 @@ mod host { } } +// Vsock is only available on Linux guests. +#[cfg(target_os = "linux")] #[guest] mod guest { use super::*; @@ -111,3 +119,7 @@ mod guest { } } } + +// Stub impl so the guest-agent binary compiles on non-Linux targets (vsock test won't be dispatched there). +#[cfg(all(feature = "guest", not(target_os = "linux")))] +impl crate::Test for TestVsockGuestConnect {} From 1f3d754ac4e8ac5ffeb971d06e246072918bbe19 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Tue, 21 Apr 2026 22:48:49 +0200 Subject: [PATCH 3/6] update `integration_tests.yml` to run FreeBSD tests - have to install lld and also clang on the self-hosted runner Signed-off-by: Jan Noha --- .github/workflows/integration_tests.yml | 40 +++++++++++++++++++++++++ tests/README.md | 6 ++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 811ecf3fd..1f553f02f 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -11,9 +11,29 @@ jobs: - name: Setup build environment uses: ./.github/actions/setup-build-env + - name: Install cross-compilation dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends lld + - name: Add musl target run: rustup target add x86_64-unknown-linux-musl + - name: Add FreeBSD x86_64 target + run: rustup target add x86_64-unknown-freebsd + + - name: Install FreeBSD test prerequisites + run: sudo apt-get install -y --no-install-recommends libarchive-tools + + - name: Build FreeBSD sysroot and init + run: make BUILD_BSD_INIT=1 -- init/init-freebsd + + - name: Install gvproxy + run: | + curl -fL -o /tmp/gvproxy https://github.com/containers/gvisor-tap-vsock/releases/download/v0.8.9/gvproxy-linux-amd64 + chmod +x /tmp/gvproxy + sudo mv /tmp/gvproxy /usr/local/bin/gvproxy + - name: Build and install libkrun to test prefix run: make test-prefix NET=1 @@ -80,9 +100,29 @@ jobs: - name: Setup build environment uses: ./.github/actions/setup-build-env + - name: Install cross-compilation dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends clang lld + - name: Add musl target run: rustup target add aarch64-unknown-linux-musl + - name: Install nightly toolchain with rust-src for FreeBSD aarch64 + run: rustup +nightly-2026-01-25 component add rust-src + + - name: Install FreeBSD test prerequisites + run: sudo apt-get install -y --no-install-recommends libarchive-tools + + - name: Build FreeBSD sysroot and init + run: make BUILD_BSD_INIT=1 -- init/init-freebsd + + - name: Install gvproxy + run: | + curl -fL -o /tmp/gvproxy https://github.com/containers/gvisor-tap-vsock/releases/download/v0.8.9/gvproxy-linux-arm64 + chmod +x /tmp/gvproxy + sudo mv /tmp/gvproxy /usr/local/bin/gvproxy + - name: Build and install libkrun to test prefix run: make test-prefix NET=1 diff --git a/tests/README.md b/tests/README.md index 1aca63971..6b5c15dc8 100644 --- a/tests/README.md +++ b/tests/README.md @@ -59,14 +59,14 @@ FreeBSD guest tests run on Linux (amd64, arm64) and macOS (arm64) hosts. They re rustup target add x86_64-unknown-freebsd ``` - **Linux/macOS arm64**: `aarch64-unknown-freebsd` has no prebuilt stdlib in rustup, - so a nightly toolchain is used with `-Z build-std` to build it from source: + so a nightly toolchain with rust-src component is needed: ```bash - rustup toolchain install nightly-2026-01-25 + rustup +nightly-2026-01-25 component add rust-src ``` 2. Build the FreeBSD sysroot and `init-freebsd` (from the libkrun root directory): ```bash - make BUILD_BSD_INIT=1 + make BUILD_BSD_INIT=1 -- init/init-freebsd ``` This downloads `freebsd-sysroot/base.txz`, extracts it to `freebsd-sysroot/`, and compiles `init/init-freebsd`. From d4ed14a75ac8006cc49a8ed42ef01648c9a5334d Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Tue, 21 Apr 2026 23:47:37 +0200 Subject: [PATCH 4/6] do not compile virtiofs test for FreeBSD guest Signed-off-by: Jan Noha --- tests/test_cases/src/test_virtiofs_root_ro.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_cases/src/test_virtiofs_root_ro.rs b/tests/test_cases/src/test_virtiofs_root_ro.rs index 1fff83ce2..a6b48cb5f 100644 --- a/tests/test_cases/src/test_virtiofs_root_ro.rs +++ b/tests/test_cases/src/test_virtiofs_root_ro.rs @@ -2,12 +2,17 @@ // virtiofs root. It is not exhaustive.For a security sensitive test it would also be better // to bypass the guest kernel and execute the virtiofs commands directly. -use macros::{guest, host}; +#[cfg(target_os = "linux")] +use macros::guest; +use macros::host; pub struct TestVirtiofsRootRo; +#[allow(dead_code)] const TEST_FILE: &str = "test-file"; +#[allow(dead_code)] const TEST_CONTENT: &[u8] = b"original content"; +#[allow(dead_code)] const EMPTY_DIR: &str = "empty-dir"; #[host] @@ -67,6 +72,7 @@ mod host { } } +#[cfg(target_os = "linux")] #[guest] mod guest { use super::*; @@ -215,3 +221,7 @@ mod guest { } } } + +// Stub impl so the guest-agent binary compiles on non-Linux targets (vsock test won't be dispatched there). +#[cfg(all(feature = "guest", not(target_os = "linux")))] +impl crate::Test for TestVirtiofsRootRo {} From 9c6e4c9d7134149472b68ea0b9a1e1f12a2a8a3c Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Wed, 29 Apr 2026 22:14:04 +0200 Subject: [PATCH 5/6] cargo fmt, clippy fixes Signed-off-by: Jan Noha --- src/arch/src/x86_64/mod.rs | 44 +++++++++++++---- src/arch/src/x86_64/regs.rs | 6 ++- src/vmm/src/builder.rs | 98 +++++++++++++++++++++++++------------ 3 files changed, 106 insertions(+), 42 deletions(-) diff --git a/src/arch/src/x86_64/mod.rs b/src/arch/src/x86_64/mod.rs index 6598143e5..5d1287cab 100644 --- a/src/arch/src/x86_64/mod.rs +++ b/src/arch/src/x86_64/mod.rs @@ -21,7 +21,9 @@ use crate::x86_64::layout::{EBDA_START, FIRST_ADDR_PAST_32BITS, MMIO_MEM_START}; #[cfg(feature = "tee")] use crate::x86_64::layout::{FIRMWARE_SIZE, FIRMWARE_START}; use crate::{ArchMemoryInfo, InitrdConfig}; -use arch_gen::x86::bootparam::{boot_params, E820_RAM, E820_RESERVED}; +#[cfg(not(feature = "tee"))] +use arch_gen::x86::bootparam::E820_RESERVED; +use arch_gen::x86::bootparam::{boot_params, E820_RAM}; use vm_memory::Bytes; use vm_memory::{Address, ByteValued, GuestAddress, GuestMemoryMmap}; use vmm_sys_util::align_upwards; @@ -358,12 +360,7 @@ fn configure_pvh( } #[cfg(not(feature = "tee"))] -fn add_memmap_entry( - memmap: &mut Vec, - addr: u64, - size: u64, - mem_type: u32, -) { +fn add_memmap_entry(memmap: &mut Vec, addr: u64, size: u64, mem_type: u32) { memmap.push(hvm_memmap_table_entry { addr, size, @@ -536,21 +533,48 @@ mod tests { let (arch_mem_info, arch_mem_regions) = arch_memory_regions(mem_size, Some(KERNEL_LOAD_ADDR), KERNEL_SIZE, 0, None); let gm = GuestMemoryMmap::from_ranges(&arch_mem_regions).unwrap(); - configure_system(&gm, &arch_mem_info, GuestAddress(0), 0, &None, no_vcpus, false).unwrap(); + configure_system( + &gm, + &arch_mem_info, + GuestAddress(0), + 0, + &None, + no_vcpus, + false, + ) + .unwrap(); // Now assigning some memory that is equal to the start of the 32bit memory hole. let mem_size = 3328 << 20; let (arch_mem_info, arch_mem_regions) = arch_memory_regions(mem_size, Some(KERNEL_LOAD_ADDR), KERNEL_SIZE, 0, None); let gm = GuestMemoryMmap::from_ranges(&arch_mem_regions).unwrap(); - configure_system(&gm, &arch_mem_info, GuestAddress(0), 0, &None, no_vcpus, false).unwrap(); + configure_system( + &gm, + &arch_mem_info, + GuestAddress(0), + 0, + &None, + no_vcpus, + false, + ) + .unwrap(); // Now assigning some memory that falls after the 32bit memory hole. let mem_size = 3330 << 20; let (arch_mem_info, arch_mem_regions) = arch_memory_regions(mem_size, Some(KERNEL_LOAD_ADDR), KERNEL_SIZE, 0, None); let gm = GuestMemoryMmap::from_ranges(&arch_mem_regions).unwrap(); - configure_system(&gm, &arch_mem_info, GuestAddress(0), 0, &None, no_vcpus, false).unwrap(); + configure_system( + &gm, + &arch_mem_info, + GuestAddress(0), + 0, + &None, + no_vcpus, + false, + ) + .unwrap(); } #[test] diff --git a/src/arch/src/x86_64/regs.rs b/src/arch/src/x86_64/regs.rs index ffd293c1a..9f3031f0d 100644 --- a/src/arch/src/x86_64/regs.rs +++ b/src/arch/src/x86_64/regs.rs @@ -155,7 +155,11 @@ fn write_idt_value(val: u64, guest_mem: &GuestMemoryMmap) -> Result<()> { .map_err(|_| Error::WriteIDT) } -fn configure_segments_and_sregs(mem: &GuestMemoryMmap, sregs: &mut kvm_sregs, pvh: bool) -> Result<()> { +fn configure_segments_and_sregs( + mem: &GuestMemoryMmap, + sregs: &mut kvm_sregs, + pvh: bool, +) -> Result<()> { let gdt_table: [u64; BOOT_GDT_MAX] = if pvh { [ gdt_entry(0, 0, 0), // NULL diff --git a/src/vmm/src/builder.rs b/src/vmm/src/builder.rs index 35a890a5b..cb7be764f 100644 --- a/src/vmm/src/builder.rs +++ b/src/vmm/src/builder.rs @@ -1155,7 +1155,10 @@ fn load_external_kernel( guest_mem: &GuestMemoryMmap, arch_mem_info: &ArchMemoryInfo, external_kernel: &ExternalKernel, -) -> std::result::Result<(GuestAddress, Option, Option, bool), StartMicrovmError> { +) -> std::result::Result< + (GuestAddress, Option, Option, bool), + StartMicrovmError, +> { #[allow(unused_mut)] let mut pvh = false; let entry_addr = match external_kernel.format { @@ -1302,7 +1305,20 @@ fn load_external_kernel( None }; - Ok((entry_addr, initrd_config, external_kernel.cmdline.clone(), pvh)) + Ok(( + entry_addr, + initrd_config, + external_kernel.cmdline.clone(), + pvh, + )) +} + +struct LoadedPayload { + guest_mem: GuestMemoryMmap, + entry_addr: GuestAddress, + initrd_config: Option, + kernel_cmdline: Option, + pvh: bool, } fn load_payload( @@ -1310,16 +1326,7 @@ fn load_payload( guest_mem: GuestMemoryMmap, _arch_mem_info: &ArchMemoryInfo, payload: &Payload, -) -> std::result::Result< - ( - GuestMemoryMmap, - GuestAddress, - Option, - Option, - bool, - ), - StartMicrovmError, -> { +) -> std::result::Result { match payload { #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] Payload::KernelCopy => { @@ -1346,7 +1353,13 @@ fn load_payload( guest_mem .write(kernel_data, GuestAddress(kernel_guest_addr)) .unwrap(); - Ok((guest_mem, GuestAddress(kernel_entry_addr), None, None, false)) + Ok(LoadedPayload { + guest_mem, + entry_addr: GuestAddress(kernel_entry_addr), + initrd_config: None, + kernel_cmdline: None, + pvh: false, + }) } #[cfg(all(target_arch = "x86_64", not(feature = "tee")))] Payload::KernelMmap => { @@ -1434,8 +1447,8 @@ fn load_payload( } }; - Ok(( - guest_mem + Ok(LoadedPayload { + guest_mem: guest_mem .insert_region(Arc::new( GuestRegionMmap::new(kernel_region, GuestAddress(kernel_guest_addr)) .ok_or_else(|| { @@ -1445,19 +1458,31 @@ fn load_payload( })?, )) .map_err(|e| StartMicrovmError::GuestMemoryMmap(format!("{e:?}")))?, - GuestAddress(kernel_entry_addr), - None, - None, - false, - )) + entry_addr: GuestAddress(kernel_entry_addr), + initrd_config: None, + kernel_cmdline: None, + pvh: false, + }) } Payload::ExternalKernel(external_kernel) => { let (entry_addr, initrd_config, cmdline, pvh) = load_external_kernel(&guest_mem, _arch_mem_info, external_kernel)?; - Ok((guest_mem, entry_addr, initrd_config, cmdline, pvh)) + Ok(LoadedPayload { + guest_mem, + entry_addr, + initrd_config, + kernel_cmdline: cmdline, + pvh, + }) } #[cfg(test)] - Payload::Empty => Ok((guest_mem, GuestAddress(0), None, None, false)), + Payload::Empty => Ok(LoadedPayload { + guest_mem, + entry_addr: GuestAddress(0), + initrd_config: None, + kernel_cmdline: None, + pvh: false, + }), #[cfg(feature = "tee")] Payload::Tee => { let (kernel_host_addr, kernel_guest_addr, kernel_size) = @@ -1505,15 +1530,21 @@ fn load_payload( size: initrd_data.len(), }; - Ok(( + Ok(LoadedPayload { guest_mem, - GuestAddress(arch::RESET_VECTOR), - Some(initrd_config), - None, - false, - )) + entry_addr: GuestAddress(arch::RESET_VECTOR), + initrd_config: Some(initrd_config), + kernel_cmdline: None, + pvh: false, + }) } - Payload::Firmware => Ok((guest_mem, GuestAddress(arch::RESET_VECTOR), None, None, false)), + Payload::Firmware => Ok(LoadedPayload { + guest_mem, + entry_addr: GuestAddress(arch::RESET_VECTOR), + initrd_config: None, + kernel_cmdline: None, + pvh: false, + }), } } @@ -1666,8 +1697,13 @@ pub fn create_guest_memory( .map_err(|e| StartMicrovmError::GuestMemoryMmap(format!("{e:?}")))? }; - let (guest_mem, entry_addr, initrd_config, cmdline, pvh) = - load_payload(vm_resources, guest_mem, &arch_mem_info, payload)?; + let LoadedPayload { + guest_mem, + entry_addr, + initrd_config, + kernel_cmdline: cmdline, + pvh, + } = load_payload(vm_resources, guest_mem, &arch_mem_info, payload)?; // Only write firmware if data exists AND this isn't an ExternalKernel payload // (ExternalKernel does direct kernel boot and doesn't use EFI firmware) From d7d8b5aa0e1eb21400819634330865fbdf14cbf8 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 2 May 2026 23:36:23 +0200 Subject: [PATCH 6/6] tests: consistently use `get_krun_add_net_unixgram`, don't duplicate the definition Signed-off-by: Jan Noha --- tests/test_cases/src/common_freebsd.rs | 3 ++- tests/test_cases/src/test_net/mod.rs | 24 +++++++++++++++++++ tests/test_cases/src/test_net/vmnet_helper.rs | 18 +------------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/tests/test_cases/src/common_freebsd.rs b/tests/test_cases/src/common_freebsd.rs index ab08ae38d..32f498f0a 100644 --- a/tests/test_cases/src/common_freebsd.rs +++ b/tests/test_cases/src/common_freebsd.rs @@ -8,6 +8,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; +use crate::test_net::get_krun_add_net_unixgram; use crate::test_net::gvproxy::{wait_for_socket, Gvproxy}; use crate::{krun_call, TestSetup}; use krun_sys::*; @@ -144,7 +145,7 @@ pub fn setup_gvproxy_backend(ctx: u32, test_setup: &TestSetup) -> anyhow::Result let mut mac = random_mac_address(); unsafe { - krun_call!(krun_add_net_unixgram( + krun_call!(get_krun_add_net_unixgram()( ctx, vfkit_cstr.as_ptr(), -1, diff --git a/tests/test_cases/src/test_net/mod.rs b/tests/test_cases/src/test_net/mod.rs index 9eb973a81..6fcb0559a 100644 --- a/tests/test_cases/src/test_net/mod.rs +++ b/tests/test_cases/src/test_net/mod.rs @@ -7,6 +7,12 @@ use crate::tcp_tester::TcpTester; use macros::{guest, host}; +#[host] +use nix::libc; + +#[host] +use std::ffi::CString; + #[host] use crate::{ShouldRun, TestSetup}; @@ -143,3 +149,21 @@ mod guest { } } } + +#[cfg(feature = "host")] +type KrunAddNetUnixgramFn = unsafe extern "C" fn( + ctx_id: u32, + c_path: *const std::ffi::c_char, + fd: i32, + c_mac: *mut u8, + features: u32, + flags: u32, +) -> i32; + +#[cfg(feature = "host")] +pub(crate) fn get_krun_add_net_unixgram() -> KrunAddNetUnixgramFn { + let symbol = CString::new("krun_add_net_unixgram").unwrap(); + let ptr = unsafe { libc::dlsym(libc::RTLD_DEFAULT, symbol.as_ptr()) }; + assert!(!ptr.is_null(), "krun_add_net_unixgram not found"); + unsafe { std::mem::transmute(ptr) } +} diff --git a/tests/test_cases/src/test_net/vmnet_helper.rs b/tests/test_cases/src/test_net/vmnet_helper.rs index 657cc2e45..26c289c1a 100644 --- a/tests/test_cases/src/test_net/vmnet_helper.rs +++ b/tests/test_cases/src/test_net/vmnet_helper.rs @@ -1,31 +1,15 @@ //! vmnet-helper backend for virtio-net test (macOS only) +use crate::test_net::get_krun_add_net_unixgram; use crate::{krun_call, ShouldRun, TestSetup}; use krun_sys::{ NET_FEATURE_CSUM, NET_FEATURE_GUEST_CSUM, NET_FEATURE_GUEST_TSO4, NET_FEATURE_HOST_TSO4, NET_FLAG_DHCP_CLIENT, }; use nix::libc; -use std::ffi::CString; use std::io::{BufRead, BufReader, Read}; use std::process::{Command, Stdio}; -type KrunAddNetUnixgramFn = unsafe extern "C" fn( - ctx_id: u32, - c_path: *const std::ffi::c_char, - fd: i32, - c_mac: *mut u8, - features: u32, - flags: u32, -) -> i32; - -fn get_krun_add_net_unixgram() -> KrunAddNetUnixgramFn { - let symbol = CString::new("krun_add_net_unixgram").unwrap(); - let ptr = unsafe { libc::dlsym(libc::RTLD_DEFAULT, symbol.as_ptr()) }; - assert!(!ptr.is_null(), "krun_add_net_unixgram not found"); - unsafe { std::mem::transmute(ptr) } -} - const VMNET_HELPER_PATH: &str = match option_env!("VMNET_HELPER_PATH") { Some(path) => path, None => "/opt/homebrew/opt/vmnet-helper/libexec/vmnet-helper",