Skip to main content

intel_vtd/spec/
scalable.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Scalable translation structures (VT-d Rev 4.1, §§3.4.3, 9.2, 9.4-9.6).
5//!
6//! These are formats, not a scalable-mode lookup implementation. The validators
7//! describe the second-stage-only profile: 39/48-bit walks, pass-through, 16-bit
8//! domain IDs, CM=0, RID_PASID, second-stage A/D, page-walk snooping and snoop
9//! control. First-stage/nested translation, memory types, explicit PASID, ATS,
10//! PRI and supervisor requests are unsupported. Defining this profile does not
11//! advertise or enable any of its capabilities.
12//!
13//! Validate only entries used by a request. Not-present entries ignore all
14//! fields except FPD (where defined). Callers must retain the original request's
15//! IOVA/source ID, accumulate context/directory/table FPD even on not-present
16//! entries, and apply it only to *qualified* faults (Table 26, §7.1.3).
17//! Format errors alone do not determine fault qualification.
18
19use super::root_context::AddressWidth;
20use bitfield_struct::bitfield;
21use inspect::Inspect;
22use open_enum::open_enum;
23use zerocopy::FromBytes;
24use zerocopy::Immutable;
25use zerocopy::IntoBytes;
26use zerocopy::KnownLayout;
27
28/// Entries in each 4KB lower/upper context table.
29pub const SCALABLE_CONTEXT_TABLE_ENTRIES: usize = 128;
30/// Entries in each 4KB PASID table.
31pub const PASID_TABLE_ENTRIES: u32 = 64;
32
33/// Validation failure in a scalable translation structure.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
35pub enum ScalableValidationError {
36    /// All fields except FPD (if any) are ignored.
37    #[error("entry not present")]
38    NotPresent,
39    /// A word has nonzero architecturally reserved bits.
40    #[error("reserved bits {bits:#x} in word {word}")]
41    ReservedBits {
42        /// Zero-based 64-bit word index.
43        word: usize,
44        /// Offending bits, relative to this word.
45        bits: u64,
46    },
47    /// Optional fields are reserved-zero in the supported profile.
48    #[error("unsupported fields {bits:#x} in word {word}")]
49    UnsupportedFields {
50        /// Zero-based 64-bit word index.
51        word: usize,
52        /// Offending bits, relative to this word.
53        bits: u64,
54    },
55    /// PGTT is unknown or outside the second-stage-only profile.
56    #[error("unsupported PASID translation type {0:?}")]
57    UnsupportedTranslationType(PasidTranslationType),
58    /// AW does not select a supported second-stage walk.
59    #[error("unsupported second-stage address width {0:?}")]
60    UnsupportedAddressWidth(AddressWidth),
61    /// PASID is outside the range selected by PDTS.
62    #[error("PASID {pasid:#x} exceeds directory limit {limit:#x} (exclusive)")]
63    PasidOutOfRange {
64        /// Requested PASID, without truncation.
65        pasid: u32,
66        /// Exclusive upper bound on PASID.
67        limit: u32,
68    },
69    /// PDTS must fit in its architectural three-bit field.
70    #[error("invalid PASID directory size encoding {0}")]
71    InvalidDirectorySize(u8),
72    /// A pointer's host address width must be between 12 and 64 bits.
73    #[error("invalid host address width {0}")]
74    InvalidHostAddressWidth(u8),
75}
76
77fn address_reserved_mask(haw: u8) -> Result<u64, ScalableValidationError> {
78    match haw {
79        12..=63 => Ok(u64::MAX << haw),
80        64 => Ok(0),
81        _ => Err(ScalableValidationError::InvalidHostAddressWidth(haw)),
82    }
83}
84
85fn reserved(word: usize, value: u64, mask: u64) -> Result<(), ScalableValidationError> {
86    let bits = value & mask;
87    if bits != 0 {
88        return Err(ScalableValidationError::ReservedBits { word, bits });
89    }
90    Ok(())
91}
92
93fn unsupported(word: usize, value: u64, mask: u64) -> Result<(), ScalableValidationError> {
94    let bits = value & mask;
95    if bits != 0 {
96        return Err(ScalableValidationError::UnsupportedFields { word, bits });
97    }
98    Ok(())
99}
100
101/// Scalable root entry (§9.2), indexed by the bus number.
102///
103/// Each half independently references a 4KB context table. Guest root entries
104/// occupy 16 bytes; their Rust representation has 8-byte alignment.
105#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, Immutable, KnownLayout)]
106#[repr(C)]
107pub struct ScalableRootEntry {
108    /// Devfn 0..=127 (devices 0..=15): LP and LCTP.
109    pub lower: ScalableRootEntryHalf,
110    /// Devfn 128..=255 (devices 16..=31): UP and UCTP.
111    pub upper: ScalableRootEntryHalf,
112}
113
114/// One 64-bit half of a scalable root entry (§9.2).
115#[bitfield(u64)]
116#[derive(IntoBytes, Immutable, KnownLayout, FromBytes, Inspect)]
117#[rustfmt::skip]
118pub struct ScalableRootEntryHalf {
119    /// Lower/upper present bit. Other fields are ignored when clear.
120    pub p: bool,
121    #[bits(11)]
122    _reserved: u64,
123    /// Lower/upper context table pointer, bits 63:12 of this half.
124    #[bits(52)]
125    pub ctp: u64,
126}
127
128impl ScalableRootEntry {
129    /// Select the half associated with the device/function (§3.4.3).
130    pub fn half(&self, devfn: u8) -> ScalableRootEntryHalf {
131        if devfn < 128 { self.lower } else { self.upper }
132    }
133}
134
135impl ScalableRootEntryHalf {
136    /// Full, 4KB-aligned context table address.
137    pub fn context_table_address(&self) -> u64 {
138        self.ctp() << 12
139    }
140
141    /// Validate this half; the other half is independent.
142    ///
143    /// Error word indices are relative to this half, not the full root entry.
144    pub fn validate(&self, haw: u8) -> Result<(), ScalableValidationError> {
145        if !self.p() {
146            return Err(ScalableValidationError::NotPresent);
147        }
148        reserved(0, self.into_bits(), 0xffe | address_reserved_mask(haw)?)
149    }
150}
151
152/// Scalable context entry (§9.4), 256 bits.
153#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, Immutable, KnownLayout)]
154#[repr(C)]
155pub struct ScalableContextEntry {
156    /// Present, FPD, optional request controls, PDTS and directory pointer.
157    pub lo: ScalableContextEntryLo,
158    /// RID_PASID and RID_PRIV.
159    pub hi: ScalableContextEntryHi,
160    /// Bits 255:128, reserved-zero.
161    pub reserved: [u64; 2],
162}
163
164/// Scalable context bits 63:0.
165#[bitfield(u64)]
166#[derive(IntoBytes, Immutable, KnownLayout, FromBytes, Inspect)]
167#[rustfmt::skip]
168pub struct ScalableContextEntryLo {
169    /// Present.
170    pub p: bool,
171    /// Evaluated even when P=0.
172    pub fpd: bool,
173    /// Device-TLB enable; reserved-zero without ATS.
174    pub dte: bool,
175    /// Explicit PASID enable; does not govern requests without PASID.
176    pub paside: bool,
177    /// Page request enable; reserved-zero without PRI.
178    pub pre: bool,
179    #[bits(4)]
180    _reserved: u64,
181    /// Directory size: 2^(PDTS+7) entries.
182    #[bits(3)]
183    pub pdts: u8,
184    /// PASID directory address, bits 63:12.
185    #[bits(52)]
186    pub pasiddirptr: u64,
187}
188
189/// Scalable context bits 127:64.
190#[bitfield(u64)]
191#[derive(IntoBytes, Immutable, KnownLayout, FromBytes, Inspect)]
192#[rustfmt::skip]
193pub struct ScalableContextEntryHi {
194    /// Implied PASID for requests without an explicit PASID.
195    #[bits(20)]
196    pub rid_pasid: u32,
197    /// Implied privilege; reserved-zero without RID_PRIV support.
198    pub rid_priv: bool,
199    #[bits(43)]
200    _reserved: u64,
201}
202
203/// Decode PDTS without truncating or shifting by an unchecked guest value.
204pub fn pasid_directory_entries(pdts: u8) -> Result<u32, ScalableValidationError> {
205    match pdts {
206        0..=7 => Ok(1u32 << (pdts + 7)),
207        _ => Err(ScalableValidationError::InvalidDirectorySize(pdts)),
208    }
209}
210
211impl ScalableContextEntry {
212    /// Index within the selected lower/upper context table (§3.4.3).
213    pub fn table_index(devfn: u8) -> usize {
214        usize::from(devfn & 0x7f)
215    }
216
217    /// Full, 4KB-aligned PASID directory address.
218    pub fn directory_address(&self) -> u64 {
219        self.lo.pasiddirptr() << 12
220    }
221
222    /// Checked directory and table indices for a PASID (§3.4.3).
223    ///
224    /// This checks both the PDTS bound and the architectural 20-bit limit
225    /// before extracting `PASID[19:6]` and `PASID[5:0]`.
226    pub fn pasid_indices(&self, pasid: u32) -> Result<(u16, u8), ScalableValidationError> {
227        let limit = pasid_directory_entries(self.lo.pdts())? * PASID_TABLE_ENTRIES;
228        if pasid >= limit {
229            return Err(ScalableValidationError::PasidOutOfRange { pasid, limit });
230        }
231        Ok(((pasid >> 6) as u16, (pasid & 0x3f) as u8))
232    }
233
234    /// Validate for the second-stage-only profile documented by this module.
235    pub fn validate(&self, haw: u8) -> Result<(), ScalableValidationError> {
236        if !self.lo.p() {
237            return Err(ScalableValidationError::NotPresent);
238        }
239        reserved(0, self.lo.into_bits(), 0x1e0 | address_reserved_mask(haw)?)?;
240        reserved(1, self.hi.into_bits(), u64::MAX << 21)?;
241        for (index, word) in self.reserved.iter().enumerate() {
242            reserved(index + 2, *word, u64::MAX)?;
243        }
244        unsupported(0, self.lo.into_bits(), 0x1c)?;
245        unsupported(1, self.hi.into_bits(), 1 << 20)?;
246        self.pasid_indices(self.hi.rid_pasid())?;
247        Ok(())
248    }
249}
250
251/// Scalable PASID-directory entry (§9.5), 64 bits.
252#[bitfield(u64)]
253#[derive(IntoBytes, Immutable, KnownLayout, FromBytes, Inspect)]
254#[rustfmt::skip]
255pub struct PasidDirectoryEntry {
256    /// Present.
257    pub p: bool,
258    /// Evaluated even when P=0; OR with the referencing context's FPD.
259    pub fpd: bool,
260    #[bits(10)]
261    _reserved: u64,
262    /// Pointer to a 4KB PASID table, bits 63:12.
263    #[bits(52)]
264    pub smptblptr: u64,
265}
266
267impl PasidDirectoryEntry {
268    /// Full, 4KB-aligned PASID table address.
269    pub fn table_address(&self) -> u64 {
270        self.smptblptr() << 12
271    }
272
273    /// Validate a directory entry, ignoring other fields when P=0.
274    pub fn validate(&self, haw: u8) -> Result<(), ScalableValidationError> {
275        if !self.p() {
276            return Err(ScalableValidationError::NotPresent);
277        }
278        reserved(0, self.into_bits(), 0xffc | address_reserved_mask(haw)?)
279    }
280}
281
282open_enum! {
283    /// PASID granular translation type (PGTT, §9.6).
284    #[derive(Inspect)]
285    #[inspect(debug)]
286    pub enum PasidTranslationType: u8 {
287        /// First-stage only (unsupported by the second-stage-only profile).
288        FIRST_STAGE = 0b001,
289        /// Second-stage only.
290        SECOND_STAGE = 0b010,
291        /// Nested first/second-stage (unsupported).
292        NESTED = 0b011,
293        /// Identity mapping.
294        PASS_THROUGH = 0b100,
295    }
296}
297
298/// Scalable PASID-table entry (§9.6), 512 bits.
299#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, Immutable, KnownLayout)]
300#[repr(C)]
301pub struct PasidTableEntry {
302    /// Present, FPD, AW, PGTT, SSADE and second-stage pointer.
303    pub lo: PasidTableEntryLo,
304    /// Domain ID and snoop/memory-type controls.
305    pub hi: PasidTableEntryHi,
306    /// First-stage controls (unsupported except for ignored bits 134:133).
307    pub first_stage: PasidTableEntryFirstStage,
308    /// Bits 511:192, reserved-zero.
309    pub reserved: [u64; 5],
310}
311
312/// PASID-table bits 63:0.
313#[bitfield(u64)]
314#[derive(IntoBytes, Immutable, KnownLayout, FromBytes, Inspect)]
315#[rustfmt::skip]
316pub struct PasidTableEntryLo {
317    /// Present.
318    pub p: bool,
319    /// Evaluated even when P=0; OR with context and directory FPD.
320    pub fpd: bool,
321    /// Second-stage address width; ignored for pass-through.
322    #[bits(3)]
323    pub aw: u8,
324    #[bits(1)]
325    _reserved1: u64,
326    /// PASID granular translation type; see [`PasidTranslationType`].
327    #[bits(3)]
328    pub pgtt: u8,
329    /// Second Stage Access/Dirty bit Enable (SSADE, bit 9).
330    /// Enables hardware updates of accessed and leaf dirty flags in the
331    /// referenced second-stage paging entries; ignored for pass-through.
332    pub ssade: bool,
333    #[bits(2)]
334    _reserved2: u64,
335    /// Second-stage root address, bits 63:12; ignored for pass-through
336    /// except that bits above HAW remain reserved.
337    #[bits(52)]
338    pub ssptptr: u64,
339}
340
341/// PASID-table bits 127:64.
342#[bitfield(u64)]
343#[derive(IntoBytes, Immutable, KnownLayout, FromBytes, Inspect)]
344#[rustfmt::skip]
345pub struct PasidTableEntryHi {
346    /// Domain identifier (all 16 bits supported, including zero with CM=0).
347    #[bits(16)]
348    pub did: u16,
349    #[bits(7)]
350    _reserved1: u64,
351    /// Page-walk snoop, full-entry bit 87.
352    pub pwsnp: bool,
353    /// Page snoop, full-entry bit 88 (supported with ECAP.SC).
354    pub pgsnp: bool,
355    /// Cache disable; reserved-zero without memory-type support.
356    pub cd: bool,
357    /// Extended memory type enable; reserved-zero without memory-type support.
358    pub emte: bool,
359    #[bits(5)]
360    _reserved2: u64,
361    /// Page attribute table; reserved-zero without memory-type support.
362    #[bits(32)]
363    pub pat: u32,
364}
365
366/// PASID-table bits 191:128.
367#[bitfield(u64)]
368#[derive(IntoBytes, Immutable, KnownLayout, FromBytes, Inspect)]
369#[rustfmt::skip]
370pub struct PasidTableEntryFirstStage {
371    /// Supervisor request enable.
372    pub sre: bool,
373    #[bits(1)]
374    _reserved1: u64,
375    /// First-stage paging mode.
376    #[bits(2)]
377    pub fspm: u8,
378    /// Write protect enable.
379    pub wpe: bool,
380    /// Must be ignored for compatibility with older software (§9.6).
381    #[bits(2)]
382    _ignored: u64,
383    /// Extended accessed flag enable.
384    pub eafe: bool,
385    #[bits(4)]
386    _reserved2: u64,
387    /// First-stage root address, bits 191:140 of the full entry.
388    #[bits(52)]
389    pub fsptptr: u64,
390}
391
392impl PasidTableEntry {
393    /// Full, 4KB-aligned second-stage root address.
394    pub fn page_table_address(&self) -> u64 {
395        self.lo.ssptptr() << 12
396    }
397
398    /// Validate for the second-stage-only profile documented by this module.
399    ///
400    /// PWSNP=0 is valid even with SSADE=1. Fault SSS.5 occurs only when an A/D
401    /// update is actually needed, not merely when the PASID entry is loaded.
402    pub fn validate(&self, haw: u8) -> Result<(), ScalableValidationError> {
403        if !self.lo.p() {
404            return Err(ScalableValidationError::NotPresent);
405        }
406        let address_reserved = address_reserved_mask(haw)?;
407        reserved(0, self.lo.into_bits(), 0xc20 | address_reserved)?;
408        reserved(1, self.hi.into_bits(), 0xf87f_0000)?;
409        reserved(2, self.first_stage.into_bits(), 0xf02 | address_reserved)?;
410        for (index, word) in self.reserved.iter().enumerate() {
411            reserved(index + 3, *word, u64::MAX)?;
412        }
413        unsupported(1, self.hi.into_bits(), 0xffff_ffff_0600_0000)?;
414        unsupported(
415            2,
416            self.first_stage.into_bits(),
417            0x9d | ((u64::MAX << 12) & !address_reserved),
418        )?;
419        match PasidTranslationType(self.lo.pgtt()) {
420            PasidTranslationType::SECOND_STAGE => match AddressWidth(self.lo.aw()) {
421                AddressWidth::AW_39BIT | AddressWidth::AW_48BIT => Ok(()),
422                aw => Err(ScalableValidationError::UnsupportedAddressWidth(aw)),
423            },
424            PasidTranslationType::PASS_THROUGH => Ok(()),
425            pgtt => Err(ScalableValidationError::UnsupportedTranslationType(pgtt)),
426        }
427    }
428}
429
430const _: () = {
431    assert!(size_of::<ScalableRootEntry>() == 16);
432    assert!(size_of::<ScalableContextEntry>() == 32);
433    assert!(size_of::<PasidDirectoryEntry>() == 8);
434    assert!(size_of::<PasidTableEntry>() == 64);
435    assert!(align_of::<ScalableRootEntry>() == 8);
436    assert!(align_of::<ScalableContextEntry>() == 8);
437    assert!(align_of::<PasidDirectoryEntry>() == 8);
438    assert!(align_of::<PasidTableEntry>() == 8);
439};
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use test_with_tracing::test;
445
446    fn decode<T: FromBytes + KnownLayout + Immutable>(words: &[u64]) -> T {
447        let bytes: Vec<_> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
448        T::read_from_bytes(&bytes).unwrap()
449    }
450
451    #[test]
452    fn test_sizes_alignment_and_offsets() {
453        assert_eq!(size_of::<ScalableRootEntry>(), 16);
454        assert_eq!(size_of::<ScalableRootEntryHalf>(), 8);
455        assert_eq!(size_of::<ScalableContextEntry>(), 32);
456        assert_eq!(size_of::<PasidDirectoryEntry>(), 8);
457        assert_eq!(size_of::<PasidTableEntry>(), 64);
458        assert_eq!(align_of::<ScalableRootEntry>(), 8);
459        assert_eq!(align_of::<ScalableContextEntry>(), 8);
460        assert_eq!(align_of::<PasidDirectoryEntry>(), 8);
461        assert_eq!(align_of::<PasidTableEntry>(), 8);
462        assert_eq!(std::mem::offset_of!(ScalableRootEntry, upper), 8);
463        assert_eq!(std::mem::offset_of!(ScalableContextEntry, hi), 8);
464        assert_eq!(std::mem::offset_of!(ScalableContextEntry, reserved), 16);
465        assert_eq!(std::mem::offset_of!(PasidTableEntry, hi), 8);
466        assert_eq!(std::mem::offset_of!(PasidTableEntry, first_stage), 16);
467        assert_eq!(std::mem::offset_of!(PasidTableEntry, reserved), 24);
468        assert_eq!(super::super::root_context::ROOT_TABLE_ENTRIES * 16, 4096);
469        assert_eq!(SCALABLE_CONTEXT_TABLE_ENTRIES * 32, 4096);
470        assert_eq!(PASID_TABLE_ENTRIES * 64, 4096);
471    }
472
473    #[test]
474    fn test_root_literal_bytes_and_half_boundaries() {
475        let entry: ScalableRootEntry = decode(&[0x1234_5001, 0xabcd_e001]);
476        assert!(entry.lower.p());
477        assert!(entry.upper.p());
478        assert_eq!(entry.lower.context_table_address(), 0x1234_5000);
479        assert_eq!(entry.upper.context_table_address(), 0xabcd_e000);
480        for devfn in 0..=255 {
481            let expected = if devfn < 128 {
482                0x1234_5000
483            } else {
484                0xabcd_e000
485            };
486            assert_eq!(entry.half(devfn).context_table_address(), expected);
487            assert_eq!(
488                ScalableContextEntry::table_index(devfn),
489                usize::from(devfn % 128)
490            );
491        }
492        for (devfn, offset) in [(0, 0), (127, 4064), (128, 0), (255, 4064)] {
493            assert_eq!(ScalableContextEntry::table_index(devfn) * 32, offset);
494        }
495        let built = ScalableRootEntry {
496            lower: ScalableRootEntryHalf::new().with_p(true).with_ctp(0x12345),
497            upper: ScalableRootEntryHalf::new().with_p(true).with_ctp(0xabcde),
498        };
499        assert_eq!(
500            built.as_bytes(),
501            &[
502                1, 0x50, 0x34, 0x12, 0, 0, 0, 0, 1, 0xe0, 0xcd, 0xab, 0, 0, 0, 0
503            ]
504        );
505    }
506
507    #[test]
508    fn test_root_reserved_bits_and_independent_presence() {
509        for bit in 1..64 {
510            let half = ScalableRootEntryHalf::from(1 | (1 << bit));
511            if (1..=11).contains(&bit) || bit >= 48 {
512                assert_eq!(
513                    half.validate(48),
514                    Err(ScalableValidationError::ReservedBits {
515                        word: 0,
516                        bits: 1 << bit
517                    })
518                );
519            } else {
520                assert_eq!(half.validate(48), Ok(()));
521            }
522        }
523        for words in [[1, u64::MAX - 1], [u64::MAX - 1, 1]] {
524            let entry: ScalableRootEntry = decode(&words);
525            for devfn in [0, 127, 128, 255] {
526                let half = entry.half(devfn);
527                if half.p() {
528                    assert_eq!(half.validate(48), Ok(()));
529                } else {
530                    assert_eq!(half.validate(48), Err(ScalableValidationError::NotPresent));
531                }
532            }
533        }
534    }
535
536    #[test]
537    fn test_context_literal_words() {
538        let entry: ScalableContextEntry = decode(&[0x1234_5678_9a03, 0x12345, 0, 0]);
539        assert!(entry.lo.p());
540        assert!(entry.lo.fpd());
541        assert_eq!(entry.lo.pdts(), 5);
542        assert_eq!(entry.directory_address(), 0x1234_5678_9000);
543        assert_eq!(entry.hi.rid_pasid(), 0x12345);
544        assert!(!entry.hi.rid_priv());
545        assert_eq!(entry.validate(48), Ok(()));
546        assert_eq!(
547            ScalableContextEntryLo::new()
548                .with_p(true)
549                .with_fpd(true)
550                .with_pdts(5)
551                .with_pasiddirptr(0x1_2345_6789)
552                .into_bits(),
553            0x1234_5678_9a03
554        );
555        assert_eq!(
556            ScalableContextEntryHi::new()
557                .with_rid_pasid(0xabcde)
558                .with_rid_priv(true)
559                .into_bits(),
560            0x1abcde
561        );
562        assert!(ScalableContextEntryLo::from(1 << 2).dte());
563        assert!(ScalableContextEntryLo::from(1 << 3).paside());
564        assert!(ScalableContextEntryLo::from(1 << 4).pre());
565    }
566
567    #[test]
568    fn test_context_every_bit_validation() {
569        for bit in 0..256 {
570            let mut words = [1, 0, 0, 0];
571            words[bit / 64] ^= 1 << (bit % 64);
572            let entry: ScalableContextEntry = decode(&words);
573            let result = entry.validate(48);
574            match bit {
575                0 => assert_eq!(result, Err(ScalableValidationError::NotPresent)),
576                1 | 9..=47 | 64..=76 => assert_eq!(result, Ok(()), "bit {bit}"),
577                2..=4 | 84 => assert_eq!(
578                    result,
579                    Err(ScalableValidationError::UnsupportedFields {
580                        word: bit / 64,
581                        bits: 1 << (bit % 64),
582                    })
583                ),
584                77..=83 => assert!(matches!(
585                    result,
586                    Err(ScalableValidationError::PasidOutOfRange { .. })
587                )),
588                _ => assert_eq!(
589                    result,
590                    Err(ScalableValidationError::ReservedBits {
591                        word: bit / 64,
592                        bits: 1 << (bit % 64),
593                    })
594                ),
595            }
596        }
597    }
598
599    #[test]
600    fn test_pdts_and_pasid_boundaries() {
601        for pdts in 0..=7 {
602            let entries = 128 << pdts;
603            let limit = 8192 << pdts;
604            assert_eq!(pasid_directory_entries(pdts), Ok(entries));
605            let mut context: ScalableContextEntry = decode(&[1 | (u64::from(pdts) << 9), 0, 0, 0]);
606            assert_eq!(context.pasid_indices(0), Ok((0, 0)));
607            assert_eq!(context.pasid_indices(63), Ok((0, 63)));
608            assert_eq!(context.pasid_indices(64), Ok((1, 0)));
609            assert_eq!(
610                context.pasid_indices(limit - 1),
611                Ok(((entries - 1) as u16, 63))
612            );
613            for pasid in [limit, 1 << 20, u32::MAX] {
614                assert_eq!(
615                    context.pasid_indices(pasid),
616                    Err(ScalableValidationError::PasidOutOfRange { pasid, limit })
617                );
618            }
619            context.hi.set_rid_pasid(limit - 1);
620            assert_eq!(context.validate(48), Ok(()));
621            if pdts < 7 {
622                context.hi.set_rid_pasid(limit);
623                assert!(matches!(
624                    context.validate(48),
625                    Err(ScalableValidationError::PasidOutOfRange { .. })
626                ));
627            }
628        }
629        for pdts in 8..=u8::MAX {
630            assert_eq!(
631                pasid_directory_entries(pdts),
632                Err(ScalableValidationError::InvalidDirectorySize(pdts))
633            );
634        }
635    }
636
637    #[test]
638    fn test_directory_literal_bytes_and_validation() {
639        let entry: PasidDirectoryEntry = decode(&[0x9876_5432_1003]);
640        assert!(entry.p());
641        assert!(entry.fpd());
642        assert_eq!(entry.table_address(), 0x9876_5432_1000);
643        assert_eq!(entry.validate(48), Ok(()));
644        assert_eq!(
645            PasidDirectoryEntry::new()
646                .with_p(true)
647                .with_fpd(true)
648                .with_smptblptr(0x9_8765_4321)
649                .as_bytes(),
650            &[3, 0x10, 0x32, 0x54, 0x76, 0x98, 0, 0]
651        );
652        for bit in 1..64 {
653            let entry = PasidDirectoryEntry::from(1 | (1 << bit));
654            if (2..=11).contains(&bit) || bit >= 48 {
655                assert_eq!(
656                    entry.validate(48),
657                    Err(ScalableValidationError::ReservedBits {
658                        word: 0,
659                        bits: 1 << bit,
660                    })
661                );
662            } else {
663                assert_eq!(entry.validate(48), Ok(()));
664            }
665        }
666    }
667
668    #[test]
669    fn test_pasid_table_literal_words_and_bytes() {
670        let words = [0x9876_5432_128b, 0x0180_abcd, 0x60, 0, 0, 0, 0, 0];
671        let entry: PasidTableEntry = decode(&words);
672        assert!(entry.lo.p());
673        assert!(entry.lo.fpd());
674        assert_eq!(entry.lo.aw(), 2);
675        assert_eq!(entry.lo.pgtt(), 2);
676        assert!(entry.lo.ssade());
677        assert_eq!(entry.page_table_address(), 0x9876_5432_1000);
678        assert_eq!(entry.hi.did(), 0xabcd);
679        assert!(entry.hi.pwsnp());
680        assert!(entry.hi.pgsnp());
681        assert_eq!(entry.validate(48), Ok(()));
682        let built = PasidTableEntry {
683            lo: PasidTableEntryLo::new()
684                .with_p(true)
685                .with_fpd(true)
686                .with_aw(2)
687                .with_pgtt(2)
688                .with_ssade(true)
689                .with_ssptptr(0x9_8765_4321),
690            hi: PasidTableEntryHi::new()
691                .with_did(0xabcd)
692                .with_pwsnp(true)
693                .with_pgsnp(true),
694            first_stage: PasidTableEntryFirstStage::from(0x60),
695            reserved: [0; 5],
696        };
697        let expected: Vec<_> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
698        assert_eq!(built.as_bytes(), expected);
699        assert_eq!(
700            PasidTableEntryLo::new().with_ssade(true).into_bits(),
701            1 << 9
702        );
703        assert_eq!(
704            PasidTableEntryHi::new().with_pwsnp(true).into_bits(),
705            1 << 23
706        );
707        assert_eq!(
708            PasidTableEntryHi::new().with_pgsnp(true).into_bits(),
709            1 << 24
710        );
711    }
712
713    #[test]
714    fn test_pasid_table_reserved_and_unsupported_bits() {
715        for bit in 0..512 {
716            let word = bit / 64;
717            let shift = bit % 64;
718            let is_reserved = match word {
719                0 => matches!(shift, 5 | 10..=11 | 48..=63),
720                1 => matches!(shift, 16..=22 | 27..=31),
721                2 => matches!(shift, 1 | 8..=11 | 48..=63),
722                _ => true,
723            };
724            let is_unsupported = match word {
725                1 => matches!(shift, 25..=26 | 32..=63),
726                2 => matches!(shift, 0 | 2..=4 | 7 | 12..=47),
727                _ => false,
728            };
729            if is_reserved || is_unsupported {
730                // Repeat for second-stage and pass-through: unsupported optional
731                // fields are reserved-zero even when the translation ignores them.
732                for pgtt in [2, 4] {
733                    let mut words = [0; 8];
734                    words[0] = 1 | (1 << 2) | (pgtt << 6);
735                    words[word] |= 1 << shift;
736                    let entry: PasidTableEntry = decode(&words);
737                    let error = if is_reserved {
738                        ScalableValidationError::ReservedBits {
739                            word,
740                            bits: 1 << shift,
741                        }
742                    } else {
743                        ScalableValidationError::UnsupportedFields {
744                            word,
745                            bits: 1 << shift,
746                        }
747                    };
748                    assert_eq!(entry.validate(48), Err(error), "bit {bit}, PGTT {pgtt}");
749                }
750            }
751        }
752    }
753
754    #[test]
755    fn test_pasid_ignored_fields_and_snoop_combinations() {
756        for ignored in [0, 1 << 5, 1 << 6, 3 << 5] {
757            for pgtt in [2, 4] {
758                for ssade in [0, 1] {
759                    for pwsnp in [0, 1] {
760                        // All 16 DID bits, including DID=0, are legal with CM=0.
761                        for did in [0, 0xffff] {
762                            let entry: PasidTableEntry = decode(&[
763                                1 | (1 << 2) | (pgtt << 6) | (ssade << 9),
764                                did | (pwsnp << 23) | (1 << 24),
765                                ignored,
766                                0,
767                                0,
768                                0,
769                                0,
770                                0,
771                            ]);
772                            assert_eq!(entry.validate(48), Ok(()));
773                        }
774                    }
775                }
776            }
777        }
778        // AW, SSADE and in-range SSPTPTR are ignored for pass-through.
779        let passthrough: PasidTableEntry = decode(&[0x0000_ffff_ffff_f31f, 0, 0x60, 0, 0, 0, 0, 0]);
780        assert_eq!(passthrough.lo.pgtt(), 4);
781        assert_eq!(passthrough.validate(48), Ok(()));
782    }
783
784    #[test]
785    fn test_pgtt_and_aw_encodings() {
786        assert_eq!(PasidTranslationType::FIRST_STAGE.0, 1);
787        assert_eq!(PasidTranslationType::SECOND_STAGE.0, 2);
788        assert_eq!(PasidTranslationType::NESTED.0, 3);
789        assert_eq!(PasidTranslationType::PASS_THROUGH.0, 4);
790        for pgtt in 0..=7u8 {
791            for aw in 0..=7u8 {
792                let entry: PasidTableEntry = decode(&[
793                    1 | (u64::from(aw) << 2) | (u64::from(pgtt) << 6),
794                    0,
795                    0,
796                    0,
797                    0,
798                    0,
799                    0,
800                    0,
801                ]);
802                assert_eq!(PasidTranslationType(entry.lo.pgtt()).0, pgtt);
803                let expected = match pgtt {
804                    2 if aw == 1 || aw == 2 => Ok(()),
805                    2 => Err(ScalableValidationError::UnsupportedAddressWidth(
806                        AddressWidth(aw),
807                    )),
808                    4 => Ok(()),
809                    _ => Err(ScalableValidationError::UnsupportedTranslationType(
810                        PasidTranslationType(pgtt),
811                    )),
812                };
813                assert_eq!(entry.validate(48), expected);
814            }
815        }
816    }
817
818    #[test]
819    fn test_absent_entries_ignore_all_but_fpd() {
820        for fpd in [false, true] {
821            let lo = !3u64 | ((fpd as u64) << 1);
822            let context: ScalableContextEntry = decode(&[lo, u64::MAX, u64::MAX, u64::MAX]);
823            let directory = PasidDirectoryEntry::from(lo);
824            let mut words = [u64::MAX; 8];
825            words[0] = lo;
826            let table: PasidTableEntry = decode(&words);
827            assert_eq!(
828                context.validate(48),
829                Err(ScalableValidationError::NotPresent)
830            );
831            assert_eq!(
832                directory.validate(48),
833                Err(ScalableValidationError::NotPresent)
834            );
835            assert_eq!(table.validate(48), Err(ScalableValidationError::NotPresent));
836            assert_eq!(context.lo.fpd(), fpd);
837            assert_eq!(directory.fpd(), fpd);
838            assert_eq!(table.lo.fpd(), fpd);
839        }
840    }
841
842    #[test]
843    fn test_host_address_width_boundaries() {
844        for haw in 0..=u8::MAX {
845            let root = ScalableRootEntryHalf::from(1);
846            let context: ScalableContextEntry = decode(&[1, 0, 0, 0]);
847            let directory = PasidDirectoryEntry::from(1);
848            let table: PasidTableEntry = decode(&[0x85, 0, 0, 0, 0, 0, 0, 0]);
849            let expected = if (12..=64).contains(&haw) {
850                Ok(())
851            } else {
852                Err(ScalableValidationError::InvalidHostAddressWidth(haw))
853            };
854            assert_eq!(root.validate(haw), expected);
855            assert_eq!(context.validate(haw), expected);
856            assert_eq!(directory.validate(haw), expected);
857            assert_eq!(table.validate(haw), expected);
858        }
859        for haw in [39, 48, 52, 63] {
860            let last_bit = 1u64 << (haw - 1);
861            let first_reserved = 1u64 << haw;
862            let valid = ScalableRootEntryHalf::from(1 | last_bit);
863            assert_eq!(valid.validate(haw), Ok(()));
864            let invalid = ScalableRootEntryHalf::from(1 | first_reserved);
865            assert_eq!(
866                invalid.validate(haw),
867                Err(ScalableValidationError::ReservedBits {
868                    word: 0,
869                    bits: first_reserved,
870                })
871            );
872        }
873        assert_eq!(
874            ScalableRootEntryHalf::from(1 | (1 << 63)).validate(64),
875            Ok(())
876        );
877    }
878}