diff --git a/rust/stub/src/main.rs b/rust/stub/src/main.rs index 7850482..da26155 100644 --- a/rust/stub/src/main.rs +++ b/rust/stub/src/main.rs @@ -2,20 +2,22 @@ #![no_std] #![feature(abi_efiapi)] #![feature(negative_impls)] +#![deny(unsafe_op_in_unsafe_fn)] extern crate alloc; mod linux_loader; +mod pe_loader; mod pe_section; mod uefi_helpers; +use pe_loader::Image; use pe_section::{pe_section, pe_section_as_string}; use sha2::{Digest, Sha256}; use uefi::{ prelude::*, proto::{ console::text::Output, - loaded_image::LoadedImage, media::file::{File, FileAttribute, FileMode, RegularFile}, }, CString16, Result, @@ -165,37 +167,13 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { let kernel_cmdline = booted_image_cmdline(system_table.boot_services()).expect("Failed to fetch command line"); - let kernel_handle = { - system_table - .boot_services() - .load_image( - handle, - uefi::table::boot::LoadImageSource::FromBuffer { - buffer: &kernel_data, - file_path: None, - }, - ) - .expect("UEFI refused to load the kernel image. It may not be signed or it may not have an EFI stub.") - }; - - let mut kernel_image = system_table - .boot_services() - .open_protocol_exclusive::(kernel_handle) - .expect("Failed to open the LoadedImage protocol"); - - unsafe { - kernel_image.set_load_options( - kernel_cmdline.as_ptr() as *const u8, - u32::try_from(kernel_cmdline.len()).unwrap(), - ); - } + let kernel = + Image::load(system_table.boot_services(), &kernel_data).expect("Failed to load the kernel"); let mut initrd_loader = InitrdLoader::new(system_table.boot_services(), handle, initrd_data) .expect("Failed to load the initrd. It may not be there or it is not signed"); - let status = system_table - .boot_services() - .start_image(kernel_handle) - .status(); + + let status = unsafe { kernel.start(handle, &system_table, &kernel_cmdline) }; initrd_loader .uninstall(system_table.boot_services()) diff --git a/rust/stub/src/pe_loader.rs b/rust/stub/src/pe_loader.rs new file mode 100644 index 0000000..9ce5305 --- /dev/null +++ b/rust/stub/src/pe_loader.rs @@ -0,0 +1,176 @@ +use core::ffi::c_void; + +use alloc::vec::Vec; +use goblin::pe::PE; +use uefi::{ + prelude::BootServices, + proto::loaded_image::LoadedImage, + table::{ + boot::{AllocateType, MemoryType}, + Boot, SystemTable, + }, + Handle, Status, +}; + +/// UEFI mandates 4 KiB pages. +const UEFI_PAGE_BITS: usize = 12; +const UEFI_PAGE_MASK: usize = (1 << UEFI_PAGE_BITS) - 1; + +#[cfg(target_arch = "x86_64")] +fn flush_instruction_cache(_start: *const u8, _length: usize) { + // x86_64 mandates coherent instruction cache +} + +pub struct Image { + image: &'static mut [u8], + entry: extern "efiapi" fn(Handle, SystemTable) -> Status, +} + +/// Converts a length in bytes to the number of required pages. +fn bytes_to_pages(bytes: usize) -> usize { + bytes + .checked_add(UEFI_PAGE_MASK) + .map(|rounded_up| rounded_up >> UEFI_PAGE_BITS) + .unwrap_or(1 << (usize::try_from(usize::BITS).unwrap() - UEFI_PAGE_BITS)) +} + +impl Image { + /// Loads and relocates a PE file. + /// + /// The image must be handed to [`start`] later. If this does not + /// happen, the memory allocated for the unpacked PE binary will + /// leak. + pub fn load(boot_services: &BootServices, file_data: &[u8]) -> uefi::Result { + let pe = PE::parse(file_data).map_err(|_| Status::LOAD_ERROR)?; + + // Allocate all memory the image will need in virtual memory. + // We follow shim here and allocate as EfiLoaderCode. + let image = { + let section_lengths = pe + .sections + .iter() + .map(|section| { + section + .virtual_address + .checked_add(section.virtual_size) + .ok_or(Status::LOAD_ERROR) + }) + .collect::, uefi::Status>>()?; + + let length = usize::try_from(section_lengths.into_iter().max().unwrap_or(0)).unwrap(); + + let base = boot_services.allocate_pages( + AllocateType::AnyPages, + MemoryType::LOADER_CODE, + bytes_to_pages(length), + )? as *mut u8; + + unsafe { + core::ptr::write_bytes(base, 0, length); + core::slice::from_raw_parts_mut(base, length) + } + }; + + // Populate all sections in virtual memory. + for section in &pe.sections { + let copy_size = + usize::try_from(u32::min(section.virtual_size, section.size_of_raw_data)).unwrap(); + let raw_start = usize::try_from(section.pointer_to_raw_data).unwrap(); + let raw_end = raw_start.checked_add(copy_size).ok_or(Status::LOAD_ERROR)?; + let virt_start = usize::try_from(section.virtual_address).unwrap(); + let virt_end = virt_start + .checked_add(copy_size) + .ok_or(Status::LOAD_ERROR)?; + + if virt_end > image.len() || raw_end > file_data.len() { + return Err(Status::LOAD_ERROR.into()); + } + image[virt_start..virt_end].copy_from_slice(&file_data[raw_start..raw_end]); + } + + // Image base relocations are not supported. + if pe + .header + .optional_header + .and_then(|h| *h.data_directories.get_base_relocation_table()) + .is_some() + { + return Err(Status::INCOMPATIBLE_VERSION.into()); + } + + flush_instruction_cache(image.as_ptr(), image.len()); + + if pe.entry >= image.len() { + return Err(Status::LOAD_ERROR.into()); + } + let entry = unsafe { core::mem::transmute(&image[pe.entry]) }; + + Ok(Image { image, entry }) + } + + /// Starts a trusted loaded PE file. + /// The caller is responsible for verifying that it trusts the PE file to uphold the invariants detailed below. + /// If the entry point returns, the image memory is subsequently deallocated. + /// + /// # Safety + /// The image is assumed to be trusted. This means: + /// * The PE file it was loaded from must have been a completely valid EFI application of the correct architecture. + /// * If the entry point returns, it must leave the system in a state that allows our stub to continue. + /// In particular: + /// * Only memory it either has allocated, or that belongs to the image, should have been altered. + /// * Memory it has not allocated should not have been freed. + /// * Boot services must not have been exited. + pub unsafe fn start( + self, + handle: Handle, + system_table: &SystemTable, + load_options: &[u8], + ) -> Status { + let mut loaded_image = system_table + .boot_services() + .open_protocol_exclusive::(handle) + .expect("Failed to open the LoadedImage protocol"); + + let (our_data, our_size) = loaded_image.info(); + let our_load_options = loaded_image + .load_options_as_bytes() + .map(|options| options.as_ptr_range()); + + // It seems to be impossible to allocate custom image handles. + // Hence, we reuse our own for the kernel. + // The shim does the same thing. + unsafe { + loaded_image.set_image( + self.image.as_ptr() as *const c_void, + self.image.len().try_into().unwrap(), + ); + loaded_image.set_load_options( + load_options.as_ptr() as *const u8, + u32::try_from(load_options.len()).unwrap(), + ); + } + + let status = (self.entry)(handle, unsafe { system_table.unsafe_clone() }); + + // If the kernel has exited boot services, it must not return any more, and has full control over the entire machine. + // If the kernel entry point returned, deallocate its image, and restore our loaded image handle. + // If it calls Exit(), that call returns directly to systemd-boot. This unfortunately causes a resource leak. + system_table + .boot_services() + .free_pages(self.image.as_ptr() as u64, bytes_to_pages(self.image.len())) + .expect("Double free attempted"); + + unsafe { + loaded_image.set_image(our_data, our_size); + match our_load_options { + Some(options) => loaded_image.set_load_options( + options.start, + options.end.offset_from(options.start).try_into().unwrap(), + ), + None => loaded_image.set_load_options(core::ptr::null(), 0), + } + } + + status + } +}