Skip to main content

loader/
smbios.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Arch-neutral SMBIOS 3.x (DMI) table builder.
5//!
6//! In firmware-less Linux direct boot there is no UEFI/PCAT firmware to
7//! synthesize SMBIOS tables, so the loader must build them itself. This module
8//! builds a SMBIOS 3.1 entry point (`_SM3_`) and a minimal structure table
9//! (Type 0 BIOS, Type 1 System, Type 127 End-of-table). The caller decides
10//! where the structure table lives in guest memory and how the entry point is
11//! delivered to the guest (x86 F-segment scan vs. aarch64 EFI configuration
12//! table).
13
14mod spec;
15
16use spec::Smbios30EntryPoint;
17use spec::SmbiosType0;
18use spec::SmbiosType1;
19use spec::SmbiosType127;
20use zerocopy::IntoBytes;
21use zerocopy::LE;
22use zerocopy::U16;
23
24/// BIOS Information (SMBIOS Type 0).
25#[derive(Debug, Copy, Clone)]
26pub struct SmbiosBiosInfo<'a> {
27    /// BIOS vendor string.
28    pub vendor: &'a str,
29    /// BIOS version string.
30    pub version: &'a str,
31    /// BIOS release date string.
32    pub release_date: &'a str,
33    /// System BIOS Major Release.
34    pub major: u8,
35    /// System BIOS Minor Release.
36    pub minor: u8,
37}
38
39/// System Information (SMBIOS Type 1).
40#[derive(Debug, Copy, Clone)]
41pub struct SmbiosSystemInfo<'a> {
42    /// System manufacturer string.
43    pub manufacturer: &'a str,
44    /// System product name string.
45    pub product_name: &'a str,
46    /// System version string.
47    pub version: &'a str,
48    /// System serial number string.
49    pub serial_number: &'a str,
50    /// System SKU number string.
51    pub sku_number: &'a str,
52    /// System family string.
53    pub family: &'a str,
54    /// System UUID, as raw EFI GUID bytes (mixed-endian, as stored by the UEFI
55    /// path).
56    pub uuid: [u8; 16],
57}
58
59/// Aggregate of the SMBIOS structures to build. The caller supplies all of the
60/// identity strings and the system UUID.
61#[derive(Debug, Copy, Clone)]
62pub struct SmbiosTables<'a> {
63    /// Type 0 BIOS Information.
64    pub bios: SmbiosBiosInfo<'a>,
65    /// Type 1 System Information.
66    pub system: SmbiosSystemInfo<'a>,
67}
68
69/// Size in bytes of the SMBIOS 3.1 entry point (`_SM3_`). Callers that place
70/// the entry point and structure table separately (e.g. the aarch64 EFI
71/// configuration-table path) use this to reserve space for the entry point
72/// before knowing the structure table's address.
73pub const ENTRY_POINT_SIZE: usize = size_of::<Smbios30EntryPoint>();
74
75/// The built SMBIOS blobs, ready to be placed in guest memory.
76#[derive(Debug, Clone)]
77pub struct BuiltSmbios {
78    /// The 24-byte `_SM3_` entry point.
79    pub entry_point: Vec<u8>,
80    /// The structure table (Type 0, Type 1, Type 127, plus string sets).
81    pub structure_table: Vec<u8>,
82}
83
84/// Accumulates the strings referenced by a single SMBIOS structure and emits
85/// the trailing string set.
86#[derive(Default)]
87struct StringSet {
88    strings: Vec<String>,
89}
90
91impl StringSet {
92    /// Adds a string and returns its 1-based index, or 0 ("no string") for an
93    /// empty string.
94    ///
95    /// SMBIOS strings are NUL-terminated, so a string set cannot contain an
96    /// interior NUL. The string is truncated at the first NUL (if any) so that
97    /// caller-supplied data cannot corrupt the NUL-separated string set framing
98    /// (bytes after an interior NUL would otherwise be parsed as a separate
99    /// string, shifting every subsequent string index).
100    fn add(&mut self, s: &str) -> u8 {
101        let s = s.split('\0').next().unwrap_or("");
102        if s.is_empty() {
103            return 0;
104        }
105        self.strings.push(s.to_string());
106        self.strings.len().try_into().unwrap()
107    }
108
109    /// Appends the NUL-terminated string set to `out`, ending with the extra
110    /// NUL that terminates the structure. A structure with no strings emits two
111    /// NUL bytes.
112    fn write_to(&self, out: &mut Vec<u8>) {
113        if self.strings.is_empty() {
114            out.extend_from_slice(&[0, 0]);
115            return;
116        }
117        for s in &self.strings {
118            out.extend_from_slice(s.as_bytes());
119            out.push(0);
120        }
121        out.push(0);
122    }
123}
124
125/// Builds the SMBIOS entry point and structure table.
126///
127/// `table_gpa` is the guest physical address at which the returned
128/// `structure_table` will be placed; it is written into the entry point's
129/// `table_addr` field.
130pub fn build(tables: &SmbiosTables<'_>, table_gpa: u64) -> BuiltSmbios {
131    let mut structure_table = Vec::new();
132
133    // Each structure needs a handle that is unique within the table; hand them
134    // out sequentially. The values are arbitrary.
135    let mut next_handle = 0u16;
136    let mut handle = || {
137        let h = next_handle;
138        next_handle += 1;
139        U16::<LE>::new(h)
140    };
141
142    // Type 0 — BIOS Information.
143    {
144        let mut strings = StringSet::default();
145        let vendor = strings.add(tables.bios.vendor);
146        let bios_version = strings.add(tables.bios.version);
147        let bios_release_date = strings.add(tables.bios.release_date);
148        let t0 = SmbiosType0 {
149            typ: 0,
150            length: size_of::<SmbiosType0>() as u8,
151            handle: handle(),
152            vendor,
153            bios_version,
154            bios_segment: 0.into(),
155            bios_release_date,
156            bios_size: 0,
157            characteristics: spec::BIOS_CHARACTERISTICS_PCI_SUPPORTED.into(),
158            characteristics_ext: [
159                spec::BIOS_CHARACTERISTICS_EXT1_ACPI,
160                spec::BIOS_CHARACTERISTICS_EXT2_VM,
161            ],
162            bios_major: tables.bios.major,
163            bios_minor: tables.bios.minor,
164            ec_major: 0xff,
165            ec_minor: 0xff,
166            ext_rom_size: 0.into(),
167        };
168        structure_table.extend_from_slice(t0.as_bytes());
169        strings.write_to(&mut structure_table);
170    }
171
172    // Type 1 — System Information.
173    {
174        let mut strings = StringSet::default();
175        let manufacturer = strings.add(tables.system.manufacturer);
176        let product_name = strings.add(tables.system.product_name);
177        let version = strings.add(tables.system.version);
178        let serial_number = strings.add(tables.system.serial_number);
179        let sku_number = strings.add(tables.system.sku_number);
180        let family = strings.add(tables.system.family);
181        let t1 = SmbiosType1 {
182            typ: 1,
183            length: size_of::<SmbiosType1>() as u8,
184            handle: handle(),
185            manufacturer,
186            product_name,
187            version,
188            serial_number,
189            uuid: tables.system.uuid,
190            wake_up_type: spec::WAKE_UP_TYPE_POWER_SWITCH,
191            sku_number,
192            family,
193        };
194        structure_table.extend_from_slice(t1.as_bytes());
195        strings.write_to(&mut structure_table);
196    }
197
198    // Type 127 — End of Table.
199    {
200        let t127 = SmbiosType127 {
201            typ: 127,
202            length: size_of::<SmbiosType127>() as u8,
203            handle: handle(),
204        };
205        structure_table.extend_from_slice(t127.as_bytes());
206        // End-of-table has no strings: emit the double-NUL terminator.
207        structure_table.extend_from_slice(&[0, 0]);
208    }
209
210    let mut entry_point = Smbios30EntryPoint {
211        anchor: *b"_SM3_",
212        checksum: 0,
213        length: size_of::<Smbios30EntryPoint>() as u8,
214        major: 3,
215        minor: 1,
216        docrev: 0,
217        revision: 0x01,
218        reserved: 0,
219        max_size: u32::try_from(structure_table.len()).unwrap().into(),
220        table_addr: table_gpa.into(),
221    };
222    let sum = entry_point
223        .as_bytes()
224        .iter()
225        .fold(0u8, |acc, b| acc.wrapping_add(*b));
226    entry_point.checksum = 0u8.wrapping_sub(sum);
227
228    BuiltSmbios {
229        entry_point: entry_point.as_bytes().to_vec(),
230        structure_table,
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    fn test_smbios_tables() -> SmbiosTables<'static> {
239        SmbiosTables {
240            system: SmbiosSystemInfo {
241                manufacturer: "Test Manufacturer",
242                product_name: "Test Product",
243                version: "Test Version",
244                serial_number: "",
245                sku_number: "Test SKU",
246                family: "Test Family",
247                uuid: [0; 16],
248            },
249            bios: SmbiosBiosInfo {
250                vendor: "Test BIOS Vendor",
251                version: "Test BIOS Version",
252                release_date: "Test BIOS Release Date",
253                major: 1,
254                minor: 2,
255            },
256        }
257    }
258
259    #[test]
260    fn entry_point_checksum_is_zero() {
261        let built = build(&test_smbios_tables(), 0xf0020);
262        let sum = built
263            .entry_point
264            .iter()
265            .fold(0u8, |acc, b| acc.wrapping_add(*b));
266        assert_eq!(sum, 0);
267    }
268
269    #[test]
270    fn entry_point_fields() {
271        let table_gpa = 0xf0020;
272        let built = build(&test_smbios_tables(), table_gpa);
273        assert_eq!(built.entry_point.len(), 0x18);
274        assert_eq!(&built.entry_point[0..5], b"_SM3_");
275        assert_eq!(built.entry_point[6], 0x18); // length
276        assert_eq!(built.entry_point[7], 3); // major
277        assert_eq!(built.entry_point[8], 1); // minor
278
279        // max_size (offset 0x0c) == structure table length.
280        let max_size = u32::from_le_bytes(built.entry_point[0x0c..0x10].try_into().unwrap());
281        assert_eq!(max_size as usize, built.structure_table.len());
282
283        // table_addr (offset 0x10) == the GPA we passed in.
284        let addr = u64::from_le_bytes(built.entry_point[0x10..0x18].try_into().unwrap());
285        assert_eq!(addr, table_gpa);
286    }
287
288    #[test]
289    fn structure_table_layout() {
290        let built = build(&test_smbios_tables(), 0xf0020);
291        let table = &built.structure_table;
292
293        // Type 0 header.
294        assert_eq!(table[0], 0); // type
295        assert_eq!(table[1], 0x1a); // length
296
297        // Type 0 string indices are assigned in order.
298        assert_eq!(table[4], 1); // vendor -> string #1
299        assert_eq!(table[5], 2); // bios_version -> string #2
300
301        // The structure table must end with the Type 127 end-of-table marker
302        // (type, length, handle) followed by the double-NUL terminator. The
303        // handle is the third one allocated (0, 1, 2), i.e. 2 little-endian.
304        let n = table.len();
305        assert_eq!(&table[n - 6..], &[127, 4, 0x02, 0x00, 0, 0]);
306    }
307
308    #[test]
309    fn strings_resolve() {
310        let built = build(&test_smbios_tables(), 0);
311        // The first string in the table (after the 0x1a-byte Type 0 formatted
312        // area) is the BIOS vendor.
313        let strings_start = 0x1a;
314        let nul = built.structure_table[strings_start..]
315            .iter()
316            .position(|&b| b == 0)
317            .unwrap();
318        let vendor = &built.structure_table[strings_start..strings_start + nul];
319        assert_eq!(vendor, "Test BIOS Vendor".as_bytes());
320    }
321
322    #[test]
323    fn empty_string_uses_index_zero() {
324        let tables = test_smbios_tables();
325        let built = build(&tables, 0);
326        let t1_off = struct_offset(&built.structure_table, 1).expect("Type 1 present");
327        // serial_number is the 4th string field, at offset 7 within the Type 1
328        // formatted area (type, length, handle:2, manufacturer, product_name,
329        // version, serial_number).
330        assert_eq!(built.structure_table[t1_off + 7], 0);
331    }
332
333    #[test]
334    fn interior_nul_is_truncated() {
335        let mut tables = test_smbios_tables();
336        // An interior NUL must not corrupt the NUL-separated string set: the
337        // string is truncated at the NUL and following bytes are dropped, so
338        // string indices are not shifted.
339        tables.system.manufacturer = "Mfg\0evil";
340        let built = build(&tables, 0);
341        let t1_off = struct_offset(&built.structure_table, 1).expect("Type 1 present");
342        // manufacturer is still string #1, product_name still #2 (unshifted).
343        assert_eq!(built.structure_table[t1_off + 4], 1);
344        assert_eq!(built.structure_table[t1_off + 5], 2);
345        // The string set holds the truncated manufacturer and none of the bytes
346        // after the interior NUL.
347        let len = built.structure_table[t1_off + 1] as usize;
348        let strings = &built.structure_table[t1_off + len..];
349        let first_nul = strings.iter().position(|&b| b == 0).unwrap();
350        assert_eq!(&strings[..first_nul], b"Mfg");
351        assert!(!strings.windows(4).any(|w| w == b"evil"));
352    }
353
354    /// Walks the structure table and returns the byte offset of the first
355    /// structure with the given type, or `None`.
356    fn struct_offset(table: &[u8], want: u8) -> Option<usize> {
357        let mut off = 0;
358        while off + 2 <= table.len() {
359            let typ = table[off];
360            let formatted_len = table[off + 1] as usize;
361            if typ == want {
362                return Some(off);
363            }
364            // Skip the formatted area, then scan past the string set, which is
365            // terminated by a double-NUL.
366            let mut i = off + formatted_len;
367            while i + 1 < table.len() && !(table[i] == 0 && table[i + 1] == 0) {
368                i += 1;
369            }
370            off = i + 2;
371        }
372        None
373    }
374}