Skip to main content

loader_defs/
paravisor.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Underhill (paravisor) definitions.
5
6use bitfield_struct::bitfield;
7use core::mem::size_of;
8use hvdef::HV_PAGE_SIZE;
9#[cfg(feature = "inspect")]
10use inspect::Inspect;
11use open_enum::open_enum;
12use static_assertions::const_assert_eq;
13use zerocopy::FromBytes;
14use zerocopy::Immutable;
15use zerocopy::IntoBytes;
16use zerocopy::KnownLayout;
17
18// Number of pages for each type of parameter in the vtl 2 unmeasured config
19// region.
20/// Size in pages for the SLIT.
21pub const PARAVISOR_CONFIG_SLIT_SIZE_PAGES: u64 = 20;
22/// Size in pages for the PPTT.
23pub const PARAVISOR_CONFIG_PPTT_SIZE_PAGES: u64 = 20;
24/// Size in pages for the device tree.
25pub const PARAVISOR_CONFIG_DEVICE_TREE_SIZE_PAGES: u64 = 64;
26
27/// The maximum size in pages of the unmeasured vtl 2 config region.
28pub const PARAVISOR_UNMEASURED_VTL2_CONFIG_REGION_PAGE_COUNT_MAX: u64 =
29    PARAVISOR_CONFIG_SLIT_SIZE_PAGES
30        + PARAVISOR_CONFIG_PPTT_SIZE_PAGES
31        + PARAVISOR_CONFIG_DEVICE_TREE_SIZE_PAGES;
32
33// Page indices for different parameters within the unmeasured vtl 2 config region.
34/// The page index to the SLIT.
35pub const PARAVISOR_CONFIG_SLIT_PAGE_INDEX: u64 = 0;
36/// The page index to the PPTT.
37pub const PARAVISOR_CONFIG_PPTT_PAGE_INDEX: u64 =
38    PARAVISOR_CONFIG_SLIT_PAGE_INDEX + PARAVISOR_CONFIG_SLIT_SIZE_PAGES;
39/// The page index to the device tree.
40pub const PARAVISOR_CONFIG_DEVICE_TREE_PAGE_INDEX: u64 =
41    PARAVISOR_CONFIG_PPTT_PAGE_INDEX + PARAVISOR_CONFIG_PPTT_SIZE_PAGES;
42/// Base index for the unmeasured vtl 2 config region
43pub const PARAVISOR_UNMEASURED_VTL2_CONFIG_REGION_BASE_INDEX: u64 =
44    PARAVISOR_CONFIG_SLIT_PAGE_INDEX;
45
46/// Size in pages for the SNP CPUID pages.
47pub const PARAVISOR_RESERVED_VTL2_SNP_CPUID_SIZE_PAGES: u64 = 2;
48/// Size in pages for the VMSA page.
49pub const PARAVISOR_RESERVED_VTL2_SNP_VMSA_SIZE_PAGES: u64 = 1;
50/// Size in pages for the secrets page.
51pub const PARAVISOR_RESERVED_VTL2_SNP_SECRETS_SIZE_PAGES: u64 = 1;
52
53/// Total size of the reserved vtl2 range.
54pub const PARAVISOR_RESERVED_VTL2_PAGE_COUNT_MAX: u64 = PARAVISOR_RESERVED_VTL2_SNP_CPUID_SIZE_PAGES
55    + PARAVISOR_RESERVED_VTL2_SNP_VMSA_SIZE_PAGES
56    + PARAVISOR_RESERVED_VTL2_SNP_SECRETS_SIZE_PAGES;
57
58// Page indices for reserved vtl2 ranges, ranges that are marked as reserved to
59// both the kernel and usermode. Today, these are SNP specific pages.
60//
61// TODO SNP: Does the kernel require that the CPUID and secrets pages are
62// persisted, or after the kernel boots, and usermode reads them, can we discard
63// them?
64//
65/// The page index to the SNP VMSA page.
66pub const PARAVISOR_RESERVED_VTL2_SNP_VMSA_PAGE_INDEX: u64 = 0;
67/// The page index to the first SNP CPUID page.
68pub const PARAVISOR_RESERVED_VTL2_SNP_CPUID_PAGE_INDEX: u64 =
69    PARAVISOR_RESERVED_VTL2_SNP_VMSA_PAGE_INDEX + PARAVISOR_RESERVED_VTL2_SNP_VMSA_SIZE_PAGES;
70/// The page index to the first SNP secrets page.
71pub const PARAVISOR_RESERVED_VTL2_SNP_SECRETS_PAGE_INDEX: u64 =
72    PARAVISOR_RESERVED_VTL2_SNP_CPUID_PAGE_INDEX + PARAVISOR_RESERVED_VTL2_SNP_CPUID_SIZE_PAGES;
73
74// Number of pages for each type of parameter in the vtl 2 measured config
75// region.
76/// Size in pages the list of accepted memory
77pub const PARAVISOR_MEASURED_VTL2_CONFIG_ACCEPTED_MEMORY_SIZE_PAGES: u64 = 1;
78
79/// Size in pages of VTL2 specific measured config
80pub const PARAVISOR_MEASURED_VTL2_CONFIG_SIZE_PAGES: u64 = 1;
81
82/// Size in pages for the per-page expected-hashes region. Holds one
83/// SHA-384 (48 bytes) per unmeasured (shared) 4 KB page in the IGVM, so the
84/// boot shim can identify which individual pages diverged from the
85/// measured baseline rather than only knowing "the combined hash was
86/// wrong". Sized generously: 256 pages = 1 MB / 48 B per hash = 21_845
87/// hashes max, comfortably above the ~16 K unmeasured pages on debug SNP
88/// images. Placed after PARAVISOR_MEASURED_VTL2_CONFIG_SIZE_PAGES so that
89/// PARAVISOR_MEASURED_VTL2_CONFIG_PAGE_INDEX stays where existing
90/// consumers expect it and only new readers need to know about the new
91/// region.
92pub const PARAVISOR_MEASURED_VTL2_CONFIG_PAGE_HASHES_SIZE_PAGES: u64 = 256;
93
94/// Count for vtl 2 measured config region size.
95pub const PARAVISOR_MEASURED_VTL2_CONFIG_REGION_PAGE_COUNT: u64 =
96    PARAVISOR_MEASURED_VTL2_CONFIG_ACCEPTED_MEMORY_SIZE_PAGES
97        + PARAVISOR_MEASURED_VTL2_CONFIG_SIZE_PAGES
98        + PARAVISOR_MEASURED_VTL2_CONFIG_PAGE_HASHES_SIZE_PAGES;
99
100// Measured config comes after the unmeasured config
101/// The page index to the list of accepted pages
102pub const PARAVISOR_MEASURED_VTL2_CONFIG_ACCEPTED_MEMORY_PAGE_INDEX: u64 =
103    PARAVISOR_UNMEASURED_VTL2_CONFIG_REGION_BASE_INDEX
104        + PARAVISOR_UNMEASURED_VTL2_CONFIG_REGION_PAGE_COUNT_MAX;
105
106/// The page index for measured VTL2 config.
107pub const PARAVISOR_MEASURED_VTL2_CONFIG_PAGE_INDEX: u64 =
108    PARAVISOR_MEASURED_VTL2_CONFIG_ACCEPTED_MEMORY_PAGE_INDEX
109        + PARAVISOR_MEASURED_VTL2_CONFIG_ACCEPTED_MEMORY_SIZE_PAGES;
110
111/// The page index for the per-page expected-hashes region.
112///
113/// Deliberately placed AFTER `PARAVISOR_MEASURED_VTL2_CONFIG_PAGE_INDEX`
114/// (rather than between it and the accepted-memory page): this keeps
115/// `PARAVISOR_MEASURED_VTL2_CONFIG_PAGE_INDEX` at exactly the same offset
116/// as before this region was added, so any consumer that only reads the
117/// existing measured config page continues to work unchanged. Only
118/// consumers that opt in to reading per-page expected hashes need to
119/// know this index exists.
120pub const PARAVISOR_MEASURED_VTL2_CONFIG_PAGE_HASHES_PAGE_INDEX: u64 =
121    PARAVISOR_MEASURED_VTL2_CONFIG_PAGE_INDEX + PARAVISOR_MEASURED_VTL2_CONFIG_SIZE_PAGES;
122
123/// The maximum size in pages out of all isolation architectures.
124pub const PARAVISOR_VTL2_CONFIG_REGION_PAGE_COUNT_MAX: u64 =
125    PARAVISOR_UNMEASURED_VTL2_CONFIG_REGION_PAGE_COUNT_MAX
126        + PARAVISOR_MEASURED_VTL2_CONFIG_REGION_PAGE_COUNT; // TODO: const fn max or macro possible?
127
128// Default memory information.
129/// The default base address for the paravisor, 128MB.
130pub const PARAVISOR_DEFAULT_MEMORY_BASE_ADDRESS: u64 = 128 * 1024 * 1024;
131/// The default page count for the memory size for the paravisor, 64MB.
132pub const PARAVISOR_DEFAULT_MEMORY_PAGE_COUNT: u64 = 64 * 1024 * 1024 / HV_PAGE_SIZE;
133/// The base VA for the local map, if present.
134pub const PARAVISOR_LOCAL_MAP_VA: u64 = 0x200000;
135/// The base size in bytes for the local map, if present.
136pub const PARAVISOR_LOCAL_MAP_SIZE: u64 = 0x200000;
137
138open_enum! {
139    /// Underhill command line policy.
140    #[derive(IntoBytes, Immutable, KnownLayout, FromBytes)]
141    pub enum CommandLinePolicy : u16 {
142        /// Use the static command line encoded only.
143        STATIC = 0,
144        /// Append the host provided value in the device tree /chosen node to
145        /// the static command line.
146        APPEND_CHOSEN = 1,
147    }
148}
149
150/// Maximum static command line size.
151pub const COMMAND_LINE_SIZE: usize = 4092;
152
153/// Command line information. This structure is an exclusive measured page.
154#[repr(C)]
155#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)]
156pub struct ParavisorCommandLine {
157    /// The policy Underhill should use.
158    pub policy: CommandLinePolicy,
159    /// The length of the command line.
160    pub static_command_line_len: u16,
161    /// The static command line. This is a valid utf8 string of length described
162    /// by the field above. This field should normally not be used, instead the
163    /// corresponding [`Self::command_line`] function should be used that
164    /// returns a [`&str`].
165    pub static_command_line: [u8; COMMAND_LINE_SIZE],
166}
167
168impl ParavisorCommandLine {
169    /// Read the static command line as a [`&str`]. Returns None if the bytes
170    /// are not a valid [`&str`].
171    pub fn command_line(&self) -> Option<&str> {
172        core::str::from_utf8(&self.static_command_line[..self.static_command_line_len as usize])
173            .ok()
174    }
175}
176
177const_assert_eq!(size_of::<ParavisorCommandLine>(), HV_PAGE_SIZE as usize);
178
179/// Describes a region of guest memory.
180#[repr(C)]
181#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes, PartialEq)]
182pub struct PageRegionDescriptor {
183    /// Guest physical page number for the base of this region.
184    pub base_page_number: u64,
185    /// Number of pages in this region. 0 means this region is not valid.
186    pub page_count: u64,
187}
188
189#[cfg(feature = "inspect")]
190impl Inspect for PageRegionDescriptor {
191    fn inspect(&self, req: inspect::Request<'_>) {
192        let pages = self.pages();
193
194        match pages {
195            None => {
196                req.ignore();
197            }
198            Some((base, count)) => {
199                req.respond()
200                    .field("base_page_number", base)
201                    .field("page_count", count);
202            }
203        }
204    }
205}
206
207impl PageRegionDescriptor {
208    /// An empty region.
209    pub const EMPTY: Self = PageRegionDescriptor {
210        base_page_number: 0,
211        page_count: 0,
212    };
213
214    /// Create a new page region descriptor with the given base page and page count.
215    pub fn new(base_page_number: u64, page_count: u64) -> Self {
216        PageRegionDescriptor {
217            base_page_number,
218            page_count,
219        }
220    }
221
222    /// Returns `Some((base page number, page count))` described by the descriptor, if valid.
223    pub fn pages(&self) -> Option<(u64, u64)> {
224        if self.page_count != 0 {
225            Some((self.base_page_number, self.page_count))
226        } else {
227            None
228        }
229    }
230}
231
232/// The header field of the imported pages region page.
233#[repr(C)]
234#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes, PartialEq)]
235pub struct ImportedRegionsPageHeader {
236    /// The cryptographic hash of the unaccepted pages.
237    pub sha384_hash: [u8; 48],
238}
239
240/// Describes a region of guest memory that has been imported into VTL2.
241#[repr(C)]
242#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes, PartialEq)]
243pub struct ImportedRegionDescriptor {
244    /// Guest physical page number for the base of this region.
245    pub base_page_number: u64,
246    /// Number of pages in this region. 0 means this region is not valid.
247    pub page_count: u64,
248    /// Whether the pages in this region were accepted during the import process.
249    pub accepted: u8,
250    /// Padding
251    padding: [u8; 7],
252}
253
254#[cfg(feature = "inspect")]
255impl Inspect for ImportedRegionDescriptor {
256    fn inspect(&self, req: inspect::Request<'_>) {
257        let pages = self.pages();
258
259        match pages {
260            None => {
261                req.ignore();
262            }
263            Some((base, count, accepted)) => {
264                req.respond()
265                    .field("base_page_number", base)
266                    .field("page_count", count)
267                    .field("accepted", accepted);
268            }
269        }
270    }
271}
272
273impl ImportedRegionDescriptor {
274    /// An empty region.
275    pub const EMPTY: Self = ImportedRegionDescriptor {
276        base_page_number: 0,
277        page_count: 0,
278        accepted: false as u8,
279        padding: [0; 7],
280    };
281
282    /// Create a new page region descriptor with the given base page and page count.
283    pub fn new(base_page_number: u64, page_count: u64, accepted: bool) -> Self {
284        ImportedRegionDescriptor {
285            base_page_number,
286            page_count,
287            accepted: accepted as u8,
288            padding: [0; 7],
289        }
290    }
291
292    /// Returns `Some((base page number, page count, accepted))` described by the descriptor, if valid.
293    pub fn pages(&self) -> Option<(u64, u64, bool)> {
294        if self.page_count != 0 {
295            Some((self.base_page_number, self.page_count, self.accepted != 0))
296        } else {
297            None
298        }
299    }
300}
301
302/// Magic identifier at the start of the per-page expected-hashes region.
303/// Chosen so an older tool inspecting the region can recognise it. Bytes
304/// spell `EPHS` in ASCII, little-endian encoded.
305pub const EXPECTED_PAGE_HASHES_MAGIC: u32 = u32::from_le_bytes(*b"EPHS");
306
307/// Current layout version of `ExpectedPageHashesHeader` + its trailing
308/// array. Bumped only on incompatible changes.
309pub const EXPECTED_PAGE_HASHES_VERSION: u32 = 1;
310
311/// Header at page 0 of the measured per-page expected-hashes region
312/// (`PARAVISOR_MEASURED_VTL2_CONFIG_PAGE_HASHES_PAGE_INDEX`). Followed
313/// immediately by an array of `ExpectedPageHash` entries. Contents are
314/// zero-padded to fill the region.
315///
316/// Entry ordering: matches the shim's walk of
317/// `imported_regions().filter(!already_accepted)`, one entry per 4 KB
318/// page in ascending GPA order.
319#[repr(C)]
320#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes, PartialEq)]
321pub struct ExpectedPageHashesHeader {
322    /// `EXPECTED_PAGE_HASHES_MAGIC`.
323    pub magic: u32,
324    /// `EXPECTED_PAGE_HASHES_VERSION`.
325    pub version: u32,
326    /// Number of `ExpectedPageHash` entries that follow.
327    pub page_hash_count: u32,
328    /// Reserved, must be zero.
329    pub reserved: u32,
330}
331
332/// SHA-384 of one 4 KB unmeasured (shared) IGVM page, zero-extended to 4
333/// KB before hashing (matching the combined hash in
334/// `ImportedRegionsPageHeader::sha384_hash`).
335#[repr(C)]
336#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes, PartialEq)]
337pub struct ExpectedPageHash {
338    /// SHA-384 of the page contents.
339    pub sha384_hash: [u8; 48],
340}
341
342/// Maximum number of `ExpectedPageHash` entries the region can hold,
343/// given `PARAVISOR_MEASURED_VTL2_CONFIG_PAGE_HASHES_SIZE_PAGES` pages
344/// minus the header.
345pub const EXPECTED_PAGE_HASH_MAX_COUNT: usize =
346    (PARAVISOR_MEASURED_VTL2_CONFIG_PAGE_HASHES_SIZE_PAGES as usize * HV_PAGE_SIZE as usize
347        - size_of::<ExpectedPageHashesHeader>())
348        / size_of::<ExpectedPageHash>();
349
350/// Measured config about linux loaded into VTL0.
351#[repr(C)]
352#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)]
353#[cfg_attr(feature = "inspect", derive(Inspect))]
354pub struct LinuxInfo {
355    /// The memory the kernel was loaded into.
356    pub kernel_region: PageRegionDescriptor,
357    /// The gpa entrypoint of the kernel.
358    pub kernel_entrypoint: u64,
359    /// The memory region the initrd was loaded into.
360    pub initrd_region: PageRegionDescriptor,
361    /// The size of the initrd in bytes.
362    pub initrd_size: u64,
363    /// An ASCII command line to use for the kernel.
364    pub command_line: PageRegionDescriptor,
365}
366
367/// Measured config about UEFI loaded into VTL0.
368#[repr(C)]
369#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)]
370#[cfg_attr(feature = "inspect", derive(Inspect))]
371pub struct UefiInfo {
372    /// The information about where UEFI's firmware and misc pages are.
373    pub firmware: PageRegionDescriptor,
374    /// The location of VTL0's VP context data.
375    pub vtl0_vp_context: PageRegionDescriptor,
376}
377
378/// Measured config about what this image can support loading in VTL0.
379#[cfg_attr(feature = "inspect", derive(Inspect))]
380#[bitfield(u64)]
381#[derive(IntoBytes, Immutable, KnownLayout, FromBytes)]
382pub struct SupportedVtl0LoadInfo {
383    /// This image supports UEFI.
384    #[bits(1)]
385    pub uefi_supported: bool,
386    /// This image supports PCAT.
387    #[bits(1)]
388    pub pcat_supported: bool,
389    /// This image supports Linux Direct.
390    #[bits(1)]
391    pub linux_direct_supported: bool,
392    /// Currently reserved.
393    #[bits(61)]
394    pub reserved: u64,
395}
396
397/// Paravisor measured config information for vtl 0. Unlike the previous loader
398/// block which contains dynamic parameter info written by the host, this config
399/// information is known at file build time, measured, and deposited as part of
400/// the initial launch data.
401#[repr(C)]
402#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)]
403#[cfg_attr(feature = "inspect", derive(Inspect))]
404pub struct ParavisorMeasuredVtl0Config {
405    /// Magic value. Must be [`Self::MAGIC`].
406    pub magic: u64,
407    /// Supported VTL0 images.
408    pub supported_vtl0: SupportedVtl0LoadInfo,
409    /// If UEFI is supported, information about UEFI for VTL0.
410    pub uefi_info: UefiInfo,
411    /// If Linux is supported, information about Linux for VTL0.
412    pub linux_info: LinuxInfo,
413}
414
415impl ParavisorMeasuredVtl0Config {
416    /// Magic value for the measured config, which is "OHCLVTL0".
417    pub const MAGIC: u64 = 0x4F48434C56544C30;
418}
419
420/// The physical page number for where the vtl 0 measured config is stored, x86_64.
421/// This address is guaranteed to exist in the guest address space as it is
422/// where the ISR table is located at reset.
423pub const PARAVISOR_VTL0_MEASURED_CONFIG_BASE_PAGE_X64: u64 = 0;
424
425/// The physical page number for where the vtl 0 measured config is stored, aarch64.
426/// Not obvious about guaranteed existence. 16MiB might be a reasonable assumption as:
427/// * UEFI uses the GPA range of [0; 0x800000), after that there are page tables,
428///   stack, and the config blob at GPA 0x824000,
429/// * Gen 2 VMs don't work with less than 32MiB,
430/// * the loaders have checks for overlap.
431pub const PARAVISOR_VTL0_MEASURED_CONFIG_BASE_PAGE_AARCH64: u64 = 16 << (20 - 12);
432
433/// Paravisor measured config for vtl2.
434///
435/// Followed in place by the optional `ProductPolicy` body at
436/// [`PRODUCT_POLICY_INLINE_OFFSET`]; `product_policy_size == 0` (the
437/// pre-feature zero-filled tail) means absent.
438#[repr(C)]
439#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)]
440#[cfg_attr(feature = "inspect", derive(Inspect))]
441pub struct ParavisorMeasuredVtl2Config {
442    /// Magic value. Must be [`Self::MAGIC`].
443    pub magic: u64,
444    /// The bit offset of vTOM, if non-zero.
445    pub vtom_offset_bit: u8,
446    /// Padding.
447    pub padding: [u8; 7],
448    /// Byte length of the inline `ProductPolicy` body, or `0` if
449    /// absent.
450    pub product_policy_size: u32,
451    /// Reserved; must be zero.
452    pub reserved: [u8; 4],
453}
454
455impl ParavisorMeasuredVtl2Config {
456    /// Magic value for the measured config, which is "OHCLVTL2".
457    pub const MAGIC: u64 = 0x4F48434C56544C32;
458}
459
460/// Byte offset of the inline `ProductPolicy` body within the
461/// measured VTL2 config region.
462pub const PRODUCT_POLICY_INLINE_OFFSET: usize = size_of::<ParavisorMeasuredVtl2Config>();
463
464/// Maximum byte size of an inline `ProductPolicy` body.
465pub const PRODUCT_POLICY_MAX_SIZE_BYTES: usize =
466    (PARAVISOR_MEASURED_VTL2_CONFIG_SIZE_PAGES as usize) * (HV_PAGE_SIZE as usize)
467        - PRODUCT_POLICY_INLINE_OFFSET;
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472    // ---------------------------------------------------------------
473    // ParavisorMeasuredVtl2Config: struct layout
474    // ---------------------------------------------------------------
475
476    #[test]
477    fn measured_vtl2_config_field_offsets() {
478        let cfg = ParavisorMeasuredVtl2Config {
479            magic: 0x1122_3344_5566_7788,
480            vtom_offset_bit: 0x99,
481            padding: [0; 7],
482            product_policy_size: 0xABCDu32,
483            reserved: [0; 4],
484        };
485        let bytes = cfg.as_bytes();
486        assert_eq!(&bytes[0..8], &0x1122_3344_5566_7788u64.to_le_bytes());
487        assert_eq!(bytes[8], 0x99);
488        assert_eq!(&bytes[9..16], &[0u8; 7]);
489        assert_eq!(&bytes[16..20], &0xABCDu32.to_le_bytes());
490        assert_eq!(&bytes[20..24], &[0u8; 4]);
491        assert_eq!(bytes.len(), 24);
492    }
493
494    #[test]
495    fn measured_vtl2_config_round_trips() {
496        let cfg = ParavisorMeasuredVtl2Config {
497            magic: ParavisorMeasuredVtl2Config::MAGIC,
498            vtom_offset_bit: 47,
499            padding: [0; 7],
500            product_policy_size: 256,
501            reserved: [0; 4],
502        };
503        let bytes = cfg.as_bytes().to_vec();
504        let (decoded, rest) = ParavisorMeasuredVtl2Config::ref_from_prefix(&bytes).unwrap();
505        assert!(rest.is_empty());
506        assert_eq!(decoded.magic, ParavisorMeasuredVtl2Config::MAGIC);
507        assert_eq!(decoded.vtom_offset_bit, 47);
508        assert_eq!(decoded.product_policy_size, 256);
509    }
510
511    #[test]
512    fn pre_feature_zeroed_page_decodes_as_absent() {
513        // Pre-feature builders wrote only the 16-byte head; the trailing
514        // zeros must decode as `product_policy_size == 0`.
515        let mut page = [0u8; HV_PAGE_SIZE as usize];
516        page[0..8].copy_from_slice(&ParavisorMeasuredVtl2Config::MAGIC.to_le_bytes());
517        page[8] = 17;
518        let (decoded, _rest) = ParavisorMeasuredVtl2Config::ref_from_prefix(&page).unwrap();
519        assert_eq!(decoded.magic, ParavisorMeasuredVtl2Config::MAGIC);
520        assert_eq!(decoded.vtom_offset_bit, 17);
521        assert_eq!(decoded.product_policy_size, 0);
522    }
523}