#x86-64 #unit-testing #vm #kvm #test-runner #os

nightly x86test

Custom test runner for bare-metal x86 tests

5 releases

0.0.5 Mar 3, 2022
0.0.4 Jan 25, 2022
0.0.3 Apr 24, 2020
0.0.2 Sep 12, 2019
0.0.1 Aug 24, 2019

#571 in Testing

MIT/Apache

2MB
9K SLoC

x86test custom test runner

x86test is a custom test runner that allows you to write unit tests which use privileged (x86) instructions.

It achieves that as follows: for every unit test it creates a tiny VM (using kvm) which mirrors the address space of the current test process inside the guest VM. Next the VM is initialized and jumps to the unit test function which is now executed in guest ring 0 (and here you can use all your fancy instructions). Finally, once the test returns (or panics), control is transferred back from the VM to our test runner.

Funky? Yes.

Is it hard to use? No! It integrates neatly with rust thanks to the rust custom test framework and procedural macros. See the example below.

Does it work? It has limitations (this is expected you're running on bare-metal x86), so don't expect much infrastructure. For panic and assert you have to use special versions, also you can't use anything that does system calls (like println!, but a custom sprintln! macro is provided).

An example

This is particularly helpful to test the x86 crate. For example say we have a function like this:

/// Read 16 bits from port
#[inline]
pub unsafe fn inw(port: u16) -> u16 {
    let ret: u16;
    asm!("inw %dx, %ax", in("dx") port, out("ax") ret, options(att_syntax));
    ret
}

The problem with inw is that it needs IO privilege level in E/RFlags to not cause an exception (and as a result crash the process). A regular Linux process will not run with this privilege level, however we can now write a x86test:

#[x86test(ioport(0x1, 0xfe))]
fn check_inw_port_read() {
    unsafe {
        kassert!(
            x86::io::inw(0x1) == 0xfe,
            "`inw` instruction didn't read the correct value"
        );
    }
}

A few things are happening here that warrant some explaining:

First, instead of #[test] we used #[x86test] to tell the system we don't want to use regular unit tests. x86test supports a few arguments (more on that later), here we just tell the "hypervisor" of the test runner to install an ioport with port number 1 that shall always return 0xfe when being read. Next, comes our function declaration -- nothing special here -- followed by unsafe, just because inw is unsafe. Finally, we use kassert!, a custom assert macro that works in guest ring 0 for our hypervisor, to check that inw does the right thing.

You'll find more example tests among the x86 tests. Note that running a x86test currently works only on Linux and requires some linking magic. Setting RUSTFLAGS="-C relocation-model=dynamic-no-pic -C code-model=kernel" should do. I expect the custom RUSTFLAGS to not be necessary in the future.

x86test reference

The x86test attribute currently supports the following parameters:

  • ioport(port, val): Reads to port will return val, writes to port other than val will fail the test.
  • ram(from, to): Adds physical memory in address range from -- to
  • should_halt: To tell the hypervisor that the test will halt (note: use like this #[x86test(should_halt)]).
  • #[should_panic]: Can be added if a test is expected to panic.

Code Organization

  • x86test_macro: contains a procedural macro implementation of x86test.
  • x86test_types: contains implementations of kassert, kpanic and the X86TestFn struct.
  • src: contains the custom test runner implementation.

Updating

Should be done in the following order:

  • Release new version of x86test-types
  • Release new version of x86test-macro (adjust version dependency of x86test-types)
  • Release new version of x86test (adjust version dependency of x86test-types and x86test-macro)
  • Tag with git tag x86test-0.0.x

Dependencies