// Copyright 2024 The ChromiumOS Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. //! Multiboot kernel loader //! //! Only Multiboot (version 0.6.96) is supported, not Multiboot2. use std::fs::File; use std::mem::size_of; use std::num::NonZeroU32; use base::error; use base::trace; use base::FileReadWriteAtVolatile; use base::VolatileSlice; use resources::AddressRange; use vm_memory::GuestAddress; use vm_memory::GuestMemory; use crate::Error; use crate::LoadedKernel; use crate::Result; /// Multiboot header retrieved from a kernel image. #[derive(Clone, Debug)] pub struct MultibootKernel { /// Byte offset of the beginning of the multiboot header in the kernel image. pub offset: u32, /// Kernel requires that boot modules are aligned to 4 KB. pub boot_modules_page_aligned: bool, /// Kernel requires available memory information (`mem_*` fields). pub need_available_memory: bool, /// Kernel load address. /// /// If present, this overrides any other executable format headers (e.g. ELF). pub load: Option, /// Kernel preferred video mode. /// /// If present, the kernel also requires information about the video mode table. pub preferred_video_mode: Option, } /// Multiboot kernel load parameters. #[derive(Clone, Debug)] pub struct MultibootLoad { /// File byte offset to load the kernel's code and initialized data from. pub file_load_offset: u64, /// Number of bytes to read from the file at `file_load_offset`. pub file_load_size: usize, /// Physical memory address where the kernel should be loaded. pub load_addr: GuestAddress, /// Physical address of the kernel entry point. pub entry_addr: GuestAddress, /// BSS physical memory starting address to zero fill, if present in kernel. pub bss_addr: Option, /// BSS size in bytes (0 if no BSS region is present). pub bss_size: usize, } /// Multiboot kernel video mode specification. #[derive(Clone, Debug)] pub struct MultibootVideoMode { /// Preferred video mode type (text or graphics). pub mode_type: MultibootVideoModeType, /// Width of the requested mode. /// /// For text modes, this is in units of characters. For graphics modes, this is in units of /// pixels. pub width: Option, /// Height of the requested mode. /// /// For text modes, this is in units of characters. For graphics modes, this is in units of /// pixels. pub height: Option, /// Requested bits per pixel (only relevant in graphics modes). pub depth: Option, } #[derive(Copy, Clone, Debug)] pub enum MultibootVideoModeType { LinearGraphics, EgaText, Other(u32), } /// Scan the provided kernel file to find a Multiboot header, if present. /// /// # Returns /// /// - `Ok(None)`: kernel file did not contain a Multiboot header. /// - `Ok(Some(...))`: kernel file contained a valid Multiboot header, which is returned. /// - `Err(...)`: kernel file contained a Multiboot header with a valid checksum but other fields in /// the header were invalid. pub fn multiboot_header_from_file(kernel_file: &mut File) -> Result> { const MIN_HEADER_SIZE: usize = 3 * size_of::(); const ALIGNMENT: usize = 4; // Read up to 8192 bytes from the beginning of the file. let kernel_file_len = kernel_file.metadata().map_err(|_| Error::ReadHeader)?.len(); let kernel_prefix_len = kernel_file_len.min(8192) as usize; if kernel_prefix_len < MIN_HEADER_SIZE { return Ok(None); } let mut kernel_bytes = vec![0u8; kernel_prefix_len]; kernel_file .read_exact_at_volatile(VolatileSlice::new(&mut kernel_bytes), 0) .map_err(|_| Error::ReadHeader)?; for offset in (0..kernel_prefix_len).step_by(ALIGNMENT) { let Some(hdr) = kernel_bytes.get(offset..) else { break; }; match multiboot_header(hdr, offset as u64, kernel_file_len) { Ok(None) => continue, Ok(Some(multiboot)) => return Ok(Some(multiboot)), Err(e) => return Err(e), } } // The file did not contain a valid Multiboot header. Ok(None) } /// Attempt to parse a Multiboot header from the prefix of a slice. /// /// # Returns /// /// - `Ok(None)`: no multiboot header here. /// - `Ok(Some(...))`: valid multiboot header is returned. /// - `Err(...)`: valid multiboot header checksum at this position in the file (meaning this is the /// real header location), but there is an invalid field later in the multiboot header (e.g. an /// impossible combination of load addresses). fn multiboot_header( hdr: &[u8], offset: u64, kernel_file_len: u64, ) -> Result> { const MAGIC: u32 = 0x1BADB002; let Ok(magic) = get_le32(hdr, 0) else { return Ok(None); }; if magic != MAGIC { return Ok(None); } // Failing to read these fields means we ran out of data at the end of the slice and did not // actually find a Multiboot header, so return `Ok(None)` to indicate no Multiboot header was // found instead of using `?`, which would return an error. let Ok(flags) = get_le32(hdr, 4) else { return Ok(None); }; let Ok(checksum) = get_le32(hdr, 8) else { return Ok(None); }; if magic.wrapping_add(flags).wrapping_add(checksum) != 0 { // Checksum did not match, so this is not a real Multiboot header. Keep searching. return Ok(None); } trace!("found Multiboot header with valid checksum at {offset:#X}"); const F_BOOT_MODULE_PAGE_ALIGN: u32 = 1 << 0; const F_AVAILABLE_MEMORY: u32 = 1 << 1; const F_VIDEO_MODE: u32 = 1 << 2; const F_ADDRESS: u32 = 1 << 16; const KNOWN_FLAGS: u32 = F_BOOT_MODULE_PAGE_ALIGN | F_AVAILABLE_MEMORY | F_VIDEO_MODE | F_ADDRESS; let unknown_flags = flags & !KNOWN_FLAGS; if unknown_flags != 0 { error!("unknown flags {unknown_flags:#X}"); return Err(Error::InvalidFlags); } let boot_modules_page_aligned = flags & F_BOOT_MODULE_PAGE_ALIGN != 0; let need_available_memory = flags & F_AVAILABLE_MEMORY != 0; let need_video_mode_table = flags & F_VIDEO_MODE != 0; let load_address_available = flags & F_ADDRESS != 0; let load = if load_address_available { let header_addr = get_le32(hdr, 12)?; let load_addr = get_le32(hdr, 16)?; let load_end_addr = get_le32(hdr, 20)?; let bss_end_addr = get_le32(hdr, 24)?; let entry_addr = get_le32(hdr, 28)?; if header_addr < load_addr { error!("header_addr {header_addr:#X} < load_addr {load_addr:#X}"); return Err(Error::InvalidKernelOffset); } // The beginning of the area to load from the file starts `load_offset` bytes before the // multiboot header. let load_offset = u64::from(header_addr - load_addr); if load_offset > offset { error!("load_offset {load_offset:#X} > offset {offset:#X}"); return Err(Error::InvalidKernelOffset); } let file_load_offset = offset - load_offset; let file_load_size = if load_end_addr == 0 { // Zero `load_end_addr` means the loadable data extends to the end of the file. (kernel_file_len - file_load_offset) .try_into() .map_err(|_| Error::InvalidKernelOffset)? } else if load_end_addr < load_addr { error!("load_end_addr {load_end_addr:#X} < load_addr {load_addr:#X}"); return Err(Error::InvalidKernelOffset); } else { load_end_addr - load_addr }; let load_end_addr = load_addr .checked_add(file_load_size) .ok_or(Error::InvalidKernelOffset)?; // The bss region immediately follows the load-from-file region in memory. let bss_addr = load_addr + file_load_size; let bss_size = if bss_end_addr == 0 { // Zero `bss_end_addr` means no bss segment is present. 0 } else if bss_end_addr < bss_addr { error!("bss_end_addr {bss_end_addr:#X} < bss_addr {bss_addr:#X}"); return Err(Error::InvalidKernelOffset); } else { bss_end_addr - bss_addr }; let bss_addr = if bss_size > 0 { Some(GuestAddress(bss_addr.into())) } else { None }; if entry_addr < load_addr || entry_addr >= load_end_addr { error!( "entry_addr {entry_addr:#X} not in load range {load_addr:#X}..{load_end_addr:#X}" ); return Err(Error::InvalidKernelOffset); } Some(MultibootLoad { file_load_offset, file_load_size: file_load_size as usize, load_addr: GuestAddress(load_addr.into()), entry_addr: GuestAddress(entry_addr.into()), bss_addr, bss_size: bss_size as usize, }) } else { None }; let preferred_video_mode = if need_video_mode_table { let mode_type = get_le32(hdr, 32)?; let width = get_le32(hdr, 36)?; let height = get_le32(hdr, 40)?; let depth = get_le32(hdr, 44)?; let mode_type = match mode_type { 0 => MultibootVideoModeType::LinearGraphics, 1 => MultibootVideoModeType::EgaText, _ => MultibootVideoModeType::Other(mode_type), }; Some(MultibootVideoMode { mode_type, width: NonZeroU32::new(width), height: NonZeroU32::new(height), depth: NonZeroU32::new(depth), }) } else { None }; let multiboot = MultibootKernel { offset: offset as u32, boot_modules_page_aligned, need_available_memory, load, preferred_video_mode, }; trace!("validated header: {multiboot:?}"); Ok(Some(multiboot)) } fn get_le32(bytes: &[u8], offset: usize) -> Result { let le32_bytes = bytes.get(offset..offset + 4).ok_or(Error::ReadHeader)?; // This can't fail because the slice is always 4 bytes long. let le32_array: [u8; 4] = le32_bytes.try_into().unwrap(); Ok(u32::from_le_bytes(le32_array)) } /// Load a Multiboot kernel image into memory. /// /// The `MultibootLoad` information can be retrieved from the optional `load` field of a /// `MultibootKernel` returned by [`multiboot_header_from_file()`]. pub fn load_multiboot( guest_mem: &GuestMemory, kernel_image: &mut F, multiboot_load: &MultibootLoad, ) -> Result where F: FileReadWriteAtVolatile, { let guest_slice = guest_mem .get_slice_at_addr(multiboot_load.load_addr, multiboot_load.file_load_size) .map_err(|_| Error::ReadKernelImage)?; kernel_image .read_exact_at_volatile(guest_slice, multiboot_load.file_load_offset) .map_err(|_| Error::ReadKernelImage)?; if let Some(bss_addr) = multiboot_load.bss_addr { let bss_slice = guest_mem .get_slice_at_addr(bss_addr, multiboot_load.bss_size) .map_err(|_| Error::ReadKernelImage)?; bss_slice.write_bytes(0); } let size: u64 = multiboot_load .file_load_size .checked_add(multiboot_load.bss_size) .ok_or(Error::InvalidProgramHeaderSize)? .try_into() .map_err(|_| Error::InvalidProgramHeaderSize)?; let address_range = AddressRange::from_start_and_size(multiboot_load.load_addr.offset(), size) .ok_or(Error::InvalidProgramHeaderSize)?; Ok(LoadedKernel { address_range, size, entry: multiboot_load.entry_addr, }) }