kernel_loader/
multiboot.rs

1// Copyright 2024 The ChromiumOS Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Multiboot kernel loader
6//!
7//! Only Multiboot (version 0.6.96) is supported, not Multiboot2.
8
9use std::fs::File;
10use std::mem::size_of;
11use std::num::NonZeroU32;
12
13use base::error;
14use base::trace;
15use base::FileReadWriteAtVolatile;
16use base::VolatileSlice;
17use resources::AddressRange;
18use vm_memory::GuestAddress;
19use vm_memory::GuestMemory;
20
21use crate::ElfClass;
22use crate::Error;
23use crate::LoadedKernel;
24use crate::Result;
25
26/// Multiboot header retrieved from a kernel image.
27#[derive(Clone, Debug)]
28pub struct MultibootKernel {
29    /// Byte offset of the beginning of the multiboot header in the kernel image.
30    pub offset: u32,
31
32    /// Kernel requires that boot modules are aligned to 4 KB.
33    pub boot_modules_page_aligned: bool,
34
35    /// Kernel requires available memory information (`mem_*` fields).
36    pub need_available_memory: bool,
37
38    /// Kernel load address.
39    ///
40    /// If present, this overrides any other executable format headers (e.g. ELF).
41    pub load: Option<MultibootLoad>,
42
43    /// Kernel preferred video mode.
44    ///
45    /// If present, the kernel also requires information about the video mode table.
46    pub preferred_video_mode: Option<MultibootVideoMode>,
47}
48
49/// Multiboot kernel load parameters.
50#[derive(Clone, Debug)]
51pub struct MultibootLoad {
52    /// File byte offset to load the kernel's code and initialized data from.
53    pub file_load_offset: u64,
54
55    /// Number of bytes to read from the file at `file_load_offset`.
56    pub file_load_size: usize,
57
58    /// Physical memory address where the kernel should be loaded.
59    pub load_addr: GuestAddress,
60
61    /// Physical address of the kernel entry point.
62    pub entry_addr: GuestAddress,
63
64    /// BSS physical memory starting address to zero fill, if present in kernel.
65    pub bss_addr: Option<GuestAddress>,
66
67    /// BSS size in bytes (0 if no BSS region is present).
68    pub bss_size: usize,
69}
70
71/// Multiboot kernel video mode specification.
72#[derive(Clone, Debug)]
73pub struct MultibootVideoMode {
74    /// Preferred video mode type (text or graphics).
75    pub mode_type: MultibootVideoModeType,
76
77    /// Width of the requested mode.
78    ///
79    /// For text modes, this is in units of characters. For graphics modes, this is in units of
80    /// pixels.
81    pub width: Option<NonZeroU32>,
82
83    /// Height of the requested mode.
84    ///
85    /// For text modes, this is in units of characters. For graphics modes, this is in units of
86    /// pixels.
87    pub height: Option<NonZeroU32>,
88
89    /// Requested bits per pixel (only relevant in graphics modes).
90    pub depth: Option<NonZeroU32>,
91}
92
93#[derive(Copy, Clone, Debug)]
94pub enum MultibootVideoModeType {
95    LinearGraphics,
96    EgaText,
97    Other(u32),
98}
99
100/// Scan the provided kernel file to find a Multiboot header, if present.
101///
102/// # Returns
103///
104/// - `Ok(None)`: kernel file did not contain a Multiboot header.
105/// - `Ok(Some(...))`: kernel file contained a valid Multiboot header, which is returned.
106/// - `Err(...)`: kernel file contained a Multiboot header with a valid checksum but other fields in
107///   the header were invalid.
108pub fn multiboot_header_from_file(kernel_file: &mut File) -> Result<Option<MultibootKernel>> {
109    const MIN_HEADER_SIZE: usize = 3 * size_of::<u32>();
110    const ALIGNMENT: usize = 4;
111
112    // Read up to 8192 bytes from the beginning of the file.
113    let kernel_file_len = kernel_file.metadata().map_err(|_| Error::ReadHeader)?.len();
114    let kernel_prefix_len = kernel_file_len.min(8192) as usize;
115
116    if kernel_prefix_len < MIN_HEADER_SIZE {
117        return Ok(None);
118    }
119
120    let mut kernel_bytes = vec![0u8; kernel_prefix_len];
121    kernel_file
122        .read_exact_at_volatile(VolatileSlice::new(&mut kernel_bytes), 0)
123        .map_err(|_| Error::ReadHeader)?;
124
125    for offset in (0..kernel_prefix_len).step_by(ALIGNMENT) {
126        let Some(hdr) = kernel_bytes.get(offset..) else {
127            break;
128        };
129        match multiboot_header(hdr, offset as u64, kernel_file_len) {
130            Ok(None) => continue,
131            Ok(Some(multiboot)) => return Ok(Some(multiboot)),
132            Err(e) => return Err(e),
133        }
134    }
135
136    // The file did not contain a valid Multiboot header.
137    Ok(None)
138}
139
140/// Attempt to parse a Multiboot header from the prefix of a slice.
141///
142/// # Returns
143///
144/// - `Ok(None)`: no multiboot header here.
145/// - `Ok(Some(...))`: valid multiboot header is returned.
146/// - `Err(...)`: valid multiboot header checksum at this position in the file (meaning this is the
147///   real header location), but there is an invalid field later in the multiboot header (e.g. an
148///   impossible combination of load addresses).
149fn multiboot_header(
150    hdr: &[u8],
151    offset: u64,
152    kernel_file_len: u64,
153) -> Result<Option<MultibootKernel>> {
154    const MAGIC: u32 = 0x1BADB002;
155
156    let Ok(magic) = get_le32(hdr, 0) else {
157        return Ok(None);
158    };
159    if magic != MAGIC {
160        return Ok(None);
161    }
162
163    // Failing to read these fields means we ran out of data at the end of the slice and did not
164    // actually find a Multiboot header, so return `Ok(None)` to indicate no Multiboot header was
165    // found instead of using `?`, which would return an error.
166    let Ok(flags) = get_le32(hdr, 4) else {
167        return Ok(None);
168    };
169    let Ok(checksum) = get_le32(hdr, 8) else {
170        return Ok(None);
171    };
172
173    if magic.wrapping_add(flags).wrapping_add(checksum) != 0 {
174        // Checksum did not match, so this is not a real Multiboot header. Keep searching.
175        return Ok(None);
176    }
177
178    trace!("found Multiboot header with valid checksum at {offset:#X}");
179
180    const F_BOOT_MODULE_PAGE_ALIGN: u32 = 1 << 0;
181    const F_AVAILABLE_MEMORY: u32 = 1 << 1;
182    const F_VIDEO_MODE: u32 = 1 << 2;
183    const F_ADDRESS: u32 = 1 << 16;
184
185    const KNOWN_FLAGS: u32 =
186        F_BOOT_MODULE_PAGE_ALIGN | F_AVAILABLE_MEMORY | F_VIDEO_MODE | F_ADDRESS;
187
188    let unknown_flags = flags & !KNOWN_FLAGS;
189    if unknown_flags != 0 {
190        error!("unknown flags {unknown_flags:#X}");
191        return Err(Error::InvalidFlags);
192    }
193
194    let boot_modules_page_aligned = flags & F_BOOT_MODULE_PAGE_ALIGN != 0;
195    let need_available_memory = flags & F_AVAILABLE_MEMORY != 0;
196    let need_video_mode_table = flags & F_VIDEO_MODE != 0;
197    let load_address_available = flags & F_ADDRESS != 0;
198
199    let load = if load_address_available {
200        let header_addr = get_le32(hdr, 12)?;
201        let load_addr = get_le32(hdr, 16)?;
202        let load_end_addr = get_le32(hdr, 20)?;
203        let bss_end_addr = get_le32(hdr, 24)?;
204        let entry_addr = get_le32(hdr, 28)?;
205
206        if header_addr < load_addr {
207            error!("header_addr {header_addr:#X} < load_addr {load_addr:#X}");
208            return Err(Error::InvalidKernelOffset);
209        }
210
211        // The beginning of the area to load from the file starts `load_offset` bytes before the
212        // multiboot header.
213        let load_offset = u64::from(header_addr - load_addr);
214        if load_offset > offset {
215            error!("load_offset {load_offset:#X} > offset {offset:#X}");
216            return Err(Error::InvalidKernelOffset);
217        }
218        let file_load_offset = offset - load_offset;
219
220        let file_load_size = if load_end_addr == 0 {
221            // Zero `load_end_addr` means the loadable data extends to the end of the file.
222            (kernel_file_len - file_load_offset)
223                .try_into()
224                .map_err(|_| Error::InvalidKernelOffset)?
225        } else if load_end_addr < load_addr {
226            error!("load_end_addr {load_end_addr:#X} < load_addr {load_addr:#X}");
227            return Err(Error::InvalidKernelOffset);
228        } else {
229            load_end_addr - load_addr
230        };
231
232        let load_end_addr = load_addr
233            .checked_add(file_load_size)
234            .ok_or(Error::InvalidKernelOffset)?;
235
236        // The bss region immediately follows the load-from-file region in memory.
237        let bss_addr = load_addr + file_load_size;
238
239        let bss_size = if bss_end_addr == 0 {
240            // Zero `bss_end_addr` means no bss segment is present.
241            0
242        } else if bss_end_addr < bss_addr {
243            error!("bss_end_addr {bss_end_addr:#X} < bss_addr {bss_addr:#X}");
244            return Err(Error::InvalidKernelOffset);
245        } else {
246            bss_end_addr - bss_addr
247        };
248
249        let bss_addr = if bss_size > 0 {
250            Some(GuestAddress(bss_addr.into()))
251        } else {
252            None
253        };
254
255        if entry_addr < load_addr || entry_addr >= load_end_addr {
256            error!(
257                "entry_addr {entry_addr:#X} not in load range {load_addr:#X}..{load_end_addr:#X}"
258            );
259            return Err(Error::InvalidKernelOffset);
260        }
261
262        Some(MultibootLoad {
263            file_load_offset,
264            file_load_size: file_load_size as usize,
265            load_addr: GuestAddress(load_addr.into()),
266            entry_addr: GuestAddress(entry_addr.into()),
267            bss_addr,
268            bss_size: bss_size as usize,
269        })
270    } else {
271        None
272    };
273
274    let preferred_video_mode = if need_video_mode_table {
275        let mode_type = get_le32(hdr, 32)?;
276        let width = get_le32(hdr, 36)?;
277        let height = get_le32(hdr, 40)?;
278        let depth = get_le32(hdr, 44)?;
279
280        let mode_type = match mode_type {
281            0 => MultibootVideoModeType::LinearGraphics,
282            1 => MultibootVideoModeType::EgaText,
283            _ => MultibootVideoModeType::Other(mode_type),
284        };
285
286        Some(MultibootVideoMode {
287            mode_type,
288            width: NonZeroU32::new(width),
289            height: NonZeroU32::new(height),
290            depth: NonZeroU32::new(depth),
291        })
292    } else {
293        None
294    };
295
296    let multiboot = MultibootKernel {
297        offset: offset as u32,
298        boot_modules_page_aligned,
299        need_available_memory,
300        load,
301        preferred_video_mode,
302    };
303
304    trace!("validated header: {multiboot:?}");
305
306    Ok(Some(multiboot))
307}
308
309fn get_le32(bytes: &[u8], offset: usize) -> Result<u32> {
310    let le32_bytes = bytes.get(offset..offset + 4).ok_or(Error::ReadHeader)?;
311    // This can't fail because the slice is always 4 bytes long.
312    let le32_array: [u8; 4] = le32_bytes.try_into().unwrap();
313    Ok(u32::from_le_bytes(le32_array))
314}
315
316/// Load a Multiboot kernel image into memory.
317///
318/// The `MultibootLoad` information can be retrieved from the optional `load` field of a
319/// `MultibootKernel` returned by [`multiboot_header_from_file()`].
320pub fn load_multiboot<F>(
321    guest_mem: &GuestMemory,
322    kernel_image: &mut F,
323    multiboot_load: &MultibootLoad,
324) -> Result<LoadedKernel>
325where
326    F: FileReadWriteAtVolatile,
327{
328    let guest_slice = guest_mem
329        .get_slice_at_addr(multiboot_load.load_addr, multiboot_load.file_load_size)
330        .map_err(|_| Error::ReadKernelImage)?;
331    kernel_image
332        .read_exact_at_volatile(guest_slice, multiboot_load.file_load_offset)
333        .map_err(|_| Error::ReadKernelImage)?;
334
335    if let Some(bss_addr) = multiboot_load.bss_addr {
336        let bss_slice = guest_mem
337            .get_slice_at_addr(bss_addr, multiboot_load.bss_size)
338            .map_err(|_| Error::ReadKernelImage)?;
339        bss_slice.write_bytes(0);
340    }
341
342    let size: u64 = multiboot_load
343        .file_load_size
344        .checked_add(multiboot_load.bss_size)
345        .ok_or(Error::InvalidProgramHeaderSize)?
346        .try_into()
347        .map_err(|_| Error::InvalidProgramHeaderSize)?;
348
349    let address_range = AddressRange::from_start_and_size(multiboot_load.load_addr.offset(), size)
350        .ok_or(Error::InvalidProgramHeaderSize)?;
351
352    Ok(LoadedKernel {
353        address_range,
354        size,
355        entry: multiboot_load.entry_addr,
356        class: ElfClass::ElfClass32,
357    })
358}