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