Skip to main content

acpi/
ssdt.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4pub use crate::aml::*;
5use memory_range::MemoryRange;
6use zerocopy::FromBytes;
7use zerocopy::Immutable;
8use zerocopy::IntoBytes;
9use zerocopy::KnownLayout;
10
11#[repr(C, packed)]
12#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)]
13pub struct DescriptionHeader {
14    pub signature: u32,
15    _length: u32, // placeholder, filled in during serialization to bytes
16    pub revision: u8,
17    _checksum: u8, // placeholder, filled in during serialization to bytes
18    pub oem_id: [u8; 6],
19    pub oem_table_id: u64,
20    pub oem_revision: u32,
21    pub creator_id: u32,
22    pub creator_rev: u32,
23}
24
25fn encode_pcie_name(mut pcie_index: u32) -> Vec<u8> {
26    assert!(pcie_index < 1000);
27    let mut temp = "PCI0".as_bytes().to_vec();
28    let mut i = temp.len() - 1;
29    while pcie_index > 0 {
30        temp[i] = b'0' + (pcie_index % 10) as u8;
31        pcie_index /= 10;
32        i -= 1;
33    }
34    temp
35}
36
37pub struct Ssdt {
38    description_header: DescriptionHeader,
39    objects: Vec<u8>,
40    pcie_ecam_ranges: Vec<MemoryRange>,
41}
42
43/// Parameters for adding a PCIe host bridge to the SSDT.
44pub struct PcieHostBridgeEntry {
45    /// Unique index of this host bridge.
46    pub index: u32,
47    /// PCIe segment number.
48    pub segment: u16,
49    /// Lowest valid bus number.
50    pub start_bus: u8,
51    /// Highest valid bus number.
52    pub end_bus: u8,
53    /// Memory range for ECAM configuration space access.
54    pub ecam_range: MemoryRange,
55    /// Memory range for low MMIO.
56    pub low_mmio: MemoryRange,
57    /// Memory range for high MMIO.
58    pub high_mmio: MemoryRange,
59    /// Whether this host bridge supports CXL.
60    pub cxl: bool,
61    /// NUMA proximity domain.
62    pub vnode: Option<u32>,
63    /// When true, emit a `_DSM` method ("Ignore PCI Boot Configurations", PCI
64    /// Firmware Spec §4.6 function 5) instructing the guest OS to keep the
65    /// firmware-assigned PCI boot configuration (bus numbers and BARs) rather
66    /// than reprogramming it.
67    pub preserve_boot_config: bool,
68}
69
70impl Ssdt {
71    pub fn new() -> Self {
72        Self {
73            description_header: DescriptionHeader {
74                signature: u32::from_le_bytes(*b"SSDT"),
75                _length: 0,
76                revision: 2,
77                _checksum: 0,
78                oem_id: *b"MSFTVM",
79                oem_table_id: 0x313054445353, // b'SSDT01'
80                oem_revision: 1,
81                creator_id: u32::from_le_bytes(*b"MSFT"),
82                creator_rev: 0x01000000,
83            },
84            objects: vec![],
85            pcie_ecam_ranges: vec![],
86        }
87    }
88
89    pub fn to_bytes(&self) -> Vec<u8> {
90        let mut byte_stream = Vec::new();
91        byte_stream.extend_from_slice(self.description_header.as_bytes());
92        byte_stream.extend_from_slice(&self.objects);
93
94        // N.B. Certain guest OSes will only probe ECAM ranges if they are
95        // reserved in the resources of an ACPI motherboard device.
96        if !self.pcie_ecam_ranges.is_empty() {
97            let mut vmod = Device::new(b"VMOD");
98            vmod.add_object(&NamedObject::new(b"_HID", &EisaId(*b"PNP0C02")));
99
100            let mut crs = CurrentResourceSettings::new();
101            for ecam_range in &self.pcie_ecam_ranges {
102                crs.add_resource(&QwordMemory::new(
103                    ecam_range.start(),
104                    ecam_range.end() - ecam_range.start(),
105                ));
106            }
107            vmod.add_object(&crs);
108            vmod.append_to_vec(&mut byte_stream);
109        }
110
111        let length = byte_stream.len();
112        byte_stream[4..8].copy_from_slice(&u32::try_from(length).unwrap().to_le_bytes());
113        let mut checksum: u8 = 0;
114        for byte in &byte_stream {
115            checksum = checksum.wrapping_add(*byte);
116        }
117
118        byte_stream[9] = (!checksum).wrapping_add(1);
119        byte_stream
120    }
121
122    pub fn add_object(&mut self, obj: &impl AmlObject) {
123        obj.append_to_vec(&mut self.objects);
124    }
125
126    /// Adds a PCI Express root complex with the specified bus number and MMIO ranges.
127    ///
128    /// ```text
129    /// Device(\_SB.PCI<N>)
130    /// {
131    ///     Name(_HID, PNP0A08)
132    ///     Name(_CID, PNP0A03)
133    ///     Name(_UID, <index>)
134    ///     Name(_SEG, <segment>)
135    ///     Name(_BBN, <bus number>)
136    ///     Name(_CCA, 1)
137    ///     Name(_CRS, ResourceTemplate()
138    ///     {
139    ///         WordBusNumber(...) // Bus number range
140    ///         QWordMemory() // Low MMIO
141    ///         QWordMemory() // High MMIO
142    ///     })
143    /// }
144    /// ```
145    pub fn add_pcie(&mut self, entry: PcieHostBridgeEntry) {
146        let PcieHostBridgeEntry {
147            index,
148            segment,
149            start_bus,
150            end_bus,
151            ecam_range,
152            low_mmio,
153            high_mmio,
154            cxl,
155            vnode,
156            preserve_boot_config,
157        } = entry;
158        let mut pcie = Device::new(encode_pcie_name(index).as_slice());
159        if cxl {
160            // As recommended by the CXL specification, describe CXL host bridges with _HID "ACPI0016"
161            // and both PNP0A03 and PNP0A08 in _CID to maximize compatibility with different OSes.
162            pcie.add_object(&NamedString::new(b"_HID", b"ACPI0016"));
163            let mut cid_data = Vec::new();
164            cid_data.extend_from_slice(&EisaId(*b"PNP0A03").to_bytes());
165            cid_data.extend_from_slice(&EisaId(*b"PNP0A08").to_bytes());
166            pcie.add_object(&NamedObject::new(
167                b"_CID",
168                &StructuredPackage {
169                    elem_count: 2,
170                    elem_data: cid_data,
171                },
172            ));
173        } else {
174            pcie.add_object(&NamedObject::new(b"_HID", &EisaId(*b"PNP0A08")));
175            pcie.add_object(&NamedObject::new(b"_CID", &EisaId(*b"PNP0A03")));
176        }
177        pcie.add_object(&NamedInteger::new(b"_UID", index.into()));
178        pcie.add_object(&NamedInteger::new(b"_SEG", segment.into()));
179        pcie.add_object(&NamedInteger::new(b"_BBN", start_bus.into()));
180        pcie.add_object(&NamedInteger::new(b"_CCA", 1));
181        if let Some(vnode) = vnode {
182            pcie.add_object(&NamedInteger::new(b"_PXM", vnode.into()));
183        }
184
185        // _OSC method: negotiate native control with the guest OS.
186        //
187        // Per ACPI spec §6.2.11 (_OSC, Operating System Capabilities), the OS
188        // calls _OSC to negotiate platform feature control.
189        //
190        // Supported UUIDs:
191        // - PCIe: 33DB4D5B-1FF7-401C-9657-7441C03DD766
192        //   (PCI Firmware Spec §4.5.1, Table 4-3)
193        // - CXL:  68F2D50B-C469-4D8A-BD3D-941A103FD3FC (CXL host bridge mode)
194        //
195        // Status DWORD[0] bits used here:
196        // - bit 1 (0x02): unrecognized revision (CXL path, Arg1 != 1)
197        // - bit 2 (0x04): unrecognized UUID
198        //   (ACPI spec §6.2.11.1, Table 6.15)
199        //
200        // Behavior:
201        // - PCIe UUID: clear status (grant all requested control)
202        // - CXL UUID: if Arg1 == 1 clear status; else set bit 1
203        // - Any other UUID: set bit 2
204        // - Return Arg3
205        let mut osc_method = Method::new(b"_OSC");
206        osc_method.set_arg_count(4);
207
208        // CreateDWordField(Arg3, 0, STS0)
209        osc_method.add_operation(&CreateDWordFieldOp {
210            source_buffer: encode_arg(3),
211            byte_index: encode_integer(0),
212            field_name: *b"STS0",
213        });
214
215        // If (LEqual(Arg0, ToUUID("33DB4D5B-1FF7-401C-9657-7441C03DD766")))
216        let pcie_osc_uuid = guid::guid!("33DB4D5B-1FF7-401C-9657-7441C03DD766");
217        let uuid_buffer = Buffer(pcie_osc_uuid.as_bytes()).to_bytes();
218        let lequal = LEqualOp {
219            left: encode_arg(0),
220            right: uuid_buffer,
221        };
222
223        let else_body = if cxl {
224            // CXL _OSC UUID: 68f2d50b-c469-4d8a-bd3d-941a103fd3fc
225            // Rev 1 is currently supported; unsupported revisions set STS0 bit 1.
226            let cxl_osc_uuid = guid::guid!("68f2d50b-c469-4d8a-bd3d-941a103fd3fc");
227            let cxl_uuid_buffer = Buffer(cxl_osc_uuid.as_bytes()).to_bytes();
228            let cxl_uuid_match = LEqualOp {
229                left: encode_arg(0),
230                right: cxl_uuid_buffer,
231            };
232
233            let cxl_revision_match = LEqualOp {
234                left: encode_arg(1),
235                right: encode_integer(1),
236            };
237
238            let cxl_store_zero = StoreOp {
239                source: encode_integer(0),
240                destination: b"STS0".to_vec(),
241            };
242            let cxl_rev_if_op = IfOp {
243                predicate: cxl_revision_match.to_bytes(),
244                body: cxl_store_zero.to_bytes(),
245            };
246
247            // STS0 bit 1: unrecognized revision.
248            let cxl_revision_or = OrOp {
249                operand1: b"STS0".to_vec(),
250                operand2: encode_integer(0x02),
251                target_name: b"STS0".to_vec(),
252            };
253            let cxl_revision_else = ElseOp {
254                body: cxl_revision_or.to_bytes(),
255            };
256
257            // STS0 bit 2: unrecognized UUID.
258            let uuid_or = OrOp {
259                operand1: b"STS0".to_vec(),
260                operand2: encode_integer(0x04),
261                target_name: b"STS0".to_vec(),
262            };
263            let unknown_uuid_else = ElseOp {
264                body: uuid_or.to_bytes(),
265            };
266
267            let cxl_if_op = IfOp {
268                predicate: cxl_uuid_match.to_bytes(),
269                body: {
270                    let mut body = Vec::new();
271                    cxl_rev_if_op.append_to_vec(&mut body);
272                    cxl_revision_else.append_to_vec(&mut body);
273                    body
274                },
275            };
276
277            ElseOp {
278                body: {
279                    let mut body = Vec::new();
280                    cxl_if_op.append_to_vec(&mut body);
281                    unknown_uuid_else.append_to_vec(&mut body);
282                    body
283                },
284            }
285        } else {
286            // STS0 bit 2: unrecognized UUID.
287            let uuid_or = OrOp {
288                operand1: b"STS0".to_vec(),
289                operand2: encode_integer(0x04),
290                target_name: b"STS0".to_vec(),
291            };
292            ElseOp {
293                body: uuid_or.to_bytes(),
294            }
295        };
296
297        // If block: UUID matches — clear status and grant everything
298        let store_zero = StoreOp {
299            source: encode_integer(0),
300            destination: b"STS0".to_vec(),
301        };
302        let if_op = IfOp {
303            predicate: lequal.to_bytes(),
304            body: store_zero.to_bytes(),
305        };
306        osc_method.add_operation(&if_op);
307        osc_method.add_operation(&else_body);
308
309        // Return(Arg3)
310        osc_method.add_operation(&ReturnOp {
311            result: encode_arg(3),
312        });
313
314        pcie.add_object(&osc_method);
315
316        // _DSM: Device Specific Method for preserving the firmware PCI boot
317        // configuration (bus numbers and BARs).
318        //
319        // UUID {E5C937D0-3553-4D7A-9117-EA4D19C3434D} is the PCI/PCIe host
320        // bridge _DSM defined in the PCI Firmware Specification §4.6.
321        //
322        // Function 0: returns a buffer with supported function bitmask
323        // Function 5 ("Ignore PCI Boot Configurations"): returns 0 to tell the
324        //             OS to preserve the firmware-programmed configuration
325        //
326        // When preserve_boot_config is false, no _DSM is emitted and the guest
327        // OS is free to reprogram bus numbers and BARs.
328        if preserve_boot_config {
329            let mut dsm_method = Method::new(b"_DSM");
330            dsm_method.set_arg_count(4);
331
332            let dsm_uuid = guid::guid!("E5C937D0-3553-4D7A-9117-EA4D19C3434D");
333            let dsm_uuid_buffer = Buffer(dsm_uuid.as_bytes()).to_bytes();
334
335            // If (LEqual(Arg0, UUID))
336            let uuid_match = LEqualOp {
337                left: encode_arg(0),
338                right: dsm_uuid_buffer,
339            };
340
341            // Function 0: return supported function bitmask (bits 0 and 5).
342            // Bit 0 = Function 0 supported, Bit 5 = Function 5 supported.
343            let fn0_match = LEqualOp {
344                left: encode_arg(2),
345                right: encode_integer(0),
346            };
347            let fn0_return = ReturnOp {
348                result: Buffer(&[0x21u8]).to_bytes(),
349            };
350            let fn0_if = IfOp {
351                predicate: fn0_match.to_bytes(),
352                body: fn0_return.to_bytes(),
353            };
354
355            // Function 5: return 0 (preserve firmware BAR assignments).
356            let fn5_match = LEqualOp {
357                left: encode_arg(2),
358                right: encode_integer(5),
359            };
360            let fn5_return = ReturnOp {
361                result: encode_integer(0),
362            };
363            let fn5_if = IfOp {
364                predicate: fn5_match.to_bytes(),
365                body: fn5_return.to_bytes(),
366            };
367
368            let uuid_if = IfOp {
369                predicate: uuid_match.to_bytes(),
370                body: {
371                    let mut body = Vec::new();
372                    fn0_if.append_to_vec(&mut body);
373                    fn5_if.append_to_vec(&mut body);
374                    body
375                },
376            };
377
378            dsm_method.add_operation(&uuid_if);
379
380            // Unrecognized UUID or function: return empty buffer.
381            dsm_method.add_operation(&ReturnOp {
382                result: Buffer(&[] as &[u8]).to_bytes(),
383            });
384
385            pcie.add_object(&dsm_method);
386        }
387
388        let mut crs = CurrentResourceSettings::new();
389        crs.add_resource(&BusNumber::new(
390            start_bus.into(),
391            (end_bus as u16) - (start_bus as u16) + 1,
392        ));
393        crs.add_resource(&QwordMemory::new(
394            low_mmio.start(),
395            low_mmio.end() - low_mmio.start(),
396        ));
397        crs.add_resource(&QwordMemory::new(
398            high_mmio.start(),
399            high_mmio.end() - high_mmio.start(),
400        ));
401        pcie.add_object(&crs);
402
403        self.add_object(&pcie);
404        self.pcie_ecam_ranges.push(ecam_range);
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411    use crate::aml::test_helpers::verify_expected_bytes;
412
413    fn verify_header(bytes: &[u8]) {
414        assert!(bytes.len() >= 36);
415
416        // signature
417        assert_eq!(bytes[0], b'S');
418        assert_eq!(bytes[1], b'S');
419        assert_eq!(bytes[2], b'D');
420        assert_eq!(bytes[3], b'T');
421
422        // length
423        let ssdt_len = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
424        assert_eq!(ssdt_len as usize, bytes.len());
425
426        // revision
427        assert_eq!(bytes[8], 2);
428
429        // Validate checksum bytes[9] by verifying content adds to zero.
430        let mut checksum: u8 = 0;
431        for byte in bytes.iter() {
432            checksum = checksum.wrapping_add(*byte);
433        }
434        assert_eq!(checksum, 0);
435
436        // oem_id
437        assert_eq!(bytes[10], b'M');
438        assert_eq!(bytes[11], b'S');
439        assert_eq!(bytes[12], b'F');
440        assert_eq!(bytes[13], b'T');
441        assert_eq!(bytes[14], b'V');
442        assert_eq!(bytes[15], b'M');
443
444        // oem_table_id
445        assert_eq!(bytes[16], b'S');
446        assert_eq!(bytes[17], b'S');
447        assert_eq!(bytes[18], b'D');
448        assert_eq!(bytes[19], b'T');
449        assert_eq!(bytes[20], b'0');
450        assert_eq!(bytes[21], b'1');
451        assert_eq!(bytes[22], 0);
452        assert_eq!(bytes[23], 0);
453
454        // oem_revision
455        let oem_revision = u32::from_le_bytes(bytes[24..28].try_into().unwrap());
456        assert_eq!(oem_revision, 1);
457
458        // creator_id
459        assert_eq!(bytes[28], b'M');
460        assert_eq!(bytes[29], b'S');
461        assert_eq!(bytes[30], b'F');
462        assert_eq!(bytes[31], b'T');
463
464        // creator_rev
465        let creator_rev = u32::from_le_bytes(bytes[32..36].try_into().unwrap());
466        assert_eq!(creator_rev, 0x01000000);
467    }
468
469    #[test]
470    fn verify_pcie_name_encoding() {
471        assert_eq!(encode_pcie_name(0), b"PCI0".to_vec());
472        assert_eq!(encode_pcie_name(1), b"PCI1".to_vec());
473        assert_eq!(encode_pcie_name(2), b"PCI2".to_vec());
474        assert_eq!(encode_pcie_name(54), b"PC54".to_vec());
475        assert_eq!(encode_pcie_name(294), b"P294".to_vec());
476    }
477
478    #[test]
479    fn verify_simple_table() {
480        let mut ssdt = Ssdt::new();
481        let nobj = NamedObject::new(b"_S0", &Package(vec![0, 0]));
482        ssdt.add_object(&nobj);
483        let bytes = ssdt.to_bytes();
484        verify_header(&bytes);
485        verify_expected_bytes(&bytes[36..], &[8, b'_', b'S', b'0', b'_', 0x12, 4, 2, 0, 0]);
486    }
487
488    fn test_pcie_entry(vnode: Option<u32>) -> PcieHostBridgeEntry {
489        PcieHostBridgeEntry {
490            index: 0,
491            segment: 0,
492            start_bus: 0,
493            end_bus: 255,
494            ecam_range: MemoryRange::new(0x1000_0000..0x2000_0000),
495            low_mmio: MemoryRange::new(0xdc00_0000..0xe000_0000),
496            high_mmio: MemoryRange::new(0x10_0000_0000..0x10_4000_0000),
497            cxl: false,
498            vnode,
499            preserve_boot_config: false,
500        }
501    }
502
503    fn contains_name(bytes: &[u8], name: &[u8; 4]) -> bool {
504        bytes.windows(4).any(|w| w == name)
505    }
506
507    fn contains_bytes(bytes: &[u8], needle: &[u8]) -> bool {
508        bytes.windows(needle.len()).any(|w| w == needle)
509    }
510
511    #[test]
512    fn pcie_includes_cca() {
513        let mut ssdt = Ssdt::new();
514        ssdt.add_pcie(test_pcie_entry(None));
515
516        let bytes = ssdt.to_bytes();
517        verify_header(&bytes);
518        assert!(
519            bytes
520                .windows(6)
521                .any(|window| window == [8, b'_', b'C', b'C', b'A', 1,])
522        );
523    }
524
525    #[test]
526    fn pcie_no_pxm_when_vnode_none() {
527        let mut ssdt = Ssdt::new();
528        ssdt.add_pcie(test_pcie_entry(None));
529
530        let bytes = ssdt.to_bytes();
531        verify_header(&bytes);
532        // _PXM should NOT be present when vnode is None.
533        assert!(
534            !bytes.windows(4).any(|window| window == *b"_PXM"),
535            "_PXM should not be emitted when vnode is None"
536        );
537    }
538
539    #[test]
540    fn pcie_includes_pxm() {
541        let mut ssdt = Ssdt::new();
542        ssdt.add_pcie(test_pcie_entry(Some(0)));
543
544        let bytes = ssdt.to_bytes();
545        verify_header(&bytes);
546        // _PXM with value 0
547        assert!(
548            bytes
549                .windows(6)
550                .any(|window| window == [8, b'_', b'P', b'X', b'M', 0,])
551        );
552    }
553
554    #[test]
555    fn pcie_pxm_nonzero_node() {
556        let mut ssdt = Ssdt::new();
557        ssdt.add_pcie(test_pcie_entry(Some(3)));
558
559        let bytes = ssdt.to_bytes();
560        verify_header(&bytes);
561        // _PXM with value 3: Name op (0x08) + "_PXM" + BytePrefix (0x0a) + 3
562        assert!(
563            bytes
564                .windows(7)
565                .any(|window| window == [8, b'_', b'P', b'X', b'M', 0x0a, 3,])
566        );
567    }
568
569    #[test]
570    fn pcie_dsm_present_when_preserve_boot_config() {
571        let mut ssdt = Ssdt::new();
572        ssdt.add_pcie(PcieHostBridgeEntry {
573            preserve_boot_config: true,
574            ..test_pcie_entry(None)
575        });
576
577        let bytes = ssdt.to_bytes();
578        verify_header(&bytes);
579
580        // _DSM method name must be present.
581        assert!(contains_name(&bytes, b"_DSM"));
582
583        // The PCI firmware _DSM UUID must appear in mixed-endian form
584        // (GUID wire format).
585        let uuid = guid::guid!("E5C937D0-3553-4D7A-9117-EA4D19C3434D");
586        assert!(contains_bytes(&bytes, uuid.as_bytes()));
587
588        // The supported-functions bitmask byte (0x21 = bits 0+5) must
589        // appear somewhere in the buffer encoding.
590        assert!(contains_bytes(&bytes, &[0x21]));
591    }
592
593    #[test]
594    fn pcie_dsm_absent_without_preserve_boot_config() {
595        let mut ssdt = Ssdt::new();
596        ssdt.add_pcie(PcieHostBridgeEntry {
597            preserve_boot_config: false,
598            ..test_pcie_entry(None)
599        });
600
601        let bytes = ssdt.to_bytes();
602        verify_header(&bytes);
603
604        // _DSM must NOT be present.
605        assert!(!contains_name(&bytes, b"_DSM"));
606
607        // The PCI firmware _DSM UUID must NOT appear.
608        let uuid = guid::guid!("E5C937D0-3553-4D7A-9117-EA4D19C3434D");
609        assert!(!contains_bytes(&bytes, uuid.as_bytes()));
610    }
611}