loader/uefi/
firmware_version.rs1use 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#[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(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}