Skip to main content

loader/
bzimage.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Support for loading Linux x86 bzImage files directly.
5//!
6//! A bzImage is the standard compressed kernel image format on x86/x86_64.
7//! It consists of a real-mode boot sector, setup code, and a compressed
8//! payload that the kernel's own startup stub decompresses at boot time.
9//! This format is specific to x86; AArch64 uses a different `Image` format.
10//!
11//! See the Linux kernel documentation for the boot protocol:
12//! <https://www.kernel.org/doc/html/latest/arch/x86/boot.html>
13
14use loader_defs::linux as defs;
15use std::io::Read;
16use std::io::Seek;
17use std::io::SeekFrom;
18use std::mem::size_of;
19use thiserror::Error;
20use zerocopy::FromBytes;
21
22/// Magic value "HdrS" at offset 0x202 in a bzImage, identifying a valid
23/// Linux setup header.
24const HDRS_MAGIC: u32 = 0x53726448;
25
26/// Boot flag value at offset 0x1FE.
27const BOOT_FLAG: u16 = 0xAA55;
28
29/// Minimum boot protocol version that supports 64-bit boot (version 2.12+).
30const MIN_PROTOCOL_VERSION: u16 = 0x020C;
31
32/// Offset of the `setup_header` struct within the boot sector.
33const SETUP_HEADER_OFFSET: usize = 0x1F1;
34
35/// Minimum number of bytes we need to read to cover the full setup header.
36/// Derived from the struct layout so it stays correct if `setup_header` changes.
37const MIN_HEADER_SIZE: usize = SETUP_HEADER_OFFSET + size_of::<defs::setup_header>();
38
39/// The `loadflags` bit indicating the protected-mode code should be loaded high (at 0x100000).
40const LOADED_HIGH: u8 = 0x01;
41
42/// The `xloadflags` bit indicating the kernel has a 64-bit entry point.
43const XLF_KERNEL_64: u16 = 0x01;
44
45/// Errors that can occur during bzImage detection and parsing.
46#[derive(Debug, Error)]
47pub enum Error {
48    /// An I/O error occurred while reading the bzImage.
49    #[error("I/O error reading bzImage")]
50    Io(#[source] std::io::Error),
51    /// The image is not a valid bzImage (missing boot flag or HdrS magic).
52    #[error("not a valid bzImage (missing boot flag or HdrS magic)")]
53    NotBzImage,
54    /// The bzImage boot protocol version is too old.
55    #[error(
56        "bzImage boot protocol version {version:#06x} is too old (need >= 2.12 for 64-bit boot)"
57    )]
58    ProtocolTooOld {
59        /// The detected protocol version.
60        version: u16,
61    },
62    /// The kernel does not support being loaded high (at 0x100000).
63    #[error("bzImage does not have LOADED_HIGH flag set")]
64    NotLoadedHigh,
65    /// The kernel does not have a 64-bit entry point.
66    #[error("bzImage does not support 64-bit boot (XLF_KERNEL_64 not set in xloadflags)")]
67    No64BitEntry,
68    /// The bzImage is truncated — the protected-mode code is too small to
69    /// contain the 64-bit entry point.
70    #[error(
71        "bzImage is truncated: protected-mode size ({size}) is too small for entry offset ({entry_offset})"
72    )]
73    Truncated {
74        /// The size of the protected-mode code.
75        size: u64,
76        /// The required entry point offset.
77        entry_offset: u64,
78    },
79}
80
81/// Information parsed from a bzImage setup header, needed for loading.
82#[derive(Debug, Clone)]
83pub struct BzImageInfo {
84    /// The setup header to copy into the zero page's `hdr` field.
85    pub setup_header: defs::setup_header,
86    /// Number of setup sectors (determines where protected-mode code starts).
87    /// The protected-mode code begins at offset `(setup_sects + 1) * 512` in the file.
88    pub setup_sects: u8,
89    /// The total size in bytes of the protected-mode code (everything after the setup).
90    pub protected_mode_size: u64,
91    /// The 64-bit entry point offset relative to the start of the protected-mode code.
92    /// For protocol >= 2.12 with XLF_KERNEL_64, this is at offset 0x200 from
93    /// the start of the protected-mode code.
94    pub entry_offset: u64,
95    /// The `init_size` field — the amount of linear contiguous memory the
96    /// kernel needs starting at the load address for initialization.
97    pub init_size: u32,
98}
99
100/// Attempt to detect whether `kernel_image` is a bzImage.
101///
102/// Returns `true` if the image has a valid Linux setup header with the
103/// "HdrS" magic. The file position is always restored to the beginning
104/// before returning.
105pub fn is_bzimage(kernel_image: &mut (impl Read + Seek)) -> Result<bool, Error> {
106    kernel_image.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
107
108    let mut buf = [0u8; MIN_HEADER_SIZE];
109    let result = kernel_image.read_exact(&mut buf);
110
111    // Always restore position before checking the read result.
112    kernel_image.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
113
114    match result {
115        Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(false),
116        Err(e) => return Err(Error::Io(e)),
117        Ok(()) => {}
118    }
119
120    let boot_flag = u16::from_le_bytes([buf[0x1fe], buf[0x1ff]]);
121    let header_magic = u32::from_le_bytes([buf[0x202], buf[0x203], buf[0x204], buf[0x205]]);
122
123    Ok(boot_flag == BOOT_FLAG && header_magic == HDRS_MAGIC)
124}
125
126/// Parse the bzImage setup header and return information needed for loading.
127///
128/// The file position of `kernel_image` is restored to the beginning on
129/// success. On error, restoration is best-effort (the seek-back is
130/// attempted but its failure is ignored in favor of the parse error).
131pub fn parse_bzimage(kernel_image: &mut (impl Read + Seek)) -> Result<BzImageInfo, Error> {
132    kernel_image.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
133    let result = parse_bzimage_inner(kernel_image);
134    let _ = kernel_image.seek(SeekFrom::Start(0));
135    result
136}
137
138fn parse_bzimage_inner(kernel_image: &mut (impl Read + Seek)) -> Result<BzImageInfo, Error> {
139    let mut buf = [0u8; MIN_HEADER_SIZE];
140    kernel_image.read_exact(&mut buf).map_err(Error::Io)?;
141
142    // Validate the bzImage identifying markers before parsing fields.
143    let boot_flag = u16::from_le_bytes([buf[0x1fe], buf[0x1ff]]);
144    let header_magic = u32::from_le_bytes([buf[0x202], buf[0x203], buf[0x204], buf[0x205]]);
145    if boot_flag != BOOT_FLAG || header_magic != HDRS_MAGIC {
146        return Err(Error::NotBzImage);
147    }
148
149    // The setup_header in boot_params starts at offset 0x1F1 relative to
150    // the start of the boot sector.
151    let hdr = defs::setup_header::read_from_bytes(
152        &buf[SETUP_HEADER_OFFSET..SETUP_HEADER_OFFSET + size_of::<defs::setup_header>()],
153    )
154    .expect("buf is large enough: MIN_HEADER_SIZE is derived from setup_header size");
155
156    let version: u16 = hdr.version.into();
157    if version < MIN_PROTOCOL_VERSION {
158        return Err(Error::ProtocolTooOld { version });
159    }
160
161    let loadflags: u8 = hdr.loadflags;
162    if loadflags & LOADED_HIGH == 0 {
163        return Err(Error::NotLoadedHigh);
164    }
165
166    let xloadflags: u16 = hdr.xloadflags.into();
167    if xloadflags & XLF_KERNEL_64 == 0 {
168        return Err(Error::No64BitEntry);
169    }
170
171    let setup_sects = if hdr.setup_sects == 0 {
172        4
173    } else {
174        hdr.setup_sects
175    };
176    let protected_mode_offset = (setup_sects as u64 + 1) * 512;
177
178    // Use the syssize field from the header (size in 16-byte paragraphs)
179    // for a precise payload size. Fall back to file size if syssize is zero.
180    let syssize: u32 = hdr.syssize.into();
181    let protected_mode_size = if syssize != 0 {
182        syssize as u64 * 16
183    } else {
184        let file_size = kernel_image.seek(SeekFrom::End(0)).map_err(Error::Io)?;
185        file_size.saturating_sub(protected_mode_offset)
186    };
187
188    // For 64-bit boot protocol, the 64-bit entry point is at offset 0x200
189    // from the beginning of the protected-mode code.
190    let entry_offset: u64 = 0x200;
191    if protected_mode_size <= entry_offset {
192        return Err(Error::Truncated {
193            size: protected_mode_size,
194            entry_offset,
195        });
196    }
197
198    let init_size: u32 = hdr.init_size.into();
199
200    tracing::debug!(
201        version = format_args!("{:#06x}", version),
202        setup_sects,
203        protected_mode_offset,
204        protected_mode_size,
205        init_size,
206        "parsed bzImage setup header"
207    );
208
209    Ok(BzImageInfo {
210        setup_header: hdr,
211        setup_sects,
212        protected_mode_size,
213        entry_offset,
214        init_size,
215    })
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use std::io::Cursor;
222
223    /// Build a minimal synthetic bzImage for testing.
224    fn make_test_bzimage() -> Vec<u8> {
225        let setup_sects: u8 = 1;
226        let protected_mode_offset = (setup_sects as u32 + 1) * 512;
227        // Some fake protected-mode code (1024 bytes).
228        let pm_code = vec![0xCC; 1024];
229
230        let total_size = protected_mode_offset as usize + pm_code.len();
231        let mut image = vec![0u8; total_size];
232
233        // setup_sects at 0x1f1
234        image[0x1f1] = setup_sects;
235        // boot_flag at 0x1fe = 0xAA55
236        image[0x1fe..0x200].copy_from_slice(&BOOT_FLAG.to_le_bytes());
237        // header magic "HdrS" at 0x202
238        image[0x202..0x206].copy_from_slice(&HDRS_MAGIC.to_le_bytes());
239        // version at 0x206 = 0x020f (protocol 2.15)
240        image[0x206..0x208].copy_from_slice(&0x020fu16.to_le_bytes());
241        // loadflags at 0x211: LOADED_HIGH
242        image[0x211] = LOADED_HIGH;
243        // xloadflags at 0x236: XLF_KERNEL_64
244        image[0x236..0x238].copy_from_slice(&XLF_KERNEL_64.to_le_bytes());
245        // pref_address at 0x258 = 0x1000000 (16MB)
246        image[0x258..0x260].copy_from_slice(&0x1000000u64.to_le_bytes());
247        // syssize at 0x1f4 = pm_code size in 16-byte paragraphs
248        let syssize = (pm_code.len() as u32) / 16;
249        image[0x1f4..0x1f8].copy_from_slice(&syssize.to_le_bytes());
250        // init_size at 0x260 = 0x1000000 (16MB)
251        image[0x260..0x264].copy_from_slice(&0x1000000u32.to_le_bytes());
252
253        // Write the protected-mode code.
254        image[protected_mode_offset as usize..].copy_from_slice(&pm_code);
255
256        image
257    }
258
259    #[test]
260    fn test_is_bzimage_positive() {
261        let bzimage = make_test_bzimage();
262        let mut cursor = Cursor::new(bzimage);
263        assert!(is_bzimage(&mut cursor).unwrap());
264    }
265
266    #[test]
267    fn test_is_bzimage_negative_elf() {
268        let mut elf = vec![0u8; 0x1000];
269        elf[0..4].copy_from_slice(&[0x7f, b'E', b'L', b'F']);
270        let mut cursor = Cursor::new(elf);
271        assert!(!is_bzimage(&mut cursor).unwrap());
272    }
273
274    #[test]
275    fn test_parse_bzimage() {
276        let bzimage = make_test_bzimage();
277        let mut cursor = Cursor::new(bzimage);
278
279        let info = parse_bzimage(&mut cursor).unwrap();
280        assert_eq!(info.setup_sects, 1);
281        assert_eq!(info.protected_mode_size, 1024);
282        assert_eq!(info.entry_offset, 0x200);
283        assert_eq!(info.init_size, 0x1000000);
284    }
285
286    #[test]
287    fn test_not_bzimage_returns_false() {
288        let mut elf = vec![0u8; 0x1000];
289        elf[0..4].copy_from_slice(&[0x7f, b'E', b'L', b'F']);
290        let mut cursor = Cursor::new(elf);
291        assert!(!is_bzimage(&mut cursor).unwrap());
292    }
293}