1use 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#[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#[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
185pub 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 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}