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 DXE_FIRMWARE_VOLUME_OFFSET: usize = 0;
17#[cfg(guest_arch = "aarch64")]
18const DXE_FIRMWARE_VOLUME_OFFSET: usize = 0x20_0000;
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(DXE_FIRMWARE_VOLUME_OFFSET..)
191        .ok_or(Error::VolumeHeader)?;
192    find_in_firmware_volume(fv)
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use zerocopy::IntoBytes;
199
200    fn size24(value: usize) -> [u8; 3] {
201        let value = (value as u32).to_le_bytes();
202        [value[0], value[1], value[2]]
203    }
204
205    fn record(major: u16, minor: u16) -> [u8; firmware_version::HEADER_SIZE] {
206        let mut record = [0; firmware_version::HEADER_SIZE];
207        record[0..4].copy_from_slice(firmware_version::SIGNATURE);
208        record[4..6].copy_from_slice(&firmware_version::STRUCT_VERSION.to_le_bytes());
209        record[6..8].copy_from_slice(&(firmware_version::HEADER_SIZE as u16).to_le_bytes());
210        record[8..12].copy_from_slice(
211            &(firmware_version::FLAG_DIRTY | firmware_version::FLAG_OFFICIAL).to_le_bytes(),
212        );
213        record[12..14].copy_from_slice(&major.to_le_bytes());
214        record[14..16].copy_from_slice(&minor.to_le_bytes());
215        record[16..21].copy_from_slice(b"26.0\0");
216        record[32..40].copy_from_slice(b"deadbee\0");
217        record
218    }
219
220    #[test]
221    fn parses_record() {
222        let record = record(1, 2);
223        let version = parse_record(&record).unwrap();
224        assert_eq!(version.interface_version_major, 1);
225        assert_eq!(version.interface_version_minor, 2);
226        assert_eq!(version.base_version, "26.0");
227        assert_eq!(version.git_commit, "deadbee");
228        assert_eq!(
229            version.flags,
230            firmware_version::FLAG_DIRTY | firmware_version::FLAG_OFFICIAL
231        );
232    }
233
234    #[test]
235    fn rejects_truncated_record() {
236        assert_eq!(parse_record(b"MVFW").unwrap_err(), Error::TruncatedRecord);
237    }
238
239    #[test]
240    fn rejects_non_ascii_string() {
241        let mut record = record(1, 0);
242        record[16..19].copy_from_slice(b"\xc3\xa9\0");
243        assert_eq!(
244            parse_record(&record).unwrap_err(),
245            Error::InvalidString("base version")
246        );
247    }
248
249    #[test]
250    fn finds_record_by_ffs_file_guid() {
251        let record = record(1, 0);
252        let section_size = size_of::<firmware_volume::CommonSectionHeader>() + record.len();
253        let file_size = size_of::<firmware_volume::FfsFileHeader>() + section_size;
254        let file_offset = size_of::<firmware_volume::FirmwareVolumeHeader>().next_multiple_of(8);
255        let mut fv = [0xff; 0x1000];
256        let fv_length = fv.len() as u64;
257        fv[32..40].copy_from_slice(&fv_length.to_le_bytes());
258        fv[40..44].copy_from_slice(b"_FVH");
259        fv[48..50].copy_from_slice(
260            &(size_of::<firmware_volume::FirmwareVolumeHeader>() as u16).to_le_bytes(),
261        );
262        fv[file_offset..file_offset + 16].copy_from_slice(firmware_version::FILE_GUID.as_bytes());
263        fv[file_offset + 20..file_offset + 23].copy_from_slice(&size24(file_size));
264        let section_offset = file_offset + size_of::<firmware_volume::FfsFileHeader>();
265        fv[section_offset..section_offset + 3].copy_from_slice(&size24(section_size));
266        fv[section_offset + 3] = firmware_volume::SECTION_RAW;
267        fv[section_offset + size_of::<firmware_volume::CommonSectionHeader>()
268            ..file_offset + file_size]
269            .copy_from_slice(&record);
270
271        let version = find_in_firmware_volume(&fv).unwrap().unwrap();
272        assert_eq!(version.interface_version_major, 1);
273        assert_eq!(version.interface_version_minor, 0);
274
275        let mut image = vec![0; DXE_FIRMWARE_VOLUME_OFFSET];
276        image.extend_from_slice(&fv);
277        let version = find_in_firmware_image(&image).unwrap().unwrap();
278        assert_eq!(version.interface_version_major, 1);
279        assert_eq!(version.interface_version_minor, 0);
280    }
281}