Our problem#

There is a problem we have with mapping…. we have no way of (knowing for sure) we are mapping it to the address we want (high virtual memory). Yes, in AllocatePages() there is an option for allocating an address, but the UEFI standard gurantees absolutely no individual address being free, so even if we did allocate an address (definitely not the address we had in the linker, as UEFI paging is identity-mapped, and I very much bet you don’t have 128 tb of RAM for 0xFFFFFFFF80000000) there’s no way we would know it would be available beyond qemu, or another firmware where they are implementing the UEFI standard completely different doing God knows what.

So, we are forced to use our own page tables. Kinda, we will just be using the UEFI identity mapped pages but an entry dedicated for our Hypervisor. We’ll eventually switch off them, don’t worry. We won’t use identity mapping forever.

so, let’s make a new .rs file called “initial_paging.rs” and populate it some simple stuff.

use core::{
    ptr::{self},
    usize,
};

use log::info;
use uefi::boot::{MemoryType, PAGE_SIZE, allocate_pages};

// On some firmware, they use 2 mb paging, on some they don't. I will use 4kb paging because it's basic and it doesn't really matter if they are using 2mb if we do 4 kib.
pub fn size_to_pages(size: usize) -> usize {
    (size / PAGE_SIZE) + if size % PAGE_SIZE > 0 { 1 } else { 0 }
}

pub fn uefi_allocator(size: usize) -> *mut u8 {
    info!("Allocating {} pages", size_to_pages(size));
    let address = allocate_pages(uefi::boot::AllocateType::AnyPages, MemoryType::LOADER_DATA, size_to_pages(size)).unwrap();

    // Zero it out
    unsafe {
        ptr::write_bytes(address.as_ptr() as *mut u8, 0x00, size);
    }

    address.as_ptr()
}

Alright, you might be saying something about your UEFI implementation using 2mb pages or something.. Indeed, we are only dividing the size into 4 kib pages. There is no official standard of the page sizes we use in UEFI so we have to practice some defensive programming and just divide into 4 kib pages, if you want to probe for different page sizes your system is using you can.

Next let’s make a function to copy the CR3.


pub fn copy_uefi_cr3() -> *mut PML4 {
    let new_ptr = uefi_allocator(size_of::<PML4>());

    let uefi_cr3 = unsafe { cr3() };

    unsafe { new_ptr.copy_from(uefi_cr3 as *const u8, size_of::<PML4>()) };

    let cr3 = new_ptr as *mut PML4;

    cr3
}

Easy!

Now let’s copy the CR3 and switch to it and see if we get any crashes.

#![no_std]
#![no_main]

mod loader;
mod initial_paging;

const FILE_NAME: &str = "hv.bin";

use log::info;
use uefi::{boot::{self, open_protocol_exclusive, ScopedProtocol}, entry, fs::{FileSystem, Path}, proto::{loaded_image::LoadedImage, media::fs::SimpleFileSystem}, CStr16, Status};
use crate::{initial_paging::copy_uefi_cr3};
use x86::controlregs::cr3_write;


fn map_hv_binary(sfs: ScopedProtocol<SimpleFileSystem>) {
    let mut fs = FileSystem::new(sfs);

    let mut name_buffer = [0; 7];

    let cstr = CStr16::from_str_with_buf(FILE_NAME, &mut name_buffer).unwrap();

    let path = Path::new(&cstr);

    let buffer = fs.read(path).unwrap();

    let cr3 = copy_uefi_cr3();

    unsafe { cr3_write(cr3.addr() as u64) };

    info!("Switched CR3");
}

#[entry]
fn main() -> Status {
    uefi::helpers::init().unwrap();

    let loaded_image = open_protocol_exclusive::<LoadedImage>(boot::image_handle()).unwrap();

    let sfs = boot::open_protocol_exclusive::<SimpleFileSystem>(loaded_image.device().unwrap()).unwrap();

    map_hv_binary(sfs);

    Status::SUCCESS
}

We are going to be testing it with qemu, let’s create an xtask so we can easily write a command to start qemu. In the root directory, make a new project with cargo new and simply do this:

use std::{env, fs::{self, create_dir}, io::{self, Error}, path::{Path, PathBuf}, process::Command};

fn get_project_root() -> PathBuf {
    Path::new(&env!("CARGO_MANIFEST_DIR")).ancestors().nth(1).unwrap().to_path_buf()
}

fn paths() -> io::Result<()> {

    if !fs::exists(get_project_root().join("qemu"))? && !fs::exists(get_project_root().join("qemu/drive"))? {
        create_dir(get_project_root().join("qemu"))?;
        create_dir(get_project_root().join("qemu/drive"))?;
        let error = Error::new(io::ErrorKind::Other, "Please add the OVMF file to the qemu folder now.");
        return Err(error);
    }

    fs::exists(get_project_root().join("qemu/OVMF.fd"))?;

    fs::copy(get_project_root().join("target/hv_x86_64/debug/hv.bin"), get_project_root().join("qemu/drive/hv.bin"))?;

    fs::copy(get_project_root().join("target/x86_64-unknown-uefi/debug/hv_uefi.efi"), get_project_root().join("qemu/drive/hv_uefi.efi"))?;

    Ok(())
}

fn try_qemu() -> io::Result<()> {

    paths()?;

    Command::new("qemu-system-x86_64").arg("-net").arg("none").arg("-bios").arg("qemu/OVMF.fd").arg("fat:rw:qemu/drive/").arg("-device").arg("virtio-scsi-pci,id=scsi0").arg("-debugcon").arg("file:qemu/uefi_debug.log").arg("-global").arg("isa-debugcon.iobase=0x402").output().expect("Failed to execute QEMU.");

    Ok(())
}


fn main() {
    if let Err(e) = try_qemu() {
        println!("{}", e);
        std::process::exit(-1);
    }
}

And make a new directory, “qemu” and put OVMF.fd you get from the Internet there. You can find many prebuilt images online or compile it yourself using the EDK2 toolchain. It isn’t that hard.

Now, let’s compile our UEFI driver with cargo build –target x86_64-unknown-uefi. Then, cd back into the root directory and type cargo xtask. Qemu with the OVMF firmware should pop up.

qemu

Easy!

Ok… now to test it.. Let’s right click the qemu window and click “compatmonitor0” and type info tlb qemu_tlb

This is the the cached memory addresses in QEMU. As you can see, as per the UEFI specification, it is identity mapped up to address ffffe00000 for me. now let’s start our UEFI program and see if the mappings are the same..

uefi_program_started

No crashes yet… Let’s input info tlb again in the monitor and see if its all good.

switched_cr3

Seems all good!