Skip to main content

openhcl_boot/arch/x86_64/
memory.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Routines to prepare VTL2 memory for launching the kernel.
5
6use super::address_space::LocalMap;
7use super::address_space::init_local_map;
8use crate::AddressSpaceManager;
9use crate::ShimParams;
10use crate::arch::TdxHypercallPage;
11use crate::arch::x86_64::address_space::tdx_share_large_page;
12use crate::host_params::PartitionInfo;
13use crate::host_params::shim_params::IsolationType;
14use crate::hypercall::hvcall;
15use crate::memory::AllocationPolicy;
16use crate::memory::AllocationType;
17use crate::off_stack;
18use crate::single_threaded::SingleThreaded;
19use arrayvec::ArrayVec;
20use core::cell::RefCell;
21use loader_defs::shim::MemoryVtlType;
22use memory_range::MemoryRange;
23use page_table::x64::MappedRange;
24use page_table::x64::PAGE_TABLE_MAX_BYTES;
25use page_table::x64::PAGE_TABLE_MAX_COUNT;
26use page_table::x64::PageTable;
27use page_table::x64::PageTableBuilder;
28use sha2::Digest;
29use sha2::Sha384;
30use static_assertions::const_assert;
31use x86defs::X64_LARGE_PAGE_SIZE;
32use x86defs::tdx::TDX_SHARED_GPA_BOUNDARY_ADDRESS_BIT;
33use zerocopy::FromZeros;
34
35// ============================================================================
36// Diagnostic instrumentation for the "Imported regions hash mismatch" panic.
37// ============================================================================
38//
39// This is a temporary debugging aid intended for local investigation of a rare
40// mismatch seen on SNP boots. It computes SHA-384 hashes at three points to
41// isolate which phase corruption occurs in:
42//   Preflight (SNP only): compare all shared imported pages with the measured
43//            hashes and report corruption before converting any imported page.
44//   Phase A: bytes captured out of the shared (host-visible) page into
45//            `ram_buffer` just before the shared -> private transition.
46//   Phase B: bytes as read from the same GPA immediately after the
47//            transition (via the C=1 identity map), i.e. after accept +
48//            copy-back.
49//   Phase C: bytes as read from the same GPA at final verify time.
50//
51// Granularity:
52//   - Phase A capture: 2 MB accept-chunk (records one SHA-384 per chunk) plus
53//     a running combined hash for compare against `imported_regions_hash()`.
54//   - Phase A -> B compare: 4 KB PAGE granularity (per Jon's feedback). On
55//     mismatch we emit a bitmap of corrupt pages, RLE ranges, per-page pre/
56//     post SHA-384 (capped), and a full 4 KB hex dump of the first bad page.
57//   - Phase A -> C compare: per-chunk (interim). Once the loader is updated
58//     to emit per-page expected hashes, Phase C can also do per-page.
59//
60// Full-page dumps are strictly one-shot per soak (see DIAG_FULL_PAGE_DUMPED)
61// -- one sample is enough to eyeball whether corruption is a bit flip, a
62// zeroed page, a substituted page, etc., and we don't want to spam COM3 with
63// 64 lines per mismatched chunk.
64//
65// Not intended for check-in.
66
67/// Diagnostic page size == HV page size (4 KB).
68const DIAG_PAGE_SIZE: usize = hvdef::HV_PAGE_SIZE as usize;
69
70/// Max pages we can bitmap in a single 2 MB accept chunk (2 MB / 4 KB = 512).
71const DIAG_MAX_PAGES_PER_CHUNK: usize = X64_LARGE_PAGE_SIZE as usize / DIAG_PAGE_SIZE;
72const _: () = assert!(DIAG_MAX_PAGES_PER_CHUNK <= 512);
73
74/// Bitmap word count (u64s) for one 2 MB chunk.
75const DIAG_BITMAP_WORDS: usize = DIAG_MAX_PAGES_PER_CHUNK / 64;
76
77/// Cap on how many per-bad-page SHA-384 lines we emit per Phase B mismatch,
78/// so a wholly-corrupt chunk doesn't spam thousands of log lines.
79const DIAG_MAX_BAD_PAGE_HASHES: usize = 32;
80
81/// Maximum number of 2 MB chunks we track. Debug builds hash roughly
82/// kernel + initrd (~80 MB), giving ~40 chunks; leave generous headroom.
83const DIAG_MAX_CHUNKS: usize = 256;
84
85/// Maximum number of individual 4 KB pages we can track for per-page
86/// expected-hash comparison. Sized with headroom over the current
87/// shared-page count observed in soaks (~20 K pages = 40 x 2 MB chunks).
88const DIAG_MAX_HASH_PAGES: usize = 32 * 1024;
89const DIAG_HASH_BITMAP_WORDS: usize = DIAG_MAX_HASH_PAGES / 64;
90
91/// Number of corrupt pages for which we save the full 4 KB contents so
92/// we can hex-dump them on final report.
93const DIAG_MAX_SAVED_BAD_PAGES: usize = 3;
94
95#[derive(Copy, Clone)]
96struct DiagChunkHash {
97    gpa: u64,
98    len: u32,
99    phase_a_hash: [u8; 48],
100}
101
102/// One corrupt page's full 4 KB contents plus the shim vs loader hashes,
103/// captured during Phase A when we first noticed the mismatch.
104#[derive(Copy, Clone)]
105struct DiagSavedBadPage {
106    /// Global page index in the shim's iteration order.
107    page_idx: u32,
108    /// Guest physical address of the page.
109    gpa: u64,
110    /// SHA-384 the shim computed over the host-loaded bytes.
111    shim_hash: [u8; 48],
112    /// SHA-384 the loader recorded at IGVM build time.
113    expected_hash: [u8; 48],
114    /// Full 4 KB page contents (as the shim saw them at Phase A).
115    contents: [u8; DIAG_PAGE_SIZE],
116}
117
118const DIAG_EMPTY_SAVED_PAGE: DiagSavedBadPage = DiagSavedBadPage {
119    page_idx: 0,
120    gpa: 0,
121    shim_hash: [0; 48],
122    expected_hash: [0; 48],
123    contents: [0; DIAG_PAGE_SIZE],
124};
125
126/// Per-page expected-hash tracking state. Populated during SNP preflight
127/// (or TDX Phase A) against the measured loader-emitted SHA-384 hashes.
128struct DiagPerPageState {
129    /// Cached slice from `ShimParams::expected_page_hashes()`. `None` if
130    /// the IGVM has no expected-page-hashes region (older loader) or the
131    /// region magic/version mismatched -- per-page compare is disabled.
132    expected: Option<&'static [loader_defs::paravisor::ExpectedPageHash]>,
133    /// Number of 4 KB shared pages processed so far.
134    seen: u32,
135    /// Number of pages whose Phase A hash did not match `expected[i]`.
136    bad: u32,
137    /// True if the shim hashed more pages than the loader emitted hashes
138    /// for; per-page compare stops for the tail of the walk when this
139    /// flips true.
140    overflow: bool,
141    /// Bitmap of mismatched pages indexed by shim page index (bit i set
142    /// = page i differed from `expected[i]`). Capped at
143    /// `DIAG_MAX_HASH_PAGES` positions -- anything beyond is not
144    /// tracked in the bitmap (still counted in `bad`).
145    bitmap: [u64; DIAG_HASH_BITMAP_WORDS],
146    /// Number of entries populated in `saved`.
147    saved_count: u32,
148    /// Full 4 KB contents of the first `DIAG_MAX_SAVED_BAD_PAGES` bad
149    /// pages, so we can hex-dump them for byte-level inspection.
150    saved: [DiagSavedBadPage; DIAG_MAX_SAVED_BAD_PAGES],
151}
152
153impl DiagPerPageState {
154    const fn new_const() -> Self {
155        Self {
156            expected: None,
157            seen: 0,
158            bad: 0,
159            overflow: false,
160            bitmap: [0; DIAG_HASH_BITMAP_WORDS],
161            saved_count: 0,
162            saved: [DIAG_EMPTY_SAVED_PAGE; DIAG_MAX_SAVED_BAD_PAGES],
163        }
164    }
165}
166
167static DIAG_CHUNK_HASHES: SingleThreaded<RefCell<ArrayVec<DiagChunkHash, DIAG_MAX_CHUNKS>>> =
168    SingleThreaded(RefCell::new(ArrayVec::new_const()));
169
170static DIAG_RUNNING_A: SingleThreaded<RefCell<Option<Sha384>>> = SingleThreaded(RefCell::new(None));
171
172/// One-shot latch guarding the full 4 KB hex dump of a corrupted page.
173/// Whichever phase (B or C) trips a mismatch first gets to dump; every
174/// subsequent detection just logs the header/bitmap/hashes and skips the
175/// full dump. Keeps COM3 quiet even when many chunks are corrupted.
176static DIAG_FULL_PAGE_DUMPED: SingleThreaded<core::cell::Cell<bool>> =
177    SingleThreaded(core::cell::Cell::new(false));
178
179/// Per-page expected-hash tracking (populated in `DiagPerPageState::record` and
180/// reported in `diag_report_per_page_expected`).
181static DIAG_PER_PAGE: SingleThreaded<RefCell<DiagPerPageState>> =
182    SingleThreaded(RefCell::new(DiagPerPageState::new_const()));
183
184/// Returns true and latches on the first call; returns false thereafter.
185fn diag_claim_full_page_dump() -> bool {
186    if DIAG_FULL_PAGE_DUMPED.0.get() {
187        return false;
188    }
189    DIAG_FULL_PAGE_DUMPED.0.set(true);
190    true
191}
192
193/// `core::fmt::Display` adapter that prints a byte slice as lowercase hex,
194/// no separators. Convenient for hashes in log lines.
195struct HexBytes<'a>(&'a [u8]);
196impl core::fmt::Display for HexBytes<'_> {
197    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
198        for b in self.0 {
199            write!(f, "{:02x}", b)?;
200        }
201        Ok(())
202    }
203}
204
205/// `core::fmt::Display` adapter that prints a page-bitmap (little-endian
206/// per byte within each u64 word) as a compact hex string. One hex char per
207/// 4 pages, so a full 2 MB / 4 KB = 512-page chunk fits in 128 hex chars.
208struct BitmapHex<'a> {
209    bitmap: &'a [u64],
210    total_pages: usize,
211}
212impl core::fmt::Display for BitmapHex<'_> {
213    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
214        let bits = self.total_pages.min(self.bitmap.len() * 64);
215        let bytes = bits.div_ceil(8);
216        for byte_idx in 0..bytes {
217            let word = byte_idx / 8;
218            let byte_in_word = byte_idx % 8;
219            let b = ((self.bitmap[word] >> (byte_in_word * 8)) & 0xff) as u8;
220            write!(f, "{:02x}", b)?;
221        }
222        Ok(())
223    }
224}
225
226/// `core::fmt::Display` adapter that walks a page-bitmap and emits corrupt
227/// page-index ranges in the form `0x8-0xa,0x11,0x20-0x21`. Human-readable
228/// alternative to the raw hex bitmap.
229struct RleRanges<'a> {
230    bitmap: &'a [u64],
231    total_pages: usize,
232}
233impl core::fmt::Display for RleRanges<'_> {
234    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
235        let mut first = true;
236        let mut run_start: Option<usize> = None;
237        let mut prev_bit: bool = false;
238        for idx in 0..self.total_pages {
239            let word = idx / 64;
240            let bit = idx % 64;
241            let set = word < self.bitmap.len() && (self.bitmap[word] >> bit) & 1 == 1;
242            if set && !prev_bit {
243                run_start = Some(idx);
244            }
245            if !set && prev_bit {
246                let start = run_start.unwrap();
247                let end = idx - 1;
248                if !first {
249                    write!(f, ",")?;
250                }
251                first = false;
252                if start == end {
253                    write!(f, "{:#x}", start)?;
254                } else {
255                    write!(f, "{:#x}-{:#x}", start, end)?;
256                }
257                run_start = None;
258            }
259            prev_bit = set;
260        }
261        // Handle a run that reaches the last page.
262        if prev_bit {
263            let start = run_start.unwrap();
264            let end = self.total_pages - 1;
265            if !first {
266                write!(f, ",")?;
267            }
268            if start == end {
269                write!(f, "{:#x}", start)?;
270            } else {
271                write!(f, "{:#x}-{:#x}", start, end)?;
272            }
273        }
274        Ok(())
275    }
276}
277
278/// Record the Phase-A hash of a chunk that was just copied out of a shared
279/// page into `ram_buffer`. Also feeds the bytes into a running combined
280/// Phase-A hasher so it can be compared against `imported_regions_hash()`
281/// on final mismatch. Per-page expected hashes are checked separately so SNP
282/// can report all corrupt shared pages before any imported page is converted.
283fn diag_record_phase_a(gpa: u64, data: &[u8]) {
284    {
285        let mut running = DIAG_RUNNING_A.0.borrow_mut();
286        if running.is_none() {
287            *running = Some(Sha384::new());
288        }
289        running.as_mut().unwrap().update(data);
290    }
291
292    let mut h = Sha384::new();
293    h.update(data);
294    let hash: [u8; 48] = h.finalize().into();
295
296    let mut chunks = DIAG_CHUNK_HASHES.0.borrow_mut();
297    if chunks
298        .try_push(DiagChunkHash {
299            gpa,
300            len: data.len() as u32,
301            phase_a_hash: hash,
302        })
303        .is_err()
304    {
305        log::error!(
306            "DIAG_CHUNK_OVERFLOW gpa={:#x} len={:#x} (increase DIAG_MAX_CHUNKS)",
307            gpa,
308            data.len(),
309        );
310    }
311}
312
313impl DiagPerPageState {
314    /// Compare shared pages and retain the first few corrupt pages for the host.
315    fn record(&mut self, gpa: u64, data: &[u8]) {
316        if let Some(expected) = self.expected {
317            for (page_off, page) in data.chunks_exact(DIAG_PAGE_SIZE).enumerate() {
318                let idx = self.seen as usize;
319                let page_gpa = gpa + (page_off * DIAG_PAGE_SIZE) as u64;
320
321                let shim_hash: [u8; 48] = Sha384::digest(page).into();
322
323                if idx >= expected.len() {
324                    self.overflow = true;
325                } else {
326                    let expected_hash = expected[idx].sha384_hash;
327                    if shim_hash != expected_hash {
328                        self.bad = self.bad.saturating_add(1);
329                        log::error!(
330                            "DIAG_PAGE_HASH_BAD_GPA idx={} gpa={:#x} bad_page_number={}",
331                            idx,
332                            page_gpa,
333                            self.bad,
334                        );
335                        if idx < DIAG_MAX_HASH_PAGES {
336                            let word = idx / 64;
337                            let bit = idx % 64;
338                            self.bitmap[word] |= 1u64 << bit;
339                        }
340                        let slot_idx = self.saved_count as usize;
341                        if slot_idx < DIAG_MAX_SAVED_BAD_PAGES {
342                            let slot = &mut self.saved[slot_idx];
343                            slot.page_idx = idx as u32;
344                            slot.gpa = page_gpa;
345                            slot.shim_hash = shim_hash;
346                            slot.expected_hash = expected_hash;
347                            slot.contents.copy_from_slice(page);
348                            self.saved_count += 1;
349                        }
350                    }
351                }
352                self.seen = self.seen.saturating_add(1);
353            }
354        }
355    }
356}
357
358/// Emit a full 4 KB page as hex, one log line per 64-byte cache line, with
359/// pre and post side by side. Used when we have both Phase-A and Phase-C
360/// bytes for the first corrupted page.
361fn diag_dump_full_page_diff(page_gpa: u64, phase: &str, pre: &[u8], post: &[u8]) {
362    debug_assert_eq!(pre.len(), DIAG_PAGE_SIZE);
363    debug_assert_eq!(post.len(), DIAG_PAGE_SIZE);
364    log::error!(
365        "DIAG_FIRST_BAD_PAGE_BEGIN page_gpa={:#x} phase={} len={:#x}",
366        page_gpa,
367        phase,
368        DIAG_PAGE_SIZE,
369    );
370    for (line_idx, (pre_line, post_line)) in pre.chunks(64).zip(post.chunks(64)).enumerate() {
371        let offset = line_idx * 64;
372        log::error!(
373            "DIAG_FIRST_BAD_PAGE_LINE page_gpa={:#x} offset={:#06x} pre={} post={}",
374            page_gpa,
375            offset,
376            HexBytes(pre_line),
377            HexBytes(post_line),
378        );
379    }
380    log::error!("DIAG_FIRST_BAD_PAGE_END page_gpa={:#x}", page_gpa);
381}
382
383/// Emit a full 4 KB page as hex, one log line per 64-byte cache line. Used
384/// when only the current (Phase-D) bytes are available for the first bad
385/// page and there is no pre-image to compare side by side.
386fn diag_dump_full_page_single(page_gpa: u64, phase: &str, data: &[u8]) {
387    debug_assert!(data.len() >= DIAG_PAGE_SIZE);
388    log::error!(
389        "DIAG_FIRST_BAD_PAGE_BEGIN page_gpa={:#x} phase={} len={:#x}",
390        page_gpa,
391        phase,
392        DIAG_PAGE_SIZE,
393    );
394    for (line_idx, line) in data[..DIAG_PAGE_SIZE].chunks(64).enumerate() {
395        let offset = line_idx * 64;
396        log::error!(
397            "DIAG_FIRST_BAD_PAGE_LINE page_gpa={:#x} offset={:#06x} bytes={}",
398            page_gpa,
399            offset,
400            HexBytes(line),
401        );
402    }
403    log::error!("DIAG_FIRST_BAD_PAGE_END page_gpa={:#x}", page_gpa);
404}
405
406/// Emit a saved corrupt page (captured in Phase A) as hex, one log line
407/// per 64-byte cache line, along with the shim vs loader hashes.
408fn diag_dump_saved_page(saved: &DiagSavedBadPage, emit: &mut impl FnMut(core::fmt::Arguments<'_>)) {
409    emit(format_args!(
410        "DIAG_PAGE_HASH_BAD_BEGIN idx={} gpa={:#x} shim_hash={} expected_hash={} len={:#x}",
411        saved.page_idx,
412        saved.gpa,
413        HexBytes(&saved.shim_hash),
414        HexBytes(&saved.expected_hash),
415        DIAG_PAGE_SIZE,
416    ));
417    for (line_idx, line) in saved.contents.chunks(64).enumerate() {
418        let offset = line_idx * 64;
419        emit(format_args!(
420            "DIAG_PAGE_HASH_BAD_LINE idx={} gpa={:#x} offset={:#06x} bytes={}",
421            saved.page_idx,
422            saved.gpa,
423            offset,
424            HexBytes(line),
425        ));
426    }
427    emit(format_args!(
428        "DIAG_PAGE_HASH_BAD_END idx={} gpa={:#x}",
429        saved.page_idx, saved.gpa,
430    ));
431}
432
433/// Cache the loader-emitted per-page expected hashes so `DiagPerPageState::record`
434/// can compare each 4 KB page against them. Must be called once, before the
435/// acceptance loop in `setup_vtl2_memory`. If the IGVM doesn't have the
436/// expected-page-hashes region (older loader) or the region magic/version
437/// mismatched, per-page compare is left disabled and `DiagPerPageState::record`
438/// silently skips the per-page work.
439fn diag_init_expected_hashes(shim_params: &ShimParams) {
440    let expected = shim_params.expected_page_hashes();
441    if expected.is_empty() {
442        log::info!(
443            "DIAG_EXPECTED_HASHES_META loader_count=0 \
444             (region absent or magic/version mismatch; per-page compare disabled)"
445        );
446        return;
447    }
448    DIAG_PER_PAGE.0.borrow_mut().expected = Some(expected);
449    log::info!("DIAG_EXPECTED_HASHES_META loader_count={}", expected.len(),);
450}
451
452/// Emit the per-page expected-hash comparison summary, corrupt-page bitmap,
453/// RLE ranges, and full 4 KB dumps of the first `DIAG_MAX_SAVED_BAD_PAGES`
454/// corrupt pages. On SNP, also called during preflight, before conversion.
455/// Keep RANGES -> BAD_BEGIN -> BAD_LINE (64 lines) -> BAD_END unchanged:
456/// the host's GHCB COM3 parser breaks at the first BAD_END.
457fn diag_report_per_page_expected() {
458    let state = DIAG_PER_PAGE.0.borrow();
459    diag_emit_per_page_expected(&state, |message| log::error!("{}", message));
460}
461
462fn diag_emit_per_page_expected(
463    state: &DiagPerPageState,
464    mut emit: impl FnMut(core::fmt::Arguments<'_>),
465) {
466    let loader_count = state.expected.map(|s| s.len()).unwrap_or(0);
467    emit(format_args!(
468        "DIAG_PAGE_HASH_SUMMARY shim_seen={} bad={} loader_count={} overflow={}",
469        state.seen, state.bad, loader_count, state.overflow,
470    ));
471    if state.expected.is_none() {
472        // Per-page compare was disabled; nothing more to report.
473        return;
474    }
475    let bitmap_total = (state.seen as usize).min(DIAG_MAX_HASH_PAGES);
476    emit(format_args!(
477        "DIAG_PAGE_HASH_BITMAP total_pages={} bad={} bitmap={}",
478        bitmap_total,
479        state.bad,
480        BitmapHex {
481            bitmap: &state.bitmap,
482            total_pages: bitmap_total,
483        },
484    ));
485    emit(format_args!(
486        "DIAG_PAGE_HASH_RANGES total_pages={} bad={} ranges={}",
487        bitmap_total,
488        state.bad,
489        RleRanges {
490            bitmap: &state.bitmap,
491            total_pages: bitmap_total,
492        },
493    ));
494    for i in 0..(state.saved_count as usize) {
495        diag_dump_saved_page(&state.saved[i], &mut emit);
496    }
497}
498
499/// Inspect every pending SNP imported page through a C=0 mapping before any
500/// imported page changes visibility. Do not read these GPAs through the private
501/// identity map, and do not call the post-accept diagnostic reporter here.
502fn diag_verify_shared_imported_regions(shim_params: &ShimParams, local_map: &mut LocalMap<'_>) {
503    let mut hasher = Sha384::new();
504    let mut total_pages = 0;
505    for (range, already_accepted) in shim_params.imported_regions() {
506        if already_accepted {
507            continue;
508        }
509        for chunk in pending_import_chunks(range) {
510            let mapping = local_map.map_pages(chunk, false);
511            hasher.update(&*mapping.data);
512            DIAG_PER_PAGE
513                .0
514                .borrow_mut()
515                .record(chunk.start(), mapping.data);
516            total_pages += chunk.len() / hvdef::HV_PAGE_SIZE;
517        }
518    }
519    if total_pages == 0 {
520        return;
521    }
522
523    let hash: [u8; 48] = hasher.finalize().into();
524    let expected = shim_params.imported_regions_hash();
525    if hash.as_slice() != expected || DIAG_PER_PAGE.0.borrow().bad != 0 {
526        log::error!(
527            "DIAG_SHARED_PREFLIGHT_MISMATCH total_pages={} combined_shared={} expected={}",
528            total_pages,
529            HexBytes(&hash),
530            HexBytes(expected),
531        );
532        diag_report_per_page_expected();
533        panic!("Imported regions hash mismatch");
534    }
535}
536
537/// Split a pending imported range at the local map's 2 MB boundaries.
538fn pending_import_chunks(mut remaining: MemoryRange) -> impl Iterator<Item = MemoryRange> {
539    core::iter::from_fn(move || {
540        if remaining.is_empty() {
541            return None;
542        }
543        let end = remaining
544            .end()
545            .min((remaining.start() + X64_LARGE_PAGE_SIZE) & !(X64_LARGE_PAGE_SIZE - 1));
546        let chunk = MemoryRange::new(remaining.start()..end);
547        remaining = MemoryRange::new(end..remaining.end());
548        Some(chunk)
549    })
550}
551
552/// Immediately after the shared -> private transition and copy-back, compare
553/// the same chunk (via the identity map) against the Phase-A bytes we captured
554/// before the transition, at 4 KB PAGE granularity. On mismatch, emits:
555///  - `DIAG_TRANSITION_MISMATCH` header with total/bad page counts and per-
556///    chunk pre/post SHA-384.
557///  - `DIAG_TRANSITION_BITMAP` (hex) and `DIAG_TRANSITION_PAGES` (RLE) so we
558///    can see the distribution of corrupt pages within the chunk.
559///  - `DIAG_BAD_PAGE_HASH` per corrupt page (capped at
560///    DIAG_MAX_BAD_PAGE_HASHES) with SHA-384 of both pre and post.
561///  - Full 4 KB hex dump of the FIRST corrupt page via
562///    `DIAG_FIRST_BAD_PAGE_{BEGIN,LINE,END}` -- but ONLY if no previous
563///    call (from any chunk or from Phase C) has already claimed the
564///    one-shot dump latch. One sample is enough to characterise the
565///    corruption pattern.
566fn diag_verify_phase_b(gpa: u64, pre: &[u8], post: &[u8]) {
567    if pre.len() != post.len() {
568        log::error!(
569            "DIAG_TRANSITION_LEN_MISMATCH gpa={:#x} pre_len={:#x} post_len={:#x}",
570            gpa,
571            pre.len(),
572            post.len(),
573        );
574        return;
575    }
576    if pre == post {
577        return;
578    }
579
580    // Bitmap of mismatched pages within this chunk. Chunks are <= 2 MB
581    // = 512 pages, so DIAG_BITMAP_WORDS u64s suffice.
582    let mut bitmap = [0u64; DIAG_BITMAP_WORDS];
583    let mut bad_count: usize = 0;
584    let mut first_bad_page_idx: Option<usize> = None;
585    let mut bad_hashes: ArrayVec<(u64, [u8; 48], [u8; 48]), DIAG_MAX_BAD_PAGE_HASHES> =
586        ArrayVec::new();
587    let total_pages = pre.len().div_ceil(DIAG_PAGE_SIZE);
588
589    for (idx, (pre_pg, post_pg)) in pre
590        .chunks(DIAG_PAGE_SIZE)
591        .zip(post.chunks(DIAG_PAGE_SIZE))
592        .enumerate()
593    {
594        if pre_pg == post_pg {
595            continue;
596        }
597        bad_count += 1;
598        if first_bad_page_idx.is_none() {
599            first_bad_page_idx = Some(idx);
600        }
601        let word = idx / 64;
602        let bit = idx % 64;
603        if word < bitmap.len() {
604            bitmap[word] |= 1u64 << bit;
605        }
606
607        if bad_hashes.len() < DIAG_MAX_BAD_PAGE_HASHES {
608            let mut ha = Sha384::new();
609            ha.update(pre_pg);
610            let hash_a: [u8; 48] = ha.finalize().into();
611            let mut hc = Sha384::new();
612            hc.update(post_pg);
613            let hash_c: [u8; 48] = hc.finalize().into();
614            let _ = bad_hashes.try_push((gpa + (idx * DIAG_PAGE_SIZE) as u64, hash_a, hash_c));
615        }
616    }
617
618    // Overall pre/post SHA-384 for the whole chunk, for quick fingerprinting.
619    let mut ha = Sha384::new();
620    ha.update(pre);
621    let chunk_hash_a: [u8; 48] = ha.finalize().into();
622    let mut hb = Sha384::new();
623    hb.update(post);
624    let chunk_hash_b: [u8; 48] = hb.finalize().into();
625
626    log::error!(
627        "DIAG_TRANSITION_MISMATCH gpa={:#x} chunk_len={:#x} total_pages={} bad_pages={} \
628         chunk_phase_a={} chunk_phase_b={}",
629        gpa,
630        pre.len(),
631        total_pages,
632        bad_count,
633        HexBytes(&chunk_hash_a),
634        HexBytes(&chunk_hash_b),
635    );
636    log::error!(
637        "DIAG_TRANSITION_BITMAP gpa={:#x} total_pages={} bitmap={}",
638        gpa,
639        total_pages,
640        BitmapHex {
641            bitmap: &bitmap,
642            total_pages,
643        },
644    );
645    log::error!(
646        "DIAG_TRANSITION_PAGES gpa={:#x} count={} ranges={}",
647        gpa,
648        bad_count,
649        RleRanges {
650            bitmap: &bitmap,
651            total_pages,
652        },
653    );
654
655    for (page_gpa, ha, hb) in &bad_hashes {
656        log::error!(
657            "DIAG_BAD_PAGE_HASH phase=A_vs_B chunk_gpa={:#x} page_gpa={:#x} phase_a={} phase_b={}",
658            gpa,
659            page_gpa,
660            HexBytes(ha),
661            HexBytes(hb),
662        );
663    }
664    if bad_count > bad_hashes.len() {
665        log::error!(
666            "DIAG_BAD_PAGE_HASH_TRUNCATED chunk_gpa={:#x} shown={} total_bad={}",
667            gpa,
668            bad_hashes.len(),
669            bad_count,
670        );
671    }
672
673    if let Some(idx) = first_bad_page_idx {
674        if diag_claim_full_page_dump() {
675            let offset = idx * DIAG_PAGE_SIZE;
676            let pre_pg = &pre[offset..offset + DIAG_PAGE_SIZE];
677            let post_pg = &post[offset..offset + DIAG_PAGE_SIZE];
678            diag_dump_full_page_diff(gpa + offset as u64, "A_vs_B", pre_pg, post_pg);
679        }
680    }
681}
682
683/// Called from `verify_imported_regions_hash` when the combined Phase-C hash
684/// does not match the expected measured value. Reports:
685/// - Whether the running combined Phase-A hash matches expected. If it does,
686///   the host supplied correct bytes and something in the shim corrupted them.
687///   If it doesn't, the host loaded bad data.
688/// - Per-chunk Phase-C vs Phase-A comparison to identify which chunk(s) drifted
689///   between the shared read and the final verify, plus a full 4 KB hex dump
690///   of the first page of the FIRST mismatched chunk (subject to the same
691///   one-shot latch as Phase B -- one sample is enough). Once the loader is
692///   updated to emit per-page expected hashes we can do per-page here too.
693fn diag_report_phase_c(expected_combined: &[u8]) {
694    let combined_a = DIAG_RUNNING_A.0.borrow_mut().take().map(|h| {
695        let out: [u8; 48] = h.finalize().into();
696        out
697    });
698
699    match combined_a {
700        Some(combined_a) => {
701            if combined_a.as_slice() == expected_combined {
702                log::error!(
703                    "DIAG_VERDICT combined_phase_a matches expected: {} \
704                     (host-supplied shared data was correct; corruption occurred at or after acceptance)",
705                    HexBytes(&combined_a),
706                );
707            } else {
708                log::error!(
709                    "DIAG_VERDICT combined_phase_a differs from expected: \
710                     phase_a={} expected={} \
711                     (host loaded incorrect data into shared pages)",
712                    HexBytes(&combined_a),
713                    HexBytes(expected_combined),
714                );
715            }
716        }
717        None => {
718            log::error!("DIAG_VERDICT combined_phase_a not captured (no chunks recorded)");
719        }
720    }
721
722    let chunks = DIAG_CHUNK_HASHES.0.borrow();
723    let total = chunks.len();
724    let mut mismatches: usize = 0;
725    for (idx, c) in chunks.iter().enumerate() {
726        // SAFETY: The GPA and length were recorded from a range that the shim
727        // itself just accepted as private VTL2 RAM, and remain identity-mapped
728        // for the duration of the shim.
729        let data = unsafe { core::slice::from_raw_parts(c.gpa as *const u8, c.len as usize) };
730        let mut h = Sha384::new();
731        h.update(data);
732        let hash_c: [u8; 48] = h.finalize().into();
733        if hash_c != c.phase_a_hash {
734            mismatches += 1;
735            log::error!(
736                "DIAG_POST_ACCEPT_MISMATCH idx={} gpa={:#x} len={:#x} phase_a={} phase_c={}",
737                idx,
738                c.gpa,
739                c.len,
740                HexBytes(&c.phase_a_hash),
741                HexBytes(&hash_c),
742            );
743            // We no longer have the Phase-A bytes (they lived in `ram_buffer`
744            // which has been reused). Full 4 KB dump of the first page of the
745            // first mismatched chunk lets us eyeball whether the chunk was
746            // zeroed, substituted with a different page, or has a subtle bit
747            // flip. One-shot: once we've dumped a page (here or in Phase B),
748            // subsequent mismatches only log the hash lines.
749            // TODO(item 5): once the loader emits per-page expected hashes,
750            // iterate the chunk at 4 KB granularity here and identify exactly
751            // which pages diverged (like diag_verify_phase_b does).
752            if data.len() >= DIAG_PAGE_SIZE && diag_claim_full_page_dump() {
753                diag_dump_full_page_single(c.gpa, "C", data);
754            }
755        }
756    }
757    if mismatches == 0 {
758        log::error!(
759            "DIAG_VERDICT all {} tracked chunks unchanged phase_a -> phase_c \
760             (combined mismatch is likely a layout/order/hashing disagreement)",
761            total,
762        );
763    } else {
764        log::error!(
765            "DIAG_VERDICT {} of {} tracked chunks changed between phase_a and phase_c",
766            mismatches,
767            total,
768        );
769    }
770
771    // Per-page comparison against the loader-emitted expected hashes.
772    // Emits the shim-vs-loader page count, the corrupt-page bitmap and
773    // RLE ranges, and full 4 KB dumps of the first
774    // `DIAG_MAX_SAVED_BAD_PAGES` corrupt pages.
775    diag_report_per_page_expected();
776}
777
778/// On isolated systems, transitions all VTL2 RAM to be private and accepted, with the appropriate
779/// VTL permissions applied.
780pub fn setup_vtl2_memory(
781    shim_params: &ShimParams,
782    partition_info: &PartitionInfo,
783    address_space: &mut AddressSpaceManager,
784) {
785    // Only if the partition is VBS-isolated, accept memory and apply vtl 2 protections here.
786    // Non-isolated partitions can undergo servicing, and additional information
787    // would be needed to determine whether vtl 2 protections should be applied
788    // or skipped, since the operation is expensive.
789    // TODO: if applying vtl 2 protections for non-isolated VMs moves to the
790    // boot shim, apply them here.
791    if let IsolationType::None = shim_params.isolation_type {
792        return;
793    }
794
795    // SNP compares these before conversion; TDX compares during Phase A.
796    diag_init_expected_hashes(shim_params);
797
798    if let IsolationType::Vbs = shim_params.isolation_type {
799        // Enable VTL protection so that vtl 2 protections can be applied. All other config
800        // should be set by the user mode
801        let vsm_config = hvdef::HvRegisterVsmPartitionConfig::new()
802            .with_default_vtl_protection_mask(0xF)
803            .with_enable_vtl_protection(true);
804
805        hvcall()
806            .set_register(
807                hvdef::HvX64RegisterName::VsmPartitionConfig.into(),
808                hvdef::HvRegisterValue::from(u64::from(vsm_config)),
809            )
810            .expect("setting vsm config shouldn't fail");
811
812        // VBS isolated VMs need to apply VTL2 protections to pages that were already accepted to
813        // prevent VTL0 access. Only those pages that belong to the VTL2 RAM region should have
814        // these protections applied - certain pages belonging to VTL0 are also among the accepted
815        // regions and should not be processed here.
816        let accepted_ranges =
817            shim_params
818                .imported_regions()
819                .filter_map(|(imported_range, already_accepted)| {
820                    already_accepted.then_some(imported_range)
821                });
822        for range in memory_range::overlapping_ranges(
823            partition_info.vtl2_ram.iter().map(|entry| entry.range),
824            accepted_ranges,
825        ) {
826            hvcall()
827                .apply_vtl2_protections(range)
828                .expect("applying vtl 2 protections cannot fail");
829        }
830    }
831
832    // Initialize the local_map
833    // TODO: Consider moving this to ShimParams to pass around.
834    let mut local_map = match shim_params.isolation_type {
835        IsolationType::Snp | IsolationType::Tdx => Some(init_local_map(
836            loader_defs::paravisor::PARAVISOR_LOCAL_MAP_VA,
837        )),
838        _ => None,
839    };
840
841    // Make sure imported regions are in increasing order.
842    let mut last_range_end = None;
843    for (imported_range, _) in shim_params.imported_regions() {
844        assert!(last_range_end.is_none() || imported_range.start() > last_range_end.unwrap());
845        last_range_end = Some(imported_range.end() - hvdef::HV_PAGE_SIZE);
846    }
847
848    if shim_params.isolation_type == IsolationType::Snp {
849        diag_verify_shared_imported_regions(shim_params, local_map.as_mut().unwrap());
850    }
851
852    // Iterate over all VTL2 RAM that is not part of an imported region and
853    // accept it with appropriate VTL protections.
854    for range in memory_range::subtract_ranges(
855        partition_info.vtl2_ram.iter().map(|e| e.range),
856        shim_params.imported_regions().map(|(r, _)| r),
857    ) {
858        accept_vtl2_memory(shim_params, &mut local_map, range);
859    }
860
861    let ram_buffer = if let Some(bounce_buffer) = shim_params.bounce_buffer {
862        assert!(bounce_buffer.start() % X64_LARGE_PAGE_SIZE == 0);
863        assert!(bounce_buffer.len() >= X64_LARGE_PAGE_SIZE);
864
865        for range in memory_range::subtract_ranges(
866            core::iter::once(bounce_buffer),
867            partition_info.vtl2_ram.iter().map(|e| e.range),
868        ) {
869            accept_vtl2_memory(shim_params, &mut local_map, range);
870        }
871
872        // SAFETY: The bounce buffer is trusted as it is obtained from measured
873        // shim parameters. The bootloader is identity mapped, and the PA is
874        // guaranteed to be mapped as the pagetable is prebuilt and measured.
875        unsafe {
876            core::slice::from_raw_parts_mut(
877                bounce_buffer.start() as *mut u8,
878                bounce_buffer.len() as usize,
879            )
880        }
881    } else {
882        &mut []
883    };
884
885    // Iterate over all imported regions that are not already accepted. They must be accepted here.
886    // TODO: No VTL0 memory is currently marked as pending.
887    for (imported_range, already_accepted) in shim_params.imported_regions() {
888        if !already_accepted {
889            accept_pending_vtl2_memory(shim_params, &mut local_map, ram_buffer, imported_range);
890        }
891    }
892
893    // TDX has specific memory initialization logic. Create a set of page tables for the APs
894    // to use during the mailbox spinloop, and carve out memory for TDCALL based hypercalls
895    if shim_params.isolation_type == IsolationType::Tdx {
896        // Allocate a range of memory for AP page tables
897        let page_table_region = address_space
898            .allocate_aligned(
899                None,
900                PAGE_TABLE_MAX_BYTES as u64,
901                AllocationType::TdxPageTables,
902                AllocationPolicy::LowMemory,
903                X64_LARGE_PAGE_SIZE,
904            )
905            .expect("allocation of space for TDX page tables must succeed");
906
907        // The local map will map a single 2MB PTE per allocation
908        const_assert!((PAGE_TABLE_MAX_BYTES as u64) < X64_LARGE_PAGE_SIZE);
909        assert_eq!(page_table_region.range.start() % X64_LARGE_PAGE_SIZE, 0);
910
911        let mut local_map = local_map.expect("must be present on TDX");
912        let page_table_region_mapping = local_map.map_pages(page_table_region.range, false);
913        page_table_region_mapping.data.fill(0);
914
915        const MAX_RANGE_COUNT: usize = 64;
916        let mut ranges = off_stack!(
917            ArrayVec::<MappedRange, MAX_RANGE_COUNT>,
918            ArrayVec::new_const()
919        );
920
921        // All VTL2_RAM ranges should be present as R+X in the AP page table mappings, the mailbox
922        // wakeup vector will be somewhere in this range, below the 4GB boundary
923        const AP_MEMORY_BOUNDARY: u64 = 4 * 1024 * 1024 * 1024;
924        let vtl2_ram = address_space
925            .vtl2_ranges()
926            .filter_map(|(range, typ)| match typ {
927                MemoryVtlType::VTL2_RAM => {
928                    if range.start() < AP_MEMORY_BOUNDARY {
929                        let end = if range.end() < AP_MEMORY_BOUNDARY {
930                            range.end()
931                        } else {
932                            AP_MEMORY_BOUNDARY
933                        };
934                        Some(MappedRange::new(range.start(), end).read_only())
935                    } else {
936                        None
937                    }
938                }
939                _ => None,
940            });
941
942        ranges.extend(vtl2_ram);
943
944        // Map the reset vector as executable and writable, as the mailbox protocol uses offsets
945        // in the reset vector to communicate with the kernel
946        const PAGE_SIZE: u64 = 0x1000;
947        ranges.push(MappedRange::new(
948            x86defs::tdx::RESET_VECTOR_PAGE,
949            x86defs::tdx::RESET_VECTOR_PAGE + PAGE_SIZE,
950        ));
951
952        ranges.sort_by_key(|r| r.start());
953
954        let mut page_table_work_buffer =
955            off_stack!(ArrayVec<PageTable, PAGE_TABLE_MAX_COUNT>, ArrayVec::new_const());
956        for _ in 0..PAGE_TABLE_MAX_COUNT {
957            page_table_work_buffer.push(PageTable::new_zeroed());
958        }
959
960        PageTableBuilder::new(
961            page_table_region.range.start(),
962            page_table_work_buffer.as_mut_slice(),
963            page_table_region_mapping.data,
964            ranges.as_slice(),
965        )
966        .expect("page table builder must return no error")
967        .build()
968        .expect("page table construction must succeed");
969
970        crate::arch::tdx::tdx_prepare_ap_trampoline(page_table_region.range.start());
971
972        // For TDVMCALL based hypercalls, take the first 2 MB region from ram_buffer for
973        // hypercall IO pages. ram_buffer must not be used again beyond this point
974        // TODO: find an approach that does not require re-using the ram_buffer
975        let free_buffer = ram_buffer.as_mut_ptr() as u64;
976        assert!(free_buffer.is_multiple_of(X64_LARGE_PAGE_SIZE));
977        // SAFETY: The bottom 2MB region of the ram_buffer is unused by the shim
978        // The region is aligned to 2MB, and mapped as a large page
979        let tdx_io_page = unsafe {
980            tdx_share_large_page(free_buffer);
981            TdxHypercallPage::new(free_buffer)
982        };
983        hvcall().initialize_tdx(tdx_io_page);
984    }
985}
986
987/// Accepts VTL2 memory in the specified gpa range.
988fn accept_vtl2_memory(
989    shim_params: &ShimParams,
990    local_map: &mut Option<LocalMap<'_>>,
991    range: MemoryRange,
992) {
993    match shim_params.isolation_type {
994        IsolationType::Vbs => {
995            hvcall()
996                .accept_vtl2_pages(range, hvdef::hypercall::AcceptMemoryType::RAM)
997                .expect("accepting vtl 2 memory must not fail");
998        }
999        IsolationType::Snp => {
1000            super::snp::set_page_acceptance(local_map.as_mut().unwrap(), range, true)
1001                .expect("accepting vtl 2 memory must not fail");
1002        }
1003        IsolationType::Tdx => {
1004            super::tdx::accept_pages(range).expect("accepting vtl2 memory must not fail")
1005        }
1006        _ => unreachable!(),
1007    }
1008}
1009
1010/// Accepts VTL2 memory in the specified range that is currently marked as pending, i.e. not
1011/// yet assigned as exclusive and private.
1012fn accept_pending_vtl2_memory(
1013    shim_params: &ShimParams,
1014    local_map: &mut Option<LocalMap<'_>>,
1015    ram_buffer: &mut [u8],
1016    range: MemoryRange,
1017) {
1018    let isolation_type = shim_params.isolation_type;
1019
1020    match isolation_type {
1021        IsolationType::Vbs => {
1022            hvcall()
1023                .accept_vtl2_pages(range, hvdef::hypercall::AcceptMemoryType::RAM)
1024                .expect("accepting vtl 2 memory must not fail");
1025        }
1026        IsolationType::Snp | IsolationType::Tdx => {
1027            let local_map = local_map.as_mut().unwrap();
1028            // Accepting pending memory for SNP is somewhat more complicated. The pending regions
1029            // are unencrypted pages. Accepting them would result in their contents being scrambled.
1030            // Instead their contents must be copied out to a private region, then copied back once
1031            // the pages have been accepted. Additionally, the access to the unencrypted pages must
1032            // happen with the C-bit cleared.
1033            for range in pending_import_chunks(range) {
1034                let ram_buffer = &mut ram_buffer[..range.len() as usize];
1035
1036                // Map the pages as shared and copy the necessary number to the buffer.
1037                {
1038                    let map_range = if isolation_type == IsolationType::Tdx {
1039                        // set vtom on the page number
1040                        MemoryRange::new(
1041                            range.start() | TDX_SHARED_GPA_BOUNDARY_ADDRESS_BIT
1042                                ..range.end() | TDX_SHARED_GPA_BOUNDARY_ADDRESS_BIT,
1043                        )
1044                    } else {
1045                        range
1046                    };
1047
1048                    let mapping = local_map.map_pages(map_range, false);
1049                    ram_buffer.copy_from_slice(mapping.data);
1050
1051                    // On SNP, evict the shared (C=0) cache lines for these
1052                    // pages while the C=0 mapping is still live.
1053                    if isolation_type == IsolationType::Snp {
1054                        let mapping_va = mapping.data.as_ptr() as u64;
1055                        for page_offset in
1056                            (0..mapping.data.len() as u64).step_by(hvdef::HV_PAGE_SIZE as usize)
1057                        {
1058                            super::snp::cache_lines_flush_page(mapping_va + page_offset);
1059                        }
1060                    }
1061                }
1062
1063                // DIAG: record the SHA-384 of this chunk while it's still the
1064                // shared/host-loaded content, and feed it into a running
1065                // combined Phase-A hash for later comparison against the
1066                // measured expected hash.
1067                diag_record_phase_a(range.start(), &ram_buffer[..]);
1068                if isolation_type == IsolationType::Tdx {
1069                    DIAG_PER_PAGE
1070                        .0
1071                        .borrow_mut()
1072                        .record(range.start(), ram_buffer);
1073                }
1074
1075                // Change visibility on the pages for this iteration.
1076                match isolation_type {
1077                    IsolationType::Snp => {
1078                        super::snp::Ghcb::change_page_visibility(range, false);
1079                    }
1080                    IsolationType::Tdx => {
1081                        super::tdx::change_page_visibility(range, false);
1082                    }
1083                    _ => unreachable!(),
1084                }
1085
1086                // accept the pages.
1087                match isolation_type {
1088                    IsolationType::Snp => {
1089                        super::snp::set_page_acceptance(local_map, range, true)
1090                            .expect("accepting vtl 2 memory must not fail");
1091                    }
1092                    IsolationType::Tdx => {
1093                        super::tdx::accept_pages(range)
1094                            .expect("accepting vtl 2 memory must not fail");
1095                    }
1096                    _ => unreachable!(),
1097                }
1098
1099                // Copy the buffer back. Use the identity map now that the memory has been accepted.
1100                {
1101                    // SAFETY: Known memory region that was just accepted.
1102                    let mapping = unsafe {
1103                        core::slice::from_raw_parts_mut(
1104                            range.start() as *mut u8,
1105                            range.len() as usize,
1106                        )
1107                    };
1108
1109                    mapping.copy_from_slice(ram_buffer);
1110                }
1111
1112                // DIAG: re-hash the chunk from the freshly written private
1113                // page (Phase B) and compare against the Phase-A bytes still
1114                // sitting in `ram_buffer`. A mismatch here means the accept/
1115                // copy-back path corrupted this chunk; per-page bitmap, RLE
1116                // ranges, per-corrupt-page SHA-384s, and (once, globally) a
1117                // full 4 KB dump of the first bad page are logged.
1118                {
1119                    // SAFETY: Same memory just written above; identity mapped.
1120                    let post = unsafe {
1121                        core::slice::from_raw_parts(
1122                            range.start() as *const u8,
1123                            range.len() as usize,
1124                        )
1125                    };
1126                    diag_verify_phase_b(range.start(), &ram_buffer[..post.len()], post);
1127                }
1128            }
1129        }
1130        _ => unreachable!(),
1131    }
1132}
1133
1134// Verify the SHA384 hash of pages that were imported as unaccepted/shared. Compare against the
1135// desired hash that is passed in as a measured parameter. Failures result in a panic.
1136pub fn verify_imported_regions_hash(shim_params: &ShimParams) {
1137    // Non isolated VMs can undergo servicing, and thus the hash might no longer be valid,
1138    // as the memory regions can change during runtime.
1139    if let IsolationType::None = shim_params.isolation_type {
1140        return;
1141    }
1142
1143    // If all imported pages are already accepted, there is no need to verify the hash.
1144    if shim_params
1145        .imported_regions()
1146        .all(|(_, already_accepted)| already_accepted)
1147    {
1148        return;
1149    }
1150
1151    let mut hasher = Sha384::new();
1152    shim_params
1153        .imported_regions()
1154        .filter(|(_, already_accepted)| !already_accepted)
1155        .for_each(|(range, _)| {
1156            // SAFETY: The location and identity of the range is trusted as it is obtained from
1157            // measured shim parameters.
1158            let mapping = unsafe {
1159                core::slice::from_raw_parts(range.start() as *const u8, range.len() as usize)
1160            };
1161            hasher.update(mapping);
1162        });
1163
1164    let final_hash: [u8; 48] = hasher.finalize().into();
1165    let expected = shim_params.imported_regions_hash();
1166    if final_hash.as_slice() != expected {
1167        log::error!(
1168            "DIAG_COMBINED_PHASE_C combined_phase_c={} expected={}",
1169            HexBytes(&final_hash),
1170            HexBytes(expected),
1171        );
1172        diag_report_phase_c(expected);
1173        panic!("Imported regions hash mismatch");
1174    }
1175}
1176
1177#[cfg(test)]
1178mod tests {
1179    use super::*;
1180    use loader_defs::paravisor::ExpectedPageHash;
1181    use test_with_tracing::test;
1182
1183    fn expected_zero_pages(count: usize) -> DiagPerPageState {
1184        let expected = vec![
1185            ExpectedPageHash {
1186                sha384_hash: Sha384::digest([0; DIAG_PAGE_SIZE]).into(),
1187            };
1188            count
1189        ];
1190        DiagPerPageState {
1191            expected: Some(Box::leak(expected.into_boxed_slice())),
1192            ..DiagPerPageState::new_const()
1193        }
1194    }
1195
1196    #[test]
1197    fn shared_scan_tracks_noncontiguous_gpas_and_host_message_order() {
1198        let mut state = expected_zero_pages(5);
1199        let good = [0; DIAG_PAGE_SIZE];
1200        let bad = [0xab; DIAG_PAGE_SIZE];
1201        state.record(0x1ff000, &good);
1202        state.record(0x200000, &bad);
1203        state.record(0x800000, &good);
1204        state.record(0x801000, &bad);
1205        state.record(0x802000, &bad);
1206
1207        assert_eq!(state.seen, 5);
1208        assert_eq!(state.bad, 3);
1209        assert!(!state.overflow);
1210        assert_eq!(state.bitmap[0], 0b11010);
1211        assert_eq!(state.saved_count, 3);
1212        for (saved, (idx, gpa)) in
1213            state
1214                .saved
1215                .iter()
1216                .zip([(1, 0x200000), (3, 0x801000), (4, 0x802000)])
1217        {
1218            assert_eq!(saved.page_idx, idx);
1219            assert_eq!(saved.gpa, gpa);
1220            assert_eq!(saved.contents, bad);
1221            assert_eq!(saved.shim_hash, <[u8; 48]>::from(Sha384::digest(bad)));
1222            assert_eq!(saved.expected_hash, state.expected.unwrap()[0].sha384_hash);
1223        }
1224
1225        let mut messages = Vec::new();
1226        diag_emit_per_page_expected(&state, |message| {
1227            messages.push(format!("[ERROR] {message}"));
1228        });
1229        assert_eq!(
1230            messages[0],
1231            "[ERROR] DIAG_PAGE_HASH_SUMMARY shim_seen=5 bad=3 loader_count=5 overflow=false"
1232        );
1233        assert_eq!(
1234            messages[1],
1235            "[ERROR] DIAG_PAGE_HASH_BITMAP total_pages=5 bad=3 bitmap=1a"
1236        );
1237        assert_eq!(
1238            messages[2],
1239            "[ERROR] DIAG_PAGE_HASH_RANGES total_pages=5 bad=3 ranges=0x1,0x3-0x4"
1240        );
1241        assert_eq!(messages.len(), 3 + 3 * 66);
1242        for (saved, dump) in state.saved.iter().zip(messages[3..].chunks_exact(66)) {
1243            assert_eq!(
1244                dump[0],
1245                format!(
1246                    "[ERROR] DIAG_PAGE_HASH_BAD_BEGIN idx={} gpa={:#x} shim_hash={} expected_hash={} len=0x1000",
1247                    saved.page_idx,
1248                    saved.gpa,
1249                    HexBytes(&saved.shim_hash),
1250                    HexBytes(&saved.expected_hash),
1251                )
1252            );
1253            for (line_idx, line) in dump[1..65].iter().enumerate() {
1254                assert_eq!(
1255                    *line,
1256                    format!(
1257                        "[ERROR] DIAG_PAGE_HASH_BAD_LINE idx={} gpa={:#x} offset={:#06x} bytes={}",
1258                        saved.page_idx,
1259                        saved.gpa,
1260                        line_idx * 64,
1261                        "ab".repeat(64),
1262                    )
1263                );
1264            }
1265            assert_eq!(
1266                dump[65],
1267                format!(
1268                    "[ERROR] DIAG_PAGE_HASH_BAD_END idx={} gpa={:#x}",
1269                    saved.page_idx, saved.gpa,
1270                )
1271            );
1272        }
1273    }
1274
1275    #[test]
1276    fn shared_scan_counts_corruption_beyond_bitmap_and_dump_limits() {
1277        let mut state = expected_zero_pages(DIAG_MAX_HASH_PAGES + 4);
1278        state.seen = (DIAG_MAX_HASH_PAGES - 1) as u32;
1279        state.record(0x1000000, &[1; 4 * DIAG_PAGE_SIZE]);
1280        assert_eq!(state.seen as usize, DIAG_MAX_HASH_PAGES + 3);
1281        assert_eq!(state.bad, 4);
1282        assert_eq!(state.saved_count as usize, DIAG_MAX_SAVED_BAD_PAGES);
1283        assert_eq!(state.bitmap[DIAG_HASH_BITMAP_WORDS - 1], 1 << 63);
1284        assert!(!state.overflow);
1285    }
1286
1287    #[test]
1288    fn shared_scan_handles_missing_or_short_expected_hashes() {
1289        let mut state = DiagPerPageState::new_const();
1290        state.record(0x1000, &[0; DIAG_PAGE_SIZE]);
1291        assert_eq!(state.bad, 0);
1292        assert_eq!(state.saved_count, 0);
1293
1294        let mut state = expected_zero_pages(1);
1295        state.record(0x1000, &[0; 2 * DIAG_PAGE_SIZE]);
1296        assert_eq!(state.seen, 2);
1297        assert_eq!(state.bad, 0);
1298        assert_eq!(state.saved_count, 0);
1299        assert!(state.overflow);
1300    }
1301
1302    #[test]
1303    fn pending_chunks_match_conversion_boundaries() {
1304        let range = MemoryRange::new(0x1ff000..0x601000);
1305        let chunks: Vec<_> = pending_import_chunks(range).collect();
1306        assert_eq!(
1307            chunks,
1308            [
1309                MemoryRange::new(0x1ff000..0x200000),
1310                MemoryRange::new(0x200000..0x400000),
1311                MemoryRange::new(0x400000..0x600000),
1312                MemoryRange::new(0x600000..0x601000),
1313            ]
1314        );
1315        assert_eq!(chunks.iter().map(|r| r.len()).sum::<u64>(), range.len());
1316        assert_eq!(
1317            pending_import_chunks(MemoryRange::new(0x1000..0x1000)).count(),
1318            0
1319        );
1320    }
1321}