Skip to main content

loader/uefi/
firmware_version.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! MSVM firmware build and interface version parsing.
5
6use core::mem::size_of;
7use thiserror::Error;
8use uefi_specs::hyperv::firmware_version;
9use uefi_specs::uefi::firmware_volume;
10use zerocopy::FromBytes;
11
12const FILE_ALIGNMENT: usize = 8;
13const SECTION_ALIGNMENT: usize = 4;
14const ERASED_BYTE: u8 = 0xff;
15#[cfg(guest_arch = "x86_64")]
16const FIRST_FIRMWARE_VOLUME_OFFSET: usize = 0;
17#[cfg(guest_arch = "aarch64")]
18const FIRST_FIRMWARE_VOLUME_OFFSET: usize = 0x1000;
19
20/// A parsed view of an MSVM firmware version record.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct MsvmFirmwareVersion<'a> {
23    pub struct_version: u16,
24    pub header_size: u16,
25    pub flags: u32,
26    pub interface_version_major: u16,
27    pub interface_version_minor: u16,
28    pub base_version: &'a str,
29    pub git_commit: &'a str,
30}
31
32/// A malformed MSVM firmware version file or record.
33#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
34pub enum Error {
35    #[error("invalid firmware volume header")]
36    VolumeHeader,
37    #[error("invalid firmware file")]
38    File,
39    #[error("invalid firmware section")]
40    Section,
41    #[error("firmware version file has no RAW section")]
42    MissingRawSection,
43    #[error("firmware version record is truncated")]
44    TruncatedRecord,
45    #[error("invalid firmware version record signature")]
46    InvalidSignature,
47    #[error("unsupported firmware version structure version {0}")]
48    UnsupportedStructVersion(u16),
49    #[error("invalid firmware version record header size {0}")]
50    InvalidHeaderSize(u16),
51    #[error("firmware version field {0} is not NUL-terminated ASCII")]
52    InvalidString(&'static str),
53}
54
55fn size24(value: [u8; 3]) -> usize {
56    usize::from(value[0]) | usize::from(value[1]) << 8 | usize::from(value[2]) << 16
57}
58
59fn align_up(value: usize, alignment: usize) -> Option<usize> {
60    value
61        .checked_add(alignment - 1)
62        .map(|value| value & !(alignment - 1))
63}
64
65fn find_file(fv: &[u8], name: guid::Guid) -> Result<Option<&[u8]>, Error> {
66    let header = firmware_volume::FirmwareVolumeHeader::read_from_prefix(fv)
67        .map_err(|_| Error::VolumeHeader)?
68        .0;
69    if header.signature != firmware_volume::FV_SIGNATURE {
70        return Err(Error::VolumeHeader);
71    }
72    let length = usize::try_from(header.fv_length)
73        .ok()
74        .filter(|&length| length <= fv.len())
75        .ok_or(Error::VolumeHeader)?;
76    let mut offset = align_up(usize::from(header.header_length), FILE_ALIGNMENT)
77        .filter(|&offset| offset <= length)
78        .ok_or(Error::VolumeHeader)?;
79
80    while offset
81        .checked_add(size_of::<firmware_volume::FfsFileHeader>())
82        .is_some_and(|end| end <= length)
83    {
84        let header_bytes = &fv[offset..offset + size_of::<firmware_volume::FfsFileHeader>()];
85        if header_bytes.iter().all(|&byte| byte == ERASED_BYTE) {
86            return Ok(None);
87        }
88        let header = firmware_volume::FfsFileHeader::read_from_prefix(header_bytes)
89            .map_err(|_| Error::File)?
90            .0;
91        let file_size = size24(header.size);
92        if file_size < size_of::<firmware_volume::FfsFileHeader>() {
93            return Err(Error::File);
94        }
95        let end = offset
96            .checked_add(file_size)
97            .filter(|&end| end <= length)
98            .ok_or(Error::File)?;
99        if header.name == name {
100            return Ok(Some(&fv[offset..end]));
101        }
102        offset = align_up(end, FILE_ALIGNMENT).ok_or(Error::File)?;
103    }
104    Ok(None)
105}
106
107fn find_unique_section(file: &[u8], section_type: u8) -> Result<Option<&[u8]>, Error> {
108    let mut offset = size_of::<firmware_volume::FfsFileHeader>();
109    let mut found = None;
110    while offset < file.len() {
111        offset = align_up(offset, SECTION_ALIGNMENT).ok_or(Error::Section)?;
112        let header = firmware_volume::CommonSectionHeader::read_from_prefix(
113            file.get(offset..).ok_or(Error::Section)?,
114        )
115        .map_err(|_| Error::Section)?
116        .0;
117        let section_size = size24(header.size);
118        if section_size < size_of::<firmware_volume::CommonSectionHeader>() {
119            return Err(Error::Section);
120        }
121        let end = offset
122            .checked_add(section_size)
123            .filter(|&end| end <= file.len())
124            .ok_or(Error::Section)?;
125        if header.section_type == section_type {
126            if found.is_some() {
127                return Err(Error::Section);
128            }
129            found = Some(&file[offset + size_of::<firmware_volume::CommonSectionHeader>()..end]);
130        }
131        offset = end;
132    }
133    Ok(found)
134}
135
136fn parse_ascii<'a>(value: &'a [u8], field: &'static str) -> Result<&'a str, Error> {
137    let nul = value
138        .iter()
139        .position(|&byte| byte == 0)
140        .ok_or(Error::InvalidString(field))?;
141    let value = &value[..nul];
142    if !value.is_ascii() {
143        return Err(Error::InvalidString(field));
144    }
145    Ok(core::str::from_utf8(value).expect("ASCII is valid UTF-8"))
146}
147
148fn parse_record(record: &[u8]) -> Result<MsvmFirmwareVersion<'_>, Error> {
149    let info = firmware_version::MsvmFirmwareVersionInfo::ref_from_prefix(record)
150        .map_err(|_| Error::TruncatedRecord)?
151        .0;
152    if &info.signature != firmware_version::SIGNATURE {
153        return Err(Error::InvalidSignature);
154    }
155    let struct_version = u16::from_le_bytes(info.struct_version);
156    if struct_version < firmware_version::STRUCT_VERSION {
157        return Err(Error::UnsupportedStructVersion(struct_version));
158    }
159    let header_size = u16::from_le_bytes(info.header_size);
160    if usize::from(header_size) < firmware_version::HEADER_SIZE
161        || usize::from(header_size) > record.len()
162    {
163        return Err(Error::InvalidHeaderSize(header_size));
164    }
165    Ok(MsvmFirmwareVersion {
166        struct_version,
167        header_size,
168        flags: u32::from_le_bytes(info.flags),
169        interface_version_major: u16::from_le_bytes(info.interface_version_major),
170        interface_version_minor: u16::from_le_bytes(info.interface_version_minor),
171        base_version: parse_ascii(&info.base_version, "base version")?,
172        git_commit: parse_ascii(&info.git_commit, "git commit")?,
173    })
174}
175
176fn find_in_firmware_volume(fv: &[u8]) -> Result<Option<MsvmFirmwareVersion<'_>>, Error> {
177    let Some(file) = find_file(fv, firmware_version::FILE_GUID)? else {
178        return Ok(None);
179    };
180    let record =
181        find_unique_section(file, firmware_volume::SECTION_RAW)?.ok_or(Error::MissingRawSection)?;
182    parse_record(record).map(Some)
183}
184
185/// Finds and parses the version record in a firmware image.
186///
187/// `Ok(None)` means the dedicated version FFS file is not present.
188pub fn find_in_firmware_image(image: &[u8]) -> Result<Option<MsvmFirmwareVersion<'_>>, Error> {
189    let fv = image
190        .get(FIRST_FIRMWARE_VOLUME_OFFSET..)
191        .ok_or(Error::VolumeHeader)?;
192    #[cfg(guest_arch = "x86_64")]
193    return find_in_firmware_volume(fv);
194    #[cfg(guest_arch = "aarch64")]
195    {
196        // Locate the record before interpreting its interface version. Older
197        // images have separate PEI/DXE volumes; newer ones combine them.
198        let mut volumes = fv;
199        loop {
200            let header = firmware_volume::FirmwareVolumeHeader::read_from_prefix(volumes)
201                .map_err(|_| Error::VolumeHeader)?
202                .0;
203            let length = usize::try_from(header.fv_length)
204                .ok()
205                .filter(|&length| {
206                    length >= size_of::<firmware_volume::FirmwareVolumeHeader>()
207                        && length <= volumes.len()
208                })
209                .ok_or(Error::VolumeHeader)?;
210            let (fv, remaining) = volumes.split_at(length);
211            if let Some(version) = find_in_firmware_volume(fv)? {
212                return Ok(Some(version));
213            }
214            if remaining.is_empty() {
215                return Ok(None);
216            }
217            volumes = remaining;
218        }
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use test_with_tracing::test;
226    use zerocopy::IntoBytes;
227
228    fn size24(value: usize) -> [u8; 3] {
229        let value = (value as u32).to_le_bytes();
230        [value[0], value[1], value[2]]
231    }
232
233    fn record(major: u16, minor: u16) -> [u8; firmware_version::HEADER_SIZE] {
234        let mut record = [0; firmware_version::HEADER_SIZE];
235        record[0..4].copy_from_slice(firmware_version::SIGNATURE);
236        record[4..6].copy_from_slice(&firmware_version::STRUCT_VERSION.to_le_bytes());
237        record[6..8].copy_from_slice(&(firmware_version::HEADER_SIZE as u16).to_le_bytes());
238        record[8..12].copy_from_slice(
239            &(firmware_version::FLAG_DIRTY | firmware_version::FLAG_OFFICIAL).to_le_bytes(),
240        );
241        record[12..14].copy_from_slice(&major.to_le_bytes());
242        record[14..16].copy_from_slice(&minor.to_le_bytes());
243        record[16..21].copy_from_slice(b"26.0\0");
244        record[32..40].copy_from_slice(b"deadbee\0");
245        record
246    }
247
248    #[test]
249    fn parses_record() {
250        let record = record(1, 2);
251        let version = parse_record(&record).unwrap();
252        assert_eq!(version.interface_version_major, 1);
253        assert_eq!(version.interface_version_minor, 2);
254        assert_eq!(version.base_version, "26.0");
255        assert_eq!(version.git_commit, "deadbee");
256        assert_eq!(
257            version.flags,
258            firmware_version::FLAG_DIRTY | firmware_version::FLAG_OFFICIAL
259        );
260    }
261
262    #[test]
263    fn rejects_truncated_record() {
264        assert_eq!(parse_record(b"MVFW").unwrap_err(), Error::TruncatedRecord);
265    }
266
267    #[test]
268    fn rejects_non_ascii_string() {
269        let mut record = record(1, 0);
270        record[16..19].copy_from_slice(b"\xc3\xa9\0");
271        assert_eq!(
272            parse_record(&record).unwrap_err(),
273            Error::InvalidString("base version")
274        );
275    }
276
277    fn volume(length: usize, interface: Option<(u16, u16)>) -> Vec<u8> {
278        let mut fv = vec![0xff; length];
279        fv[32..40].copy_from_slice(&(length as u64).to_le_bytes());
280        fv[40..44].copy_from_slice(b"_FVH");
281        fv[48..50].copy_from_slice(
282            &(size_of::<firmware_volume::FirmwareVolumeHeader>() as u16).to_le_bytes(),
283        );
284        let Some((major, minor)) = interface else {
285            return fv;
286        };
287        let record = record(major, minor);
288        let section_size = size_of::<firmware_volume::CommonSectionHeader>() + record.len();
289        let file_size = size_of::<firmware_volume::FfsFileHeader>() + section_size;
290        let file_offset = size_of::<firmware_volume::FirmwareVolumeHeader>().next_multiple_of(8);
291        fv[file_offset..file_offset + 16].copy_from_slice(firmware_version::FILE_GUID.as_bytes());
292        fv[file_offset + 20..file_offset + 23].copy_from_slice(&size24(file_size));
293        let section_offset = file_offset + size_of::<firmware_volume::FfsFileHeader>();
294        fv[section_offset..section_offset + 3].copy_from_slice(&size24(section_size));
295        fv[section_offset + 3] = firmware_volume::SECTION_RAW;
296        fv[section_offset + size_of::<firmware_volume::CommonSectionHeader>()
297            ..file_offset + file_size]
298            .copy_from_slice(&record);
299        fv
300    }
301
302    #[test]
303    fn finds_record_by_ffs_file_guid() {
304        let fv = volume(0x1000, Some((1, 0)));
305        let version = find_in_firmware_volume(&fv).unwrap().unwrap();
306        assert_eq!(version.interface_version_major, 1);
307        assert_eq!(version.interface_version_minor, 0);
308
309        let mut image = vec![0; FIRST_FIRMWARE_VOLUME_OFFSET];
310        image.extend_from_slice(&fv);
311        let version = find_in_firmware_image(&image).unwrap().unwrap();
312        assert_eq!(version.interface_version_major, 1);
313        assert_eq!(version.interface_version_minor, 0);
314    }
315
316    fn image(split: bool, interface: Option<(u16, u16)>) -> Vec<u8> {
317        #[cfg(guest_arch = "x86_64")]
318        let mut image = Vec::new();
319        #[cfg(guest_arch = "aarch64")]
320        let mut image = vec![0; 0x1000];
321        if split {
322            image.extend(volume(0x1ff000, None));
323        }
324        image.extend(volume(0x600000, interface));
325        image
326    }
327
328    #[test]
329    fn parses_combined_layout_with_both_interface_versions() {
330        for major in [1, 2] {
331            let image = image(false, Some((major, 0)));
332            let version = find_in_firmware_image(&image).unwrap().unwrap();
333            assert_eq!(version.interface_version_major, major);
334            assert_eq!(version.interface_version_minor, 0);
335        }
336        assert_eq!(find_in_firmware_image(&image(false, None)).unwrap(), None);
337    }
338
339    #[test]
340    #[cfg(guest_arch = "aarch64")]
341    fn parses_old_split_layout() {
342        let image = image(true, Some((1, 0)));
343        let version = find_in_firmware_image(&image).unwrap().unwrap();
344        assert_eq!(version.interface_version_major, 1);
345        assert_eq!(version.interface_version_minor, 0);
346        assert_eq!(
347            find_in_firmware_image(&self::image(true, None)).unwrap(),
348            None
349        );
350    }
351
352    #[test]
353    fn rejects_malformed_volumes_and_records() {
354        #[cfg(guest_arch = "x86_64")]
355        let layouts = [(false, 0)];
356        #[cfg(guest_arch = "aarch64")]
357        let layouts = [(false, 0x1000), (true, 0x200000)];
358        for (split, offset) in layouts {
359            let original = image(split, Some((2, 0)));
360            let mut malformed = original.clone();
361            malformed[offset + 32..offset + 40].copy_from_slice(&u64::MAX.to_le_bytes());
362            assert_eq!(find_in_firmware_image(&malformed), Err(Error::VolumeHeader));
363            let mut malformed = original.clone();
364            malformed[offset + 40..offset + 44].fill(0);
365            assert_eq!(find_in_firmware_image(&malformed), Err(Error::VolumeHeader));
366            let mut truncated = original.clone();
367            truncated.pop();
368            assert_eq!(find_in_firmware_image(&truncated), Err(Error::VolumeHeader));
369            let mut malformed = original;
370            let record_offset = offset
371                + size_of::<firmware_volume::FirmwareVolumeHeader>().next_multiple_of(8)
372                + size_of::<firmware_volume::FfsFileHeader>()
373                + size_of::<firmware_volume::CommonSectionHeader>();
374            malformed[record_offset] = 0;
375            assert_eq!(
376                find_in_firmware_image(&malformed),
377                Err(Error::InvalidSignature)
378            );
379        }
380        assert_eq!(find_in_firmware_image(&[]), Err(Error::VolumeHeader));
381    }
382
383    #[test]
384    #[cfg(guest_arch = "aarch64")]
385    fn rejects_non_progressing_volume_lengths() {
386        for length in [0_u64, 1] {
387            let mut malformed = image(true, Some((1, 0)));
388            malformed[0x1000 + 32..0x1000 + 40].copy_from_slice(&length.to_le_bytes());
389            assert_eq!(find_in_firmware_image(&malformed), Err(Error::VolumeHeader));
390        }
391    }
392}