diff options
| -rw-r--r-- | test/long_mode/behavior.rs | 820 |
1 files changed, 646 insertions, 174 deletions
diff --git a/test/long_mode/behavior.rs b/test/long_mode/behavior.rs index 2c8715b..b1b67e5 100644 --- a/test/long_mode/behavior.rs +++ b/test/long_mode/behavior.rs @@ -8,6 +8,7 @@ mod kvm { KVM_GUESTDBG_ENABLE, KVM_GUESTDBG_SINGLESTEP, }; + use yaxpeax_x86::long_mode; use yaxpeax_x86::long_mode::behavior::Exception; use rand::prelude::*; @@ -16,20 +17,28 @@ mod kvm { /// /// there is one CPU which is configured for long-mode execution. all memory is /// identity-mapped with 1GiB pages. page tables are configured to cover 512 GiB of memory, but - /// much much lss than that is actually allocated and usable through `memory.` + /// much much less than that is actually allocated and usable through `memory.` /// /// it is configured with `mem_size` bytes of memory at guest address 0, accessible through - /// host pointer `memory`. + /// host pointer `memory`. this region is used for "control structures"; page tables, GDT, IDT, + /// and stack. `test_memory` and `test_mem_size` describe an additional region intended for + /// instruction reads and writes. #[allow(unused)] struct TestVm { vm: VmFd, vcpu: VcpuFd, + idt_configured: bool, memory: *mut u8, mem_size: usize, + test_memory: *mut u8, + test_mem_size: usize, } const GB: u64 = 1 << 30; + // TODO: cite APM/SDM + const IDT_ENTRIES: u16 = 256; + #[derive(Copy, Clone)] struct GuestAddress(u64); @@ -110,10 +119,24 @@ mod kvm { ) as *mut u8 }; + let test_mem_size = 9 * 128 * 1024; + let test_mem_addr: *mut u8 = unsafe { + libc::mmap( + core::ptr::null_mut(), + test_mem_size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_ANONYMOUS | libc::MAP_SHARED | libc::MAP_NORESERVE, + -1, + 0, + ) as *mut u8 + }; + assert!(!mem_addr.is_null()); + assert!(!test_mem_addr.is_null()); // look, mmap should only be in the business of returning page-aligned addresses but i // just wanna see it, you know... assert!(mem_addr as usize % 4096 == 0); + assert!(test_mem_addr as usize % 4096 == 0); let region = kvm_userspace_memory_region { slot: 0, @@ -129,10 +152,15 @@ mod kvm { let mut this = TestVm { vm, vcpu, + idt_configured: false, memory: mem_addr, mem_size, + test_memory: test_mem_addr, + test_mem_size, }; + this.map_test_mem(); + let mut vcpu_regs = this.vcpu.get_regs().unwrap(); let mut vcpu_sregs = this.vcpu.get_sregs().unwrap(); @@ -150,6 +178,49 @@ mod kvm { this } + // we need to keep accesses from falling into mapped-but-not-backed regions + // of guest memory, so we don't get MMIO exits (which would just test + // Linux's x86 emulation). control structures are at in the low 1G (really 1M) + // of memory, which memory references under test shoul not touch. + // + // we'll limit discriminants to 511 (arbitrary), which means that 512-byte + // increments of 1..16 can distinguish registers. given SIB addressing the + // highest address that can be formed is something like... + // + // > (1G + 15 * 512) + (1G + 16 * 512) * 8 + 512 + // + // or just under 9G + 16k. that access *could* be a wide AVX-512 situation, + // so the highest byte addressed can be a few bytes later. + // + // this can be read as "the first 32k at each 1G may be accessed", but only + // GB boundaries at 1, 2, 3, 5, and 9 can be accessed in this way (non-SIB, + // then SIB with scale = 1, 2, 4, 8). + // + // while memory is Yikes Expensive, setting up 128k at each 1G offset that might be + // accessed is only 1M 128K, so that's what we'll do here. + fn map_test_mem(&mut self) { + // arbitrayish, but does ned to be greater than a few bytes larger than 16k. see above. + const GB_CHUNK_SIZE: u64 = 128 * 1024; + for i in 0..=8 { + eprintln!("mapping chunk {}", i); + let host_test_offset = i * GB_CHUNK_SIZE; + assert!(host_test_offset + GB_CHUNK_SIZE <= self.test_mem_size as u64); + + let host_ptr = unsafe { + self.test_memory.offset(host_test_offset as isize) as u64 + }; + + let region = kvm_userspace_memory_region { + slot: 1 + i as u32, + guest_phys_addr: 0x1_0000_0000 * (1 + i), + memory_size: GB_CHUNK_SIZE, + userspace_addr: host_ptr, + flags: 0, + }; + unsafe { self.vm.set_user_memory_region(region).unwrap() }; + } + } + // TODO: seems like there's a KVM bug where if the VM is configured for single-step and the // single-stepped instruction is a read-modify-write to MMIO memory, the single-step // doesn't actually take effect. compare `0x33 0x00` and `0x31 0x00`. what the hell! @@ -164,13 +235,30 @@ mod kvm { } fn run(&mut self) -> VcpuExit<'_> { - self.vcpu.run().unwrap() + self.vcpu.run().unwrap_or_else(|e| { + panic!("error running vcpu: {}", e); + }) } unsafe fn host_ptr(&self, address: GuestAddress) -> *mut u8 { + assert!(address.0 < self.mem_size as u64); self.memory.offset(address.0 as isize) } + unsafe fn testmem_ptr(&self, address: GuestAddress) -> *mut u8 { + let upper = address.0 >> 32; + let lower = address.0 & 0xffff_ffff; + + eprintln!("upper: {}", upper); + // see comment on map_test_mem for why this bounds check is not totally bonkers + assert!(upper >= 1 && upper <= 9); + // again, see map_test_mem + assert!(lower < 128 * 1024); + + let testmem_offset = 128 * 1024 * (upper - 1) + lower; + self.test_memory.offset(testmem_offset as isize) + } + fn gdt_addr(&self) -> GuestAddress { GuestAddress(0x1000) } @@ -208,7 +296,7 @@ mod kvm { // stack grows *down* but if someone pops a lot of bytes from rsp we'd go up and // clobber the page tables. so leave a bit of space. - GuestAddress(0xf800) + GuestAddress(0x19800) } /// selector 0x10 is used for code everywhere in these tests. @@ -229,6 +317,18 @@ mod kvm { assert!(self.mem_size as u64 >= end); } + fn check_testrange(&self, base: GuestAddress, size: u64) { + let base = base.0; + assert!(base >= 0x1_0000_0000); + + let test_chunk = base >> 32; + assert!(test_chunk < 0xa); + let test_offset = base & 0xffff_ffff; + let end = test_offset.checked_add(size).expect("no overflow"); + + assert!(end < 128 * 1024); + } + pub fn write_mem(&mut self, addr: GuestAddress, data: &[u8]) { self.check_range(addr, data.len() as u64); @@ -243,6 +343,58 @@ mod kvm { } } + pub fn read_mem(&mut self, addr: GuestAddress, buf: &mut [u8]) { + self.check_range(addr, buf.len() as u64); + + // SAFETY: `check_range` above validates the range to copy, and... please do not + // provide a slice of guest memory as what should be read into... + unsafe { + std::ptr::copy_nonoverlapping( + self.host_ptr(addr) as *const _, + buf.as_mut_ptr(), + buf.len() + ); + } + } + + pub fn test_mem(&self) -> &[u8] { + // SAFETY: since this is &mut self we know the VM is not running and will not be + // running as long as this slice exists. so there are no concurrent readers or writers + // of this slice. + unsafe { + std::slice::from_raw_parts( + self.test_memory, + self.test_mem_size + ) + } + } + + pub fn test_mem_mut(&mut self) -> &mut [u8] { + // SAFETY: since this is &mut self we know the VM is not running and will not be + // running as long as this slice exists. so there are no concurrent readers or writers + // of this slice. + unsafe { + std::slice::from_raw_parts_mut( + self.test_memory, + self.test_mem_size + ) + } + } + + pub fn write_testmem(&mut self, addr: GuestAddress, data: &[u8]) { + self.check_testrange(addr, data.len() as u64); + + // SAFETY: `check_range` above validates the range to copy, and... please do not + // provide a slice of guest memory as what the guest should be programmed for... + unsafe { + std::ptr::copy_nonoverlapping( + data.as_ptr(), + self.testmem_ptr(addr), + data.len() + ); + } + } + pub fn program(&mut self, code: &[u8], regs: &mut kvm_regs) { let addr = self.code_addr(); self.write_mem(addr, code); @@ -432,7 +584,6 @@ mod kvm { } fn configure_idt(&mut self, regs: &mut kvm_regs, sregs: &mut kvm_sregs) { - const IDT_ENTRIES: u16 = 256; sregs.idt.base = self.idt_addr().0; sregs.idt.limit = IDT_ENTRIES * 16 - 1; // IDT is 256 entries of 16 bytes each @@ -465,6 +616,7 @@ mod kvm { // we might just have to limit possible rsp permutations so as to be able to test in // 16- and 32-bit modes anyway. regs.rsp = self.stack_addr().0; + self.idt_configured = true; } } @@ -488,14 +640,16 @@ mod kvm { after: u64, } - struct AccessTestCtx<'regs> { - regs: &'regs mut kvm_regs, + struct AccessTestCtx<'a> { + regs: &'a mut kvm_regs, + vm: &'a TestVm, + preserve_rsp: bool, used_regs: [bool; 16], expected_reg: Vec<ExpectedRegAccess>, expected_mem: Vec<ExpectedMemAccess>, } - impl<'regs> AccessTestCtx<'regs> { + impl<'a> AccessTestCtx<'a> { fn into_expectations(self) -> (Vec<ExpectedRegAccess>, Vec<ExpectedMemAccess>) { let AccessTestCtx { expected_reg, @@ -510,7 +664,7 @@ mod kvm { use yaxpeax_x86::long_mode::{RegSpec, behavior::AccessVisitor}; use yaxpeax_x86::long_mode::register_class; - impl<'regs> AccessVisitor for AccessTestCtx<'regs> { + impl<'a> AccessVisitor for AccessTestCtx<'a> { fn register_read(&mut self, reg: RegSpec) { self.expected_reg.push(ExpectedRegAccess { write: false, @@ -534,24 +688,23 @@ mod kvm { 8, 9, 10, 11, 12, 13, 14, 15, ]; let kvm_reg_nr = KVM_REG_LUT[reg.num() as usize]; - if self.used_regs[reg.num() as usize] { + + // some ridiculous circumstances require us to not permute rsp, even + // though we *would* set it to a mapped address. + let allocated = self.used_regs[reg.num() as usize] || + (reg.num() == RegSpec::rsp().num() && self.preserve_rsp); + + if allocated { let value = unsafe { (self.regs as *mut _ as *mut u64).offset(kvm_reg_nr as isize).read() }; Some(value) } else { - // register value allocation is done carefully to keep memory accesses out - // of the first 1G of memory. that keeps test instructions from clobbering - // page tables. registers used for memory access are set to at least to 8G, - // so that disp32 offsets can cause an instruction to access only as early - // as 4G. SIB addressing means the highest address that may be accessed - // could be 8G + 0xf00_0000 + (8G + 0x1000_0000) * 4 + 2G, or somewhere - // around 42G. + // register value allocation is done .. carefully. // - // since the highest accessible address is (probably) 2^48 or 256T, even in - // the most convoluted case we're always going to be forming canonical - // (lower-half) addresses. - let value = 0x2_0000_0000 + (kvm_reg_nr as u64 + 1) * 0x100_0000; + // see the comment on `map_test_mem` about why these numbers make any + // sense. + let value = 0x1_0000_0000 + (kvm_reg_nr as u64 + 1) * 0x0200; unsafe { (self.regs as *mut _ as *mut u64).offset(kvm_reg_nr as isize).write(value); } @@ -587,6 +740,17 @@ mod kvm { let end_pc = loop { eprintln!("about to run! here's some state:"); let regs = vm.vcpu.get_regs().unwrap(); + + unsafe { + let bytes = vm.host_ptr(GuestAddress(regs.rip)); + let slc = std::slice::from_raw_parts(bytes, 15); + let decoded = yaxpeax_x86::long_mode::InstDecoder::default() + .decode_slice(slc); + if let Ok(decoded) = decoded { + eprintln!("step. next: {:06x}: {}", regs.rip, decoded); + } + } + dump_regs(®s); // let sregs = vm.vcpu.get_sregs().unwrap(); // eprintln!("sregs: {:?}", sregs); @@ -595,13 +759,17 @@ mod kvm { match exit { VcpuExit::MmioRead(addr, buf) => { eprintln!("mmio: [{:08x}:{}] <- ..", addr, buf.len()); - // TODO: better + // TODO: with expected memory accesses we should be able to perhaps pick some + // values ahead of time and permute them (so as to tickle flags changes in + // `add [rcx], rdi` for example. buf.fill(1); } VcpuExit::MmioWrite(addr, buf) => { eprintln!("mmio: .. -> [{:08x}:{}]", addr, buf.len()); } VcpuExit::Debug(info) => { + let regs = vm.vcpu.get_regs().unwrap(); + dump_regs(®s); unsafe { let bytes = vm.host_ptr(GuestAddress(info.pc)); let slc = std::slice::from_raw_parts(bytes, 15); @@ -636,6 +804,23 @@ mod kvm { return; } + fn exception_exit(vm: &TestVm) -> Option<Exception> { + let regs = vm.vcpu.get_regs().unwrap(); + let intr_handler_base = vm.interrupt_handlers_start(); + + // by the time we've exited the `hlt` of the interrupt handler has completed, so rip is + // advanced by one. subtract back out to convert to an exception vector number. + let intr_start = regs.rip - 1; + + if intr_start >= intr_handler_base.0 && intr_start < intr_handler_base.0 + IDT_ENTRIES as u64 { + Some(Exception::vector( + (intr_start - intr_handler_base.0).try_into().expect("handler offset is in range") + )) + } else { + None + } + } + fn dump_regs(regs: &kvm_regs) { eprintln!("rip flags "); eprintln!("{:016x} {:016x}", regs.rip, regs.rflags); @@ -649,51 +834,46 @@ mod kvm { eprintln!("{:016x} {:016x} {:016x} {:016x}", regs.r12, regs.r13, regs.r14, regs.r15); } - fn run_with_mem_checks(vm: &mut TestVm, expected_end: u64, expected_mem: &[ExpectedMemAccess]) { - let mut expected_mem = expected_mem.to_vec(); - let mut unexpected_mem = Vec::new(); + fn run_with_mem_checks(vm: &mut TestVm, expected_end: u64) -> Result<(), Exception> { + vm.test_mem_mut().fill(0xaa); let mut exits = 0; let end_pc = loop { eprintln!("about to run! here's some state:"); let regs = vm.vcpu.get_regs().unwrap(); - eprintln!("regs: {:?}", regs); + dump_regs(®s); // let sregs = vm.vcpu.get_sregs().unwrap(); // eprintln!("sregs: {:?}", sregs); let exit = vm.run(); exits += 1; match exit { VcpuExit::MmioRead(addr, buf) => { - let position = expected_mem.iter().position(|e| { - e.addr == addr && e.size as usize == buf.len() && e.write == false - }); - - if let Some(position) = position { - expected_mem.swap_remove(position); - } else { - unexpected_mem.push((false, addr, buf.len())); - } - // TODO: better - buf.fill(1); + panic!("shoud not be mmio accesses anymore"); } VcpuExit::MmioWrite(addr, buf) => { - let position = expected_mem.iter().position(|e| { - e.addr == addr && e.size as usize == buf.len() && e.write - }); - - if let Some(position) = position { - expected_mem.swap_remove(position); - } else { - unexpected_mem.push((true, addr, buf.len())); - } - - // TODO: verify write? probably can't without full semantics. + panic!("shoud not be mmio accesses anymore"); } VcpuExit::Debug(info) => { break info.pc; } VcpuExit::Hlt => { let regs = vm.vcpu.get_regs().unwrap(); - break regs.rip; + eprintln!("hit hlt"); + dump_regs(®s); + let intr_handler_base = vm.interrupt_handlers_start(); + + // by the time we've exited the `hlt` of the interrupt handler has completed, so rip is + // advanced by one. subtract back out to convert to an exception vector number. + let intr_start = regs.rip - 1; + + if intr_start >= intr_handler_base.0 && intr_start < intr_handler_base.0 + IDT_ENTRIES as u64 { + let exception = Exception::vector( + (intr_start - intr_handler_base.0).try_into().expect("handler offset is in range") + ); + eprintln!("VM exited at exception: {:?}", exception); + return Err(exception); + } else { + break regs.rip; + } } other => { eprintln!("unhandled exit: {:?} ... after {}", other, exits); @@ -710,6 +890,7 @@ mod kvm { panic!("single-step ended at {:08x}, expected {:08x}", end_pc, expected_end); } + /* if !unexpected_mem.is_empty() { eprintln!("memory access surprise!"); if expected_mem.is_empty() { @@ -728,7 +909,8 @@ mod kvm { } panic!("stop"); } - return; + */ + return Ok(()); } fn check_contains(larger: RegSpec, smaller: RegSpec) -> bool { @@ -781,7 +963,9 @@ mod kvm { // but rex byte regs are all low-byte register_class::RB => (diff & !0xff) == 0, register_class::W => (diff & !0xffff) == 0, - register_class::D => (diff & !0xffffffff) == 0, + // x86_64 zero-extends 32-bit writes to 64-bit, so writes to "32-bit" registers still + // are fully-clobbers. + register_class::D => (diff & !0xffffffff_ffffffff) == 0, register_class::Q => (diff & !0xffffffff_ffffffff) == 0, register_class::RFLAGS => (diff & !0xffffffff_ffffffff) == 0, other => { @@ -860,110 +1044,7 @@ mod kvm { } } - fn verify_reg_changes( - expected_regs: &[ExpectedRegAccess], - before_regs: kvm_regs, after_regs: kvm_regs, - before_sregs: kvm_sregs, after_sregs: kvm_sregs - ) { - let mut unexpected_regs = Vec::new(); - - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rax(), before_regs.rax, after_regs.rax); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rcx(), before_regs.rcx, after_regs.rcx); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rdx(), before_regs.rdx, after_regs.rdx); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rbx(), before_regs.rbx, after_regs.rbx); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rsp(), before_regs.rsp, after_regs.rsp); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rbp(), before_regs.rbp, after_regs.rbp); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rsi(), before_regs.rsi, after_regs.rsi); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rdi(), before_regs.rdi, after_regs.rdi); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::r8(), before_regs.r8, after_regs.r8); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::r9(), before_regs.r9, after_regs.r9); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::r10(), before_regs.r10, after_regs.r10); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::r11(), before_regs.r11, after_regs.r11); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::r12(), before_regs.r12, after_regs.r12); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::r13(), before_regs.r13, after_regs.r13); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::r14(), before_regs.r14, after_regs.r14); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::r15(), before_regs.r15, after_regs.r15); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rflags(), before_regs.rflags, after_regs.rflags); - - verify_seg(&mut unexpected_regs, &expected_regs, RegSpec::cs(), before_sregs.cs.selector, after_sregs.cs.selector); - verify_seg(&mut unexpected_regs, &expected_regs, RegSpec::ds(), before_sregs.ds.selector, after_sregs.ds.selector); - verify_seg(&mut unexpected_regs, &expected_regs, RegSpec::es(), before_sregs.es.selector, after_sregs.es.selector); - verify_seg(&mut unexpected_regs, &expected_regs, RegSpec::fs(), before_sregs.fs.selector, after_sregs.fs.selector); - verify_seg(&mut unexpected_regs, &expected_regs, RegSpec::gs(), before_sregs.gs.selector, after_sregs.gs.selector); - verify_seg(&mut unexpected_regs, &expected_regs, RegSpec::ss(), before_sregs.ss.selector, after_sregs.ss.selector); - - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::cr0(), before_sregs.cr0, after_sregs.cr0); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::cr2(), before_sregs.cr2, after_sregs.cr2); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::cr3(), before_sregs.cr3, after_sregs.cr3); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::cr4(), before_sregs.cr4, after_sregs.cr4); - verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::cr8(), before_sregs.cr8, after_sregs.cr8); - - if !unexpected_regs.is_empty() { - eprintln!("unexpected reg changes:"); - for change in unexpected_regs { - eprintln!(" {}: {:08x} -> {:08x}", change.reg.name(), change.before, change.after); - } - panic!("stop"); - } - } - - fn check_behavior(vm: &mut TestVm, inst: &[u8]) { - let mut insts = inst.to_vec(); - // cap things off with a `hlt` to work around single-step sometimes .. not? see comment on - // set_single_step. this ensures that even if single-stepping doesn't do the needful, the - // next address _will_ get the vCPU back out to us. - // - // this obviously doesn't work if code is overwritten (so really [TODO] the first page - // should be made non-writable), and doesn't work if the one executed instruction is a - // call, jump, etc. in those cases the instruction doesn't rmw memory .. .except for - // call/ret, where the `rsp` access might. so we might have to just have to skip them? - // - // alternatively, probably should set up the IDT such that there's a handler for the - // exception raised by `TF` that just executes hlt. then everything other than popf will - // work out of the box and popf can be caught by kvm single-stepping. - insts.push(0xf4); - let decoded = yaxpeax_x86::long_mode::InstDecoder::default() - .decode_slice(inst).expect("can decode"); - use yaxpeax_arch::LengthedInstruction; - assert_eq!(insts.len(), 0.wrapping_offset(decoded.len()) as usize + 1); - let behavior = decoded.behavior(); - eprintln!("checking behavior of {}", decoded); - - let before_sregs = vm.vcpu.get_sregs().unwrap(); - let mut regs = vm.vcpu.get_regs().unwrap(); - vm.set_single_step(true); - vm.program(insts.as_slice(), &mut regs); - - let mut rng = rand::rng(); - - regs.rax = rng.next_u64(); - regs.rbx = rng.next_u64(); - regs.rcx = rng.next_u64(); - regs.rdx = rng.next_u64(); - regs.rsp = rng.next_u64(); - regs.rbp = rng.next_u64(); - regs.rsi = rng.next_u64(); - regs.rdi = rng.next_u64(); - - regs.r8 = rng.next_u64(); - regs.r9 = rng.next_u64(); - regs.r10 = rng.next_u64(); - regs.r11 = rng.next_u64(); - regs.r12 = rng.next_u64(); - regs.r13 = rng.next_u64(); - regs.r14 = rng.next_u64(); - regs.r15 = rng.next_u64(); - - let mut ctx = AccessTestCtx { - regs: &mut regs, - used_regs: [false; 16], - expected_reg: Vec::new(), - expected_mem: Vec::new(), - }; - behavior.visit_accesses(&mut ctx).expect("can visit accesses"); - let (expected_reg, expected_mem) = ctx.into_expectations(); - - fn compute_dontcares(accesses: &[ExpectedRegAccess]) -> Vec<RegSpec> { + fn compute_dontcares(vm: &TestVm, accesses: &[ExpectedRegAccess]) -> Vec<RegSpec> { // use a bitmap for dontcares, mask out bits as registers are seen to be read. let mut reg_bitmap: u32 = 0xffffffff; @@ -984,6 +1065,10 @@ mod kvm { } } + if vm.idt_configured { + reg_bitmap &= !(1 << (RegSpec::rsp().num())); + } + for acc in accesses.iter() { if acc.write { continue; @@ -1047,9 +1132,6 @@ mod kvm { regs } - let dontcare_regs = compute_dontcares(&expected_reg); - let written_regs = compute_writes(&expected_reg); - fn permute_dontcares(dontcare_regs: &[RegSpec], regs: &mut kvm_regs) { let mut rng = rand::rng(); @@ -1068,30 +1150,367 @@ mod kvm { } } - permute_dontcares(dontcare_regs.as_slice(), &mut regs); + fn permute_memdontcare(expected_mem: &[ExpectedMemAccess], vm: &mut TestVm) { + for acc in expected_mem.iter() { + if acc.write { + continue; + } - eprintln!("setting regs to: {:?}", regs); - vm.vcpu.set_regs(®s).unwrap(); + /* + * WRONG + let mut buf = vec![0; acc.size as usize]; + let mut rng = rand::rng(); + rng.fill(&mut buf); + + if acc.addr >= 0x1_0000_0000 { + vm.write_testmem(GuestAddress(acc.addr), buf.as_slice()); + } else { + // check we're not going to "permute" page tables or something. + // instruction text might get clobbered, which would be Weird, but.. + assert!(acc.addr > vm.page_table_addr().0 + 2 * 0x1000); + vm.write_mem(GuestAddress(acc.addr), buf.as_slice()); + } + */ + } + } + + fn verify_mem_changes( + expected_mem: &[ExpectedMemAccess], + vm: &TestVm, + ) { + // test the expected writes by process of elimination: reset any expected-to-be-written + // areas to the initial pattern. then, anything in test memory that is not the default + // pattern must have been an unexpected write. + for acc in expected_mem { + if !acc.write { + continue; + } + + if acc.addr >= 0x1_0000_0000 { + unsafe { + let ptr = vm.testmem_ptr(GuestAddress(acc.addr)); + let slice = std::slice::from_raw_parts_mut(ptr, acc.size as usize); + slice.fill(0xaa); + } + } else { + unsafe { + let ptr = vm.host_ptr(GuestAddress(acc.addr)); + let slice = std::slice::from_raw_parts_mut(ptr, acc.size as usize); + slice.fill(0xaa); + } + } + } + + struct MemoryDiff { + addr: GuestAddress, + bytes: Vec<u8>, + } + + use std::fmt; + + impl fmt::Display for MemoryDiff { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "diff at 0x{:08x}: ", self.addr.0)?; + for b in self.bytes.iter() { + write!(f, "{:02x}", b)?; + } + Ok(()) + } + } - run_with_mem_checks(vm, regs.rip + insts.len() as u64, expected_mem.as_slice()); + let mut unexpected_acc = Vec::new(); + let mut current_diff: Option<MemoryDiff> = None; - let initial_after_regs = vm.vcpu.get_regs().unwrap(); + let test_mem = vm.test_mem(); + for i in 0..test_mem.len() { + if let Some(mut diff) = current_diff.take() { + if test_mem[i] != 0xaa { + diff.bytes.push(test_mem[i]); + } else { + unexpected_acc.push(diff); + } + } else { + if test_mem[i] != 0xaa { + const CHUNK_SIZE: usize = 128 * 1024; + let guest_test_chunk = i / CHUNK_SIZE; + let guest_addr = (guest_test_chunk + 1) * 0x1_0000_0000 + i % CHUNK_SIZE; + current_diff = Some(MemoryDiff { + addr: GuestAddress(guest_addr as u64), + bytes: vec![test_mem[i]], + }); + } + } + } + + if !unexpected_acc.is_empty() { + for diff in unexpected_acc { + eprintln!("{}", diff); + } + panic!("unexpected memory accesses!"); + } + } + + fn verify_reg_changes( + expected_regs: &[ExpectedRegAccess], + before_regs: &kvm_regs, after_regs: &kvm_regs, + before_sregs: &kvm_sregs, after_sregs: &kvm_sregs + ) { + let mut unexpected_regs = Vec::new(); + + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rax(), before_regs.rax, after_regs.rax); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rcx(), before_regs.rcx, after_regs.rcx); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rdx(), before_regs.rdx, after_regs.rdx); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rbx(), before_regs.rbx, after_regs.rbx); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rsp(), before_regs.rsp, after_regs.rsp); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rbp(), before_regs.rbp, after_regs.rbp); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rsi(), before_regs.rsi, after_regs.rsi); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rdi(), before_regs.rdi, after_regs.rdi); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::r8(), before_regs.r8, after_regs.r8); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::r9(), before_regs.r9, after_regs.r9); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::r10(), before_regs.r10, after_regs.r10); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::r11(), before_regs.r11, after_regs.r11); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::r12(), before_regs.r12, after_regs.r12); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::r13(), before_regs.r13, after_regs.r13); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::r14(), before_regs.r14, after_regs.r14); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::r15(), before_regs.r15, after_regs.r15); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::rflags(), before_regs.rflags, after_regs.rflags); + + verify_seg(&mut unexpected_regs, &expected_regs, RegSpec::cs(), before_sregs.cs.selector, after_sregs.cs.selector); + verify_seg(&mut unexpected_regs, &expected_regs, RegSpec::ds(), before_sregs.ds.selector, after_sregs.ds.selector); + verify_seg(&mut unexpected_regs, &expected_regs, RegSpec::es(), before_sregs.es.selector, after_sregs.es.selector); + verify_seg(&mut unexpected_regs, &expected_regs, RegSpec::fs(), before_sregs.fs.selector, after_sregs.fs.selector); + verify_seg(&mut unexpected_regs, &expected_regs, RegSpec::gs(), before_sregs.gs.selector, after_sregs.gs.selector); + verify_seg(&mut unexpected_regs, &expected_regs, RegSpec::ss(), before_sregs.ss.selector, after_sregs.ss.selector); + + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::cr0(), before_sregs.cr0, after_sregs.cr0); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::cr2(), before_sregs.cr2, after_sregs.cr2); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::cr3(), before_sregs.cr3, after_sregs.cr3); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::cr4(), before_sregs.cr4, after_sregs.cr4); + verify_reg(&mut unexpected_regs, &expected_regs, RegSpec::cr8(), before_sregs.cr8, after_sregs.cr8); + + if !unexpected_regs.is_empty() { + eprintln!("unexpected reg changes:"); + for change in unexpected_regs { + eprintln!(" {}: {:08x} -> {:08x}", change.reg.name(), change.before, change.after); + } + panic!("stop"); + } + } + + // check the side effects of the instruction that `regs.rip` points to. the side effects are + // enumerated across `expected_reg` and `expected_mem`. if this instruction instead raises an + // exception, return that instead. + // + // TODO: it's possible that this instruction permuts either the instruction bytes or vCPU + // control structures (GDT, IDT, or page tables). these could be made read-only, but then we'd + // need to verify that these structures are not modified via Weird Different Mapping or + // whatever. such a mapping shouldn't exist anyway. but making these read-only also implies + // moving the stack elsewhere, and the stack would have to be zeroed to not introduce Weirdness + // across permutations too. + fn check_side_effects( + vm: &mut TestVm, regs: &kvm_regs, sregs: &kvm_sregs, + expected_end: u64, expected_reg: &[ExpectedRegAccess], expected_mem: &[ExpectedMemAccess] + ) -> Result<(kvm_regs, kvm_sregs), Exception> { + run_with_mem_checks(vm, expected_end)?; + + let after_regs = vm.vcpu.get_regs().unwrap(); let after_sregs = vm.vcpu.get_sregs().unwrap(); - verify_reg_changes(&expected_reg, regs, initial_after_regs, before_sregs, after_sregs); + verify_reg_changes(&expected_reg, ®s, &after_regs, &sregs, &after_sregs); + verify_mem_changes(&expected_mem, &vm); + + Ok((after_regs, after_sregs)) + } + // run the VM a few times permuting the "dontcare" registers each time and checking that we + // really did not care about them. "4" steps is of course arbitrary, but makes for some kind of + // confidence about flag registers in particular, probably. + fn test_dontcares( + vm: &mut TestVm, regs: &mut kvm_regs, sregs: &kvm_sregs, + expected_end: u64, expected_reg: &[ExpectedRegAccess], expected_mem: &[ExpectedMemAccess], + dontcare_regs: &[RegSpec], written_regs: &[RegSpec], + first_after_regs: &kvm_regs, _first_after_sregs: &kvm_sregs + ) -> Result<(), Exception> { for _ in 0..4 { - permute_dontcares(dontcare_regs.as_slice(), &mut regs); + permute_dontcares(dontcare_regs, regs); + // TODO: + // permute_memread(expected_mem, vm); vm.vcpu.set_regs(®s).unwrap(); - run_with_mem_checks(vm, regs.rip + insts.len() as u64, expected_mem.as_slice()); + let (after_regs, _after_sregs) = check_side_effects( + vm, ®s, &sregs, + expected_end, expected_reg, expected_mem + )?; + + verify_dontcares(written_regs, &first_after_regs, &after_regs); + } + + Ok(()) + } - let after_regs = vm.vcpu.get_regs().unwrap(); - let after_sregs = vm.vcpu.get_sregs().unwrap(); + fn inrange_displacements(vm: &TestVm, inst: &long_mode::Instruction) -> bool { + // see comment on `map_test_mem`. this limit is partially used to figure out what memory + // must be backed by real memory vs holes that can have mmio traps. + let disp_lim = 511; - verify_reg_changes(&expected_reg, regs, after_regs, before_sregs, after_sregs); - verify_dontcares(written_regs.as_slice(), &initial_after_regs, &after_regs); + let ops = match inst.behavior().all_operands() { + Ok(ops) => ops, + Err(e) => { + // TODO: is it true that all ComplexOp do not have displacements? + return true; + } + }; + for op in ops.iter().operands() { + let disp = match op { + long_mode::Operand::AbsoluteU32 { .. } | + long_mode::Operand::AbsoluteU64 { .. } => { + return false; + } + long_mode::Operand::Disp { disp, .. } => disp, + long_mode::Operand::MemIndexScaleDisp { disp, .. } => disp, + long_mode::Operand::MemBaseIndexScaleDisp { disp, .. } => disp, + long_mode::Operand::DispMasked { disp, .. } => disp, + long_mode::Operand::MemIndexScaleDispMasked { disp, .. } => disp, + long_mode::Operand::MemBaseIndexScaleDispMasked { disp, .. } => disp, + _ => { + continue; + } + }; + + if disp > disp_lim { + return false; + } + } + + true + } + + fn check_behavior(vm: &mut TestVm, inst: &[u8]) { + let mut insts = inst.to_vec(); + // cap things off with a `hlt` to work around single-step sometimes .. not? see comment on + // set_single_step. this ensures that even if single-stepping doesn't do the needful, the + // next address _will_ get the vCPU back out to us. + // + // this obviously doesn't work if code is overwritten (so really [TODO] the first page + // should be made non-writable), and doesn't work if the one executed instruction is a + // call, jump, etc. in those cases the instruction doesn't rmw memory .. .except for + // call/ret, where the `rsp` access might. so we might have to just have to skip them? + // + // alternatively, probably should set up the IDT such that there's a handler for the + // exception raised by `TF` that just executes hlt. then everything other than popf will + // work out of the box and popf can be caught by kvm single-stepping. + insts.push(0xf4); + let decoded = yaxpeax_x86::long_mode::InstDecoder::default() + .decode_slice(inst).expect("can decode"); + use yaxpeax_arch::LengthedInstruction; + assert_eq!(insts.len(), 0.wrapping_offset(decoded.len()) as usize + 1); + + if !inrange_displacements(vm, &decoded) { + panic!("unable to test '{}': displacement(s) are larger than test VM memory.", decoded); + } + + let behavior = decoded.behavior(); + eprintln!("checking behavior of {}", decoded); + + let sregs = vm.vcpu.get_sregs().unwrap(); + let mut regs = vm.vcpu.get_regs().unwrap(); + // vm.set_single_step(true); + vm.program(insts.as_slice(), &mut regs); + + let mut rng = rand::rng(); + + regs.rax = rng.next_u64(); + regs.rbx = rng.next_u64(); + regs.rcx = rng.next_u64(); + regs.rdx = rng.next_u64(); + if !vm.idt_configured { + regs.rsp = rng.next_u64(); + } + regs.rbp = rng.next_u64(); + regs.rsi = rng.next_u64(); + regs.rdi = rng.next_u64(); + + regs.r8 = rng.next_u64(); + regs.r9 = rng.next_u64(); + regs.r10 = rng.next_u64(); + regs.r11 = rng.next_u64(); + regs.r12 = rng.next_u64(); + regs.r13 = rng.next_u64(); + regs.r14 = rng.next_u64(); + regs.r15 = rng.next_u64(); + + let mut ctx = AccessTestCtx { + regs: &mut regs, + vm, + // if an interrupt handler is initialized with rsp pointing to addresses that cause + // MMIO exits the vcpu ends up in a loop doing nothing particularly interesting + // (seemingly in a loop trying to raise #UD after resetting?). this is a Linux issue + // i'm not tracking down right now. instead, if the IDT is initialized then keep the + // rsp pointed somewhere "normal" so that exceptions still work right. + // + // to reproduce this issue, set this to `false` unconditionally, then run + // `kvm_verify_popmem`. it will infinite loop in the kernel and you'll see + // x86_decode_emulated_instruction failing over and over and over and ... + preserve_rsp: vm.idt_configured, + used_regs: [false; 16], + expected_reg: Vec::new(), + expected_mem: Vec::new(), + }; + behavior.visit_accesses(&mut ctx).expect("can visit accesses"); + let (expected_reg, expected_mem) = ctx.into_expectations(); + + let dontcare_regs = compute_dontcares(&vm, &expected_reg); + let written_regs = compute_writes(&expected_reg); + + permute_dontcares(dontcare_regs.as_slice(), &mut regs); + + eprintln!("setting regs to: {:?}", regs); + vm.vcpu.set_regs(®s).unwrap(); + + let expected_end = regs.rip + insts.len() as u64; + + let (after_regs, after_sregs) = match check_side_effects(vm, ®s, &sregs, expected_end, &expected_reg, &expected_mem) { + Ok((a, b)) => (a, b), + Err(other) => { + let vm_regs = vm.vcpu.get_regs().unwrap(); + let vm_sregs = vm.vcpu.get_sregs().unwrap(); + let mut prev_rip = [0u8; 8]; + vm.read_mem(GuestAddress(vm_regs.rsp + 8), &mut prev_rip[..]); + let mut buf = [0u8; 8]; + vm.read_mem(GuestAddress(vm_regs.rsp), &mut buf[..]); + eprintln!( + "error code: {:#08x} accessing {:016x} @ rip={:#016x} (cr3={:016x})", + u64::from_le_bytes(buf), vm_sregs.cr2, + u64::from_le_bytes(prev_rip), vm_sregs.cr3 + ); + if other == Exception::PF { + let mut pdpt = [0u8; 4096]; + vm.read_mem(vm.page_tables().pdpt_addr(), &mut pdpt[..]); + eprintln!("pdpt: {:x?}", &pdpt[..8]); + } + panic!("TODO: handle exceptions ({:?})", other); + } + }; + + let res = test_dontcares( + vm, &mut regs, &sregs, + expected_end, expected_reg.as_slice(), expected_mem.as_slice(), + dontcare_regs.as_slice(), written_regs.as_slice(), + &after_regs, &after_sregs + ); + + match res { + Ok(()) => { + let mut pdpt = [0u8; 4096]; + vm.read_mem(vm.page_tables().pdpt_addr(), &mut pdpt[..]); + eprintln!("pdpt: {:x?}", &pdpt[..8]); + } + Err(Exception::PF) => { + } + Err(other) => { + panic!("TODO: handle exceptions ({:?})", other); + } } } @@ -1136,6 +1555,52 @@ mod kvm { } #[test] + fn kvm_verify_popmem() { + let mut vm = TestVm::create(); + + // `pop [rax]` + let inst: &'static [u8] = &[0x8f, 0x00]; + check_behavior(&mut vm, &inst[0..2]); + } + + // #[test] + fn kvm_hugepage_bug() { + let mut vm = TestVm::create(); + + // `add [rsp], al; add [rcx], al; pop [rcx]; hlt` + // the first instruction runs fine. the second instruction runs fine. + // the third instruction gets a page fault at 0xf800? which worked fine for the add. + // this turns out to be an issue in linux' paging64_gva_to_gpa() when the va is mapped with + // huge pages. + let inst: &'static [u8] = &[0x00, 0x04, 0x24, 0x00, 0x01, 0x8f, 0x01, 0xf4]; + let mut regs = vm.vcpu.get_regs().unwrap(); + regs.rax = 0x00000002_00100000; + regs.rcx = 0x00000002_00100000; + vm.program(inst, &mut regs); + vm.vcpu.set_regs(®s).unwrap(); + vm.set_single_step(true); + run(&mut vm); + + let vm_regs = vm.vcpu.get_regs().unwrap(); + let vm_sregs = vm.vcpu.get_sregs().unwrap(); + let mut prev_rip = [0u8; 8]; + vm.read_mem(GuestAddress(vm_regs.rsp + 8), &mut prev_rip[..]); + let mut buf = [0u8; 8]; + vm.read_mem(GuestAddress(vm_regs.rsp), &mut buf[..]); + eprintln!( + "error code: {:#08x} accessing {:016x} @ rip={:#016x} (cr3={:016x})", + u64::from_le_bytes(buf), vm_sregs.cr2, + u64::from_le_bytes(prev_rip), vm_sregs.cr3 + ); + if vm_regs.rip == 0x300f { + let mut pdpt = [0u8; 4096]; + vm.read_mem(vm.page_tables().pdpt_addr(), &mut pdpt[..]); + eprintln!("pdpt: {:x?}", &pdpt[..8]); + } + panic!("no"); + } + + #[test] fn kvm_verify_ins() { let mut vm = TestVm::create(); @@ -1207,10 +1672,8 @@ mod kvm { run(&mut vm); - let after_regs = vm.vcpu.get_regs().unwrap(); - - let bp_exit_addr: u64 = vm.interrupt_handlers_start().0 + Exception::BP.to_u8() as u64 + 1; - assert_eq!(after_regs.rip, bp_exit_addr); + let intr_exit = exception_exit(&vm); + assert_eq!(intr_exit, Some(Exception::BP)); } #[test] @@ -1222,6 +1685,7 @@ mod kvm { let decoder = InstDecoder::default(); let mut buf = Instruction::default(); + let initial_regs = vm.vcpu.get_regs().unwrap(); for word in 0..u16::MAX { let inst = word.to_le_bytes(); @@ -1236,6 +1700,14 @@ mod kvm { } else { eprintln!("checking behavior of {:02x} {:02x}: {}", inst[0], inst[1], buf); } + use yaxpeax_x86::long_mode::Opcode; + // mov es, word [rax] + // does an inf loop too...? + if [Opcode::MOV, Opcode::INS, Opcode::OUTS, Opcode::IN, Opcode::OUT].contains(&buf.opcode()) { + eprintln!("skipping {}", buf.opcode()); + continue; + } + vm.vcpu.set_regs(&initial_regs).unwrap(); check_behavior(&mut vm, &inst[..inst_len]); } } |
