From 5329930ff4998448fc34548f5c611d01dd0590d6 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sun, 25 Aug 2024 18:10:15 -0700 Subject: [PATCH 1/3] rust-tests: Some RISC-V emulation tests There were previously no RISC-V emulation tests at all. Now there is a happy-path test of successfully executing a single instruction, along with two different ways of hooking an invalid instruction (using either general interrupt hook or invalid instruction hook) and also using general interrupt hook to handle an ecall instruction. --- tests/rust-tests/main.rs | 218 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 216 insertions(+), 2 deletions(-) diff --git a/tests/rust-tests/main.rs b/tests/rust-tests/main.rs index b35e395f34..f8561c4d03 100644 --- a/tests/rust-tests/main.rs +++ b/tests/rust-tests/main.rs @@ -3,9 +3,11 @@ extern crate alloc; use alloc::rc::Rc; use core::cell::RefCell; use unicorn_engine::unicorn_const::{ - uc_error, Arch, HookType, MemType, Mode, Permission, SECOND_SCALE, TlbEntry, TlbType + uc_error, Arch, HookType, MemType, Mode, Permission, TlbEntry, TlbType, SECOND_SCALE, +}; +use unicorn_engine::{ + InsnSysX86, RegisterARM, RegisterMIPS, RegisterPPC, RegisterRISCV, RegisterX86, Unicorn, }; -use unicorn_engine::{InsnSysX86, RegisterARM, RegisterMIPS, RegisterPPC, RegisterX86, Unicorn}; pub static X86_REGISTERS: [RegisterX86; 125] = [ RegisterX86::AH, @@ -623,6 +625,218 @@ fn emulate_ppc() { assert_eq!(emu.reg_read(RegisterPPC::R26), Ok(1379)); } +#[test] +fn emulate_riscv64() { + let riscv_code: Vec = vec![0x13, 0x05, 0xa5, 0x00]; // addi a0, a0, 10 + + let mut emu = unicorn_engine::Unicorn::new(Arch::RISCV, Mode::RISCV64) + .expect("failed to initialize unicorn instance"); + assert_eq!(emu.reg_write(RegisterRISCV::A0, 123), Ok(())); + assert_eq!(emu.reg_read(RegisterRISCV::A0), Ok(123)); + + // Attempt to write to memory before mapping it. + assert_eq!( + emu.mem_write(0x1000, &riscv_code), + (Err(uc_error::WRITE_UNMAPPED)) + ); + + assert_eq!(emu.mem_map(0x1000, 0x4000, Permission::ALL), Ok(())); + assert_eq!(emu.mem_write(0x1000, &riscv_code), Ok(())); + assert_eq!( + emu.mem_read_as_vec(0x1000, riscv_code.len()), + Ok(riscv_code.clone()) + ); + + assert_eq!( + emu.emu_start( + 0x1000, + (0x1000 + riscv_code.len()) as u64, + 10 * SECOND_SCALE, + 1000 + ), + Ok(()) + ); + assert_eq!(emu.reg_read(RegisterRISCV::A0), Ok(123 + 10)); +} + +#[test] +fn emulate_riscv64_invalid_insn_hook() { + let riscv_code: Vec = vec![0x73, 0x10, 0x00, 0xc0]; // "unimp" + + struct Data { + invalid_hook_called: bool, + } + + let mut emu = unicorn_engine::Unicorn::new_with_data( + Arch::RISCV, + Mode::RISCV64, + Data { + invalid_hook_called: false, + }, + ) + .expect("failed to initialize unicorn instance"); + + // Attempt to write to memory before mapping it. + assert_eq!( + emu.mem_write(0x1000, &riscv_code), + (Err(uc_error::WRITE_UNMAPPED)) + ); + + assert_eq!(emu.mem_map(0x1000, 0x4000, Permission::ALL), Ok(())); + assert_eq!(emu.mem_write(0x1000, &riscv_code), Ok(())); + assert_eq!( + emu.mem_read_as_vec(0x1000, riscv_code.len()), + Ok(riscv_code.clone()) + ); + + emu.add_insn_invalid_hook(|emu| { + let data = emu.get_data_mut(); + data.invalid_hook_called = true; + emu.emu_stop().expect("failed to stop"); + false + }) + .expect("failed to add invalid instruction hook"); + + assert_eq!( + emu.emu_start( + 0x1000, + (0x1000 + riscv_code.len()) as u64, + 10 * SECOND_SCALE, + 1000 + ), + Ok(()) + ); + + assert!( + emu.get_data().invalid_hook_called, + "invalid instruction hook was not called" + ); +} + +#[test] +fn emulate_riscv64_invalid_insn_interrupt() { + let riscv_code: Vec = vec![0x73, 0x10, 0x00, 0xc0]; // "unimp" + + struct Data { + hook_calls: usize, + mcause: Option, + } + + let mut emu = unicorn_engine::Unicorn::new_with_data( + Arch::RISCV, + Mode::RISCV64, + Data { + hook_calls: 0, + mcause: None, + }, + ) + .expect("failed to initialize unicorn instance"); + + // Attempt to write to memory before mapping it. + assert_eq!( + emu.mem_write(0x1000, &riscv_code), + (Err(uc_error::WRITE_UNMAPPED)) + ); + + assert_eq!(emu.mem_map(0x1000, 0x4000, Permission::ALL), Ok(())); + assert_eq!(emu.mem_write(0x1000, &riscv_code), Ok(())); + assert_eq!( + emu.mem_read_as_vec(0x1000, riscv_code.len()), + Ok(riscv_code.clone()) + ); + + emu.add_intr_hook(|emu, mcause| { + let data = emu.get_data_mut(); + data.hook_calls += 1; + data.mcause = Some(mcause); + emu.emu_stop().expect("failed to stop"); + }) + .expect("failed to add interrupt hook"); + + assert_eq!( + emu.emu_start( + 0x1000, + (0x1000 + riscv_code.len()) as u64, + 10 * SECOND_SCALE, + 1000 + ), + Ok(()) + ); + + assert_eq!( + emu.get_data().hook_calls, + 1, + "interrupt hook should have been called exactly once" + ); + assert_eq!( + emu.get_data().mcause, + Some(2_u32), + "wrong mcause value for illegal instruction" + ); +} + +#[test] +fn emulate_riscv64_ecall_interrupt() { + let riscv_code: Vec = vec![0x73, 0x00, 0x00, 0x00]; // ecall + + struct Data { + hook_calls: usize, + mcause: Option, + } + + let mut emu = unicorn_engine::Unicorn::new_with_data( + Arch::RISCV, + Mode::RISCV64, + Data { + hook_calls: 0, + mcause: None, + }, + ) + .expect("failed to initialize unicorn instance"); + + // Attempt to write to memory before mapping it. + assert_eq!( + emu.mem_write(0x1000, &riscv_code), + (Err(uc_error::WRITE_UNMAPPED)) + ); + + assert_eq!(emu.mem_map(0x1000, 0x4000, Permission::ALL), Ok(())); + assert_eq!(emu.mem_write(0x1000, &riscv_code), Ok(())); + assert_eq!( + emu.mem_read_as_vec(0x1000, riscv_code.len()), + Ok(riscv_code.clone()) + ); + + emu.add_intr_hook(|emu, mcause| { + let data = emu.get_data_mut(); + data.hook_calls += 1; + data.mcause = Some(mcause); + emu.emu_stop().expect("failed to stop"); + }) + .expect("failed to add interrupt hook"); + + assert_eq!( + emu.emu_start( + 0x1000, + (0x1000 + riscv_code.len()) as u64, + 10 * SECOND_SCALE, + 1000 + ), + Ok(()) + ); + + assert_eq!( + emu.get_data().hook_calls, + 1, + "interrupt hook should have been called exactly once" + ); + assert_eq!( + emu.get_data().mcause, + Some(8_u32), + "wrong mcause value for ecall from U-Mode" + ); +} + #[test] fn mem_unmapping() { let mut emu = unicorn_engine::Unicorn::new(Arch::X86, Mode::MODE_32) From 761b0c9f8f1dc136a3720583fb585eef1393d839 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sun, 25 Aug 2024 19:21:22 -0700 Subject: [PATCH 2/3] rust-tests: handling of RISC-V memory access errors --- tests/rust-tests/main.rs | 174 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/tests/rust-tests/main.rs b/tests/rust-tests/main.rs index f8561c4d03..a7dd462d29 100644 --- a/tests/rust-tests/main.rs +++ b/tests/rust-tests/main.rs @@ -775,6 +775,180 @@ fn emulate_riscv64_invalid_insn_interrupt() { ); } +#[test] +fn emulate_riscv64_mem_error_hook() { + let riscv_code: Vec = vec![ + 0x17, 0x45, 0x01, 0x00, // auipc a0,0x14 + 0x13, 0x05, 0x85, 0x00, // addi a0,a0,8 # 14008 + 0x03, 0x25, 0x05, 0x00, // lw a0,0(a0) + ]; + + struct Data { + hook_calls: usize, + call: Option, + } + #[derive(Debug, PartialEq)] + struct HookCall { + typ: MemType, + addr: u64, + size: usize, + } + + let mut emu = unicorn_engine::Unicorn::new_with_data( + Arch::RISCV, + Mode::RISCV64, + Data { + hook_calls: 0, + call: None, + }, + ) + .expect("failed to initialize unicorn instance"); + + // Attempt to write to memory before mapping it. + assert_eq!( + emu.mem_write(0x1000, &riscv_code), + (Err(uc_error::WRITE_UNMAPPED)) + ); + + assert_eq!(emu.mem_map(0x1000, 0x4000, Permission::ALL), Ok(())); + assert_eq!(emu.mem_write(0x1000, &riscv_code), Ok(())); + assert_eq!( + emu.mem_read_as_vec(0x1000, riscv_code.len()), + Ok(riscv_code.clone()) + ); + + emu.ctl_tlb_type(unicorn_engine::TlbType::VIRTUAL) + .expect("failed to select virtual TLB"); + emu.add_tlb_hook(0, !0, |_, vaddr, _| { + if vaddr < 0x4000 { + // The first page is identity-mapped. + Some(TlbEntry { + paddr: vaddr, + perms: Permission::ALL, + }) + } else { + // All other memory is unmapped + None + } + }) + .expect("failed to add TLB hook"); + + emu.add_mem_hook(HookType::MEM_INVALID, 0, !0, |emu, typ, addr, size, _| { + let data = emu.get_data_mut(); + data.hook_calls += 1; + data.call = Some(HookCall { typ, addr, size }); + false + }) + .expect("failed to add memory hook"); + + assert_eq!( + emu.emu_start( + 0x1000, + (0x1000 + riscv_code.len()) as u64, + 10 * SECOND_SCALE, + 1000 + ), + Ok(()) + ); + + assert_eq!( + emu.get_data().hook_calls, + 1, + "interrupt hook should have been called exactly once" + ); + assert_eq!( + emu.get_data().call, + Some(HookCall { + typ: MemType::READ_PROT, + addr: !0, + size: 8, + }), + "wrong hook call for read from unmapped memory" + ); +} + +#[test] +fn emulate_riscv64_mem_error_interrupt() { + let riscv_code: Vec = vec![ + 0x17, 0x45, 0x01, 0x00, // auipc a0,0x14 + 0x13, 0x05, 0x85, 0x00, // addi a0,a0,8 # 14008 + 0x03, 0x25, 0x05, 0x00, // lw a0,0(a0) + ]; + + struct Data { + hook_calls: usize, + mcause: Option, + } + + let mut emu = unicorn_engine::Unicorn::new_with_data( + Arch::RISCV, + Mode::RISCV64, + Data { + hook_calls: 0, + mcause: None, + }, + ) + .expect("failed to initialize unicorn instance"); + + // Attempt to write to memory before mapping it. + assert_eq!( + emu.mem_write(0x1000, &riscv_code), + (Err(uc_error::WRITE_UNMAPPED)) + ); + + assert_eq!(emu.mem_map(0x1000, 0x4000, Permission::ALL), Ok(())); + assert_eq!(emu.mem_write(0x1000, &riscv_code), Ok(())); + assert_eq!( + emu.mem_read_as_vec(0x1000, riscv_code.len()), + Ok(riscv_code.clone()) + ); + + emu.ctl_tlb_type(unicorn_engine::TlbType::VIRTUAL) + .expect("failed to select virtual TLB"); + emu.add_tlb_hook(0, !0, |_, vaddr, _| { + if vaddr < 0x4000 { + // The first page is identity-mapped. + Some(TlbEntry { + paddr: vaddr, + perms: Permission::ALL, + }) + } else { + // All other memory is unmapped + None + } + }) + .expect("failed to add TLB hook"); + + emu.add_intr_hook(|emu, mcause| { + let data = emu.get_data_mut(); + data.hook_calls += 1; + data.mcause = Some(mcause); + emu.emu_stop().expect("failed to stop"); + }) + .expect("failed to add interrupt hook"); + + assert_eq!( + emu.emu_start( + 0x1000, + (0x1000 + riscv_code.len()) as u64, + 10 * SECOND_SCALE, + 1000 + ), + Ok(()) + ); + + assert_eq!( + emu.get_data().hook_calls, + 1, + "interrupt hook should have been called exactly once" + ); + assert_eq!( + emu.get_data().mcause, + Some(13_u32), + "wrong mcause value for load page fault" + ); +} + #[test] fn emulate_riscv64_ecall_interrupt() { let riscv_code: Vec = vec![0x73, 0x00, 0x00, 0x00]; // ecall From 507494a087ad9d4f81a1267c282466d941861384 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Mon, 26 Aug 2024 10:15:21 -0700 Subject: [PATCH 3/3] rust-tests: Don't use virtual TLB mode in the memory error tests A TLB miss in virtual TLB mode causes Err(EXCEPTION) to be returned immediately, without calling any hooks or giving the target-specific code a chance to emulate a page fault exception. Therefore this now tests in the default TLB mode that uses target-specific TLB building logic. --- tests/rust-tests/main.rs | 49 ++++++++-------------------------------- 1 file changed, 9 insertions(+), 40 deletions(-) diff --git a/tests/rust-tests/main.rs b/tests/rust-tests/main.rs index a7dd462d29..da8e0fe5fc 100644 --- a/tests/rust-tests/main.rs +++ b/tests/rust-tests/main.rs @@ -693,7 +693,7 @@ fn emulate_riscv64_invalid_insn_hook() { let data = emu.get_data_mut(); data.invalid_hook_called = true; emu.emu_stop().expect("failed to stop"); - false + true }) .expect("failed to add invalid instruction hook"); @@ -704,7 +704,7 @@ fn emulate_riscv64_invalid_insn_hook() { 10 * SECOND_SCALE, 1000 ), - Ok(()) + Err(uc_error::EXCEPTION), // FIXME: Should return Ok(()) because the hook stopped emulation? ); assert!( @@ -817,27 +817,12 @@ fn emulate_riscv64_mem_error_hook() { Ok(riscv_code.clone()) ); - emu.ctl_tlb_type(unicorn_engine::TlbType::VIRTUAL) - .expect("failed to select virtual TLB"); - emu.add_tlb_hook(0, !0, |_, vaddr, _| { - if vaddr < 0x4000 { - // The first page is identity-mapped. - Some(TlbEntry { - paddr: vaddr, - perms: Permission::ALL, - }) - } else { - // All other memory is unmapped - None - } - }) - .expect("failed to add TLB hook"); - emu.add_mem_hook(HookType::MEM_INVALID, 0, !0, |emu, typ, addr, size, _| { let data = emu.get_data_mut(); data.hook_calls += 1; data.call = Some(HookCall { typ, addr, size }); - false + emu.emu_stop().expect("failed to stop emulation"); + true }) .expect("failed to add memory hook"); @@ -848,7 +833,7 @@ fn emulate_riscv64_mem_error_hook() { 10 * SECOND_SCALE, 1000 ), - Ok(()) + Err(uc_error::MAP), // FIXME: Should return Ok(()) because the hook stopped emulation? ); assert_eq!( @@ -859,9 +844,9 @@ fn emulate_riscv64_mem_error_hook() { assert_eq!( emu.get_data().call, Some(HookCall { - typ: MemType::READ_PROT, - addr: !0, - size: 8, + typ: MemType::READ_UNMAPPED, + addr: 0x15008, + size: 4, }), "wrong hook call for read from unmapped memory" ); @@ -903,22 +888,6 @@ fn emulate_riscv64_mem_error_interrupt() { Ok(riscv_code.clone()) ); - emu.ctl_tlb_type(unicorn_engine::TlbType::VIRTUAL) - .expect("failed to select virtual TLB"); - emu.add_tlb_hook(0, !0, |_, vaddr, _| { - if vaddr < 0x4000 { - // The first page is identity-mapped. - Some(TlbEntry { - paddr: vaddr, - perms: Permission::ALL, - }) - } else { - // All other memory is unmapped - None - } - }) - .expect("failed to add TLB hook"); - emu.add_intr_hook(|emu, mcause| { let data = emu.get_data_mut(); data.hook_calls += 1; @@ -934,7 +903,7 @@ fn emulate_riscv64_mem_error_interrupt() { 10 * SECOND_SCALE, 1000 ), - Ok(()) + Err(uc_error::READ_UNMAPPED), // FIXME: Should return Ok(()) because the hook stopped emulation? ); assert_eq!(