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