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