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