blob: 70996f92d7ce8cef1ef3cc551945471f554e4d4c [file] [log] [blame]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::disk::GptDisk;
use crate::page_alloc::ScopedPageAllocation;
use crate::pe::{get_vbpubk_from_image, PeInfo};
use crate::{Error, Result};
use core::ffi::c_void;
use core::mem;
use log::info;
use uefi::proto::loaded_image::LoadedImage;
use uefi::table::boot::{AllocateType, BootServices, MemoryType};
use uefi::table::{Boot, SystemTable};
use uefi::{CStr16, CString16, Handle};
use vboot::{LoadKernelInputs, LoadedKernel};
type Entrypoint = unsafe extern "efiapi" fn(Handle, SystemTable<Boot>);
fn is_64bit() -> bool {
match mem::size_of::<usize>() {
8 => true,
4 => false,
other => panic!("invalid size of usize: {}", other),
}
}
/// Read a little-endian u32 field from with the kernel data at the
/// given `offset`.
fn get_u32_field(kernel_data: &[u8], offset: usize) -> Result<u32> {
let end = offset
.checked_add(mem::size_of::<u32>())
.ok_or(Error::Overflow("get_u32_field"))?;
let bytes = kernel_data
.get(offset..end)
.ok_or(Error::OutOfBounds("get_u32_field"))?;
// OK to unwrap, the length of the slice is already known to be correct.
Ok(u32::from_le_bytes(bytes.try_into().unwrap()))
}
/// Check that the kernel buffer is big enough to run the kernel without
/// relocation. This is done by comparing the buffer size with the
/// `init_size` field in the kernel boot header.
///
/// The layout of the header is described here:
/// <https://docs.kernel.org/x86/boot.html>
pub(crate) fn validate_kernel_buffer_size(kernel_buffer: &[u8]) -> Result<()> {
const SETUP_MAGIC: u32 = 0x5372_6448; // "HdrS"
const MAGIC_OFFSET: usize = 0x0202;
const INIT_SIZE_OFFSET: usize = 0x0260;
// Check that the correct header magic is present.
let magic = get_u32_field(kernel_buffer, MAGIC_OFFSET)?;
if magic != SETUP_MAGIC {
return Err(Error::InvalidKernelMagic);
}
// Get the `init_size` field.
let init_size = get_u32_field(kernel_buffer, INIT_SIZE_OFFSET)?;
info!("minimum required size: {}", init_size);
let init_size =
usize::try_from(init_size).map_err(|_| Error::Overflow("init_size"))?;
if init_size <= kernel_buffer.len() {
Ok(())
} else {
Err(Error::KernelBufferTooSmall(init_size, kernel_buffer.len()))
}
}
fn entry_point_from_offset(
data: &[u8],
entry_point_offset: u32,
) -> Result<Entrypoint> {
info!("entry_point_offset: 0x{:x}", entry_point_offset);
let entry_point_offset = usize::try_from(entry_point_offset)
.map_err(|_| Error::Overflow("entry_point_offset"))?;
// Ensure that the entry point is somewhere in the kernel data.
if entry_point_offset >= data.len() {
return Err(Error::OutOfBounds("entry_point_offset"));
}
unsafe {
let entry_point = data.as_ptr().add(entry_point_offset);
info!("entry_point: 0x{:x?}", entry_point);
// Transmute is needed to convert from a regular pointer to a
// function pointer:
// rust-lang.github.io/unsafe-code-guidelines/layout/function-pointers.html
let entry_point: Entrypoint = mem::transmute(entry_point);
Ok(entry_point)
}
}
fn modify_loaded_image(
bt: &BootServices,
kernel_data: &[u8],
cmdline_ucs2: &CStr16,
) -> Result<()> {
let mut li = bt
.open_protocol_exclusive::<LoadedImage>(bt.image_handle())
.map_err(|err| Error::LoadedImageProtocolMissing(err.status()))?;
// Set kernel command line.
let load_options_size = cmdline_ucs2.num_bytes();
let load_options_size = u32::try_from(load_options_size)
.map_err(|_| Error::CommandLineTooBig(load_options_size))?;
unsafe {
li.set_load_options(cmdline_ucs2.as_ptr().cast(), load_options_size);
}
// Set kernel data.
let image_size =
u64::try_from(kernel_data.len()).expect("usize is larger than u64");
unsafe {
li.set_image(kernel_data.as_ptr().cast::<c_void>(), image_size);
}
Ok(())
}
/// Hand off control to the Linux EFI stub.
///
/// As mentioned in [1], the preferred method for loading the kernel
/// on UEFI is to build in the EFI stub and run it as a normal PE/COFF
/// executable. This is indeed much simpler than trying to use the EFI
/// handover protocol, which is not fully documented. The kernel's PE
/// header does not require any relocations to be performed, so the
/// only thing we need to get from the header is the entry point.
///
/// Note that we can't use LoadImage+StartImage for this, because with
/// secure boot enabled it would try to verify the signature of the
/// kernel which would fail unless we signed the kernel in the way
/// UEFI expects. Since we have already verified the kernel via the
/// vboot structures (as well as the command line parameters), this
/// would be an unnecessary verification.
///
/// [1]: kernel.org/doc/html/latest/x86/boot.html#efi-handover-protocol-deprecated
fn execute_linux_efi_stub(
kernel_data: &[u8],
entry_point: Entrypoint,
system_table: SystemTable<Boot>,
cmdline: &str,
) -> Result<()> {
info!("booting the EFI stub");
// Convert the string to UCS-2.
let cmdline_ucs2 = CString16::try_from(cmdline)
.map_err(|_| Error::CommandLineUcs2ConversionFailed)?;
// Ideally we could create a new image here, but I'm not sure
// there's any way to do that without calling LoadImage, which we
// can't do due to secure boot. This is the same method shim uses:
// modify the existing image's parameters.
modify_loaded_image(
system_table.boot_services(),
kernel_data,
&cmdline_ucs2,
)?;
unsafe {
(entry_point)(
system_table.boot_services().image_handle(),
system_table,
);
}
Err(Error::KernelDidNotTakeControl)
}
fn execute_linux_kernel(
kernel: &LoadedKernel,
system_table: SystemTable<Boot>,
) -> Result<()> {
let cmdline = kernel.command_line().ok_or(Error::GetCommandLineFailed)?;
info!("command line: {}", cmdline);
let pe = PeInfo::parse(kernel.data())?;
let execute_linux_efi_stub = |system_table, entry_point_offset| {
execute_linux_efi_stub(
kernel.data(),
entry_point_from_offset(kernel.data(), entry_point_offset)?,
system_table,
&cmdline,
)
};
if is_64bit() {
execute_linux_efi_stub(system_table, pe.entry_point)
} else {
execute_linux_efi_stub(system_table, pe.ia32_compat_entry_point)
}
}
/// Use vboot to load the kernel from the appropriate kernel partition,
/// then execute it. If successful, this function will never return.
pub fn load_and_execute_kernel(system_table: SystemTable<Boot>) -> Result<()> {
let mut workbuf = ScopedPageAllocation::new(
// Safety: this system table clone will remain valid until
// ExitBootServices is called. That won't happen until after the
// kernel is executed, at which point crdyboot code is no longer
// running.
unsafe { system_table.unsafe_clone() },
AllocateType::AnyPages,
MemoryType::LOADER_DATA,
LoadKernelInputs::RECOMMENDED_WORKBUF_SIZE,
)?;
// Allocate a fairly large buffer. This buffer must be big enough to
// hold the kernel data loaded by vboot, but should also be big
// enough for the kernel to successfully run without relocation. The
// latter requirement is checked below with
// `validate_kernel_buffer_size`. The actual required size is
// currently around 35 MiB, so 64 MiB should be plenty for the
// forseeable future.
//
// This buffer will never be freed, unless loading or executing the
// kernel fails.
let mut kernel_buffer = ScopedPageAllocation::new(
// Safety: this system table clone will remain valid until
// ExitBootServices is called. That won't happen until after the
// kernel is executed, at which point crdyboot code is no longer
// running.
unsafe { system_table.unsafe_clone() },
AllocateType::AnyPages,
MemoryType::LOADER_CODE,
64 * 1024 * 1024,
)?;
let boot_services = system_table.boot_services();
let kernel_verification_key = get_vbpubk_from_image(boot_services)?;
info!(
"kernel_verification_key len={}",
kernel_verification_key.len()
);
let kernel = vboot::load_kernel(
LoadKernelInputs {
workbuf: &mut workbuf,
kernel_buffer: &mut kernel_buffer,
packed_pubkey: kernel_verification_key,
},
&mut GptDisk::new(boot_services)?,
)
.map_err(Error::LoadKernelFailed)?;
// Go ahead and free the workbuf, not needed anymore.
drop(workbuf);
// Ensure the buffer is large enough to actually run the
// kernel. We could just allocate a bigger buffer here, but it
// shouldn't be needed unless something has gone wrong anyway.
validate_kernel_buffer_size(kernel.data())?;
execute_linux_kernel(&kernel, system_table)
}