hyperlight-host 0.17.0

A lightweight Virtual Machine Manager that can be hosted in an application to safely run untrusted or code within a VM partition with very low latency and overhead.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
// SPDX-License-Identifier: Apache-2.0
// Copyright 2025 The Hyperlight Authors.

//! Model-specific register (MSR) state restored with a snapshot.
//!
//! The reset set contains every MSR whose guest-written state can persist.
//! Which MSRs the guest may access is a separate concern: declared MSRs still
//! reset.

use serde::{Deserialize, Serialize};

/// A single MSR captured for reset.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct MsrEntry {
    /// The index passed to `RDMSR` and `WRMSR`.
    pub index: u32,
    /// The captured value.
    pub value: u64,
}

/// The MSR reset set and initialization baseline for one VM.
#[derive(Debug, Clone)]
pub(crate) struct MsrResetState {
    /// Creation-time value for every MSR this VM resets on restore and can
    /// capture in a snapshot: the fixed core, required MTRRs, and the declared
    /// guest MSRs. Sorted by index and deduplicated.
    baseline: Vec<MsrEntry>,
    /// The declared guest MSRs
    /// ([`SandboxConfiguration::guest_msrs`](crate::sandbox::SandboxConfiguration::guest_msrs)),
    /// sorted and deduplicated. A subset of the `baseline` indices, kept
    /// separately to pick which reset entries a snapshot persists.
    guest_msrs: Vec<u32>,
}

/// MSRs always saved and restored, independent of the declared guest MSRs.
/// `SWAPGS` reaches `KERNEL_GS_BASE` and CET instructions reach active SSP on
/// the Hyper-V backends, so a `WRMSR` filter cannot gate them. `TSC` and
/// `TSC_ADJUST` carry guest time. All must survive restore for faithful resume.
const CORE_PERSIST_MSRS: &[u32] = &[MSR_KERNEL_GS_BASE, MSR_TSC, MSR_TSC_ADJUST, MSR_IA32_SSP];

impl MsrResetState {
    /// Captures and normalizes the reset state for a backend-provided index set.
    pub fn capture(
        mut indices: Vec<u32>,
        guest_msrs: &[u32],
        read: impl FnOnce(
            &[u32],
        )
            -> Result<Vec<MsrEntry>, crate::hypervisor::virtual_machine::RegisterError>,
    ) -> Result<Self, crate::hypervisor::virtual_machine::RegisterError> {
        indices.sort_unstable();
        indices.dedup();
        let baseline = read(&indices)?;
        let mut guest_msrs = guest_msrs.to_vec();
        guest_msrs.sort_unstable();
        guest_msrs.dedup();
        Ok(Self {
            baseline,
            guest_msrs,
        })
    }

    /// Every MSR index in the reset set.
    #[cfg(test)]
    pub fn reset_indices(&self) -> Vec<u32> {
        self.baseline.iter().map(|entry| entry.index).collect()
    }

    /// The MSR indices captured into a snapshot: the declared guest MSRs plus
    /// the fixed core, intersected with this VM's reset set. Restore scrubs the
    /// rest of the reset set to the destination baseline.
    pub fn persist_indices(&self) -> Vec<u32> {
        self.baseline
            .iter()
            .map(|entry| entry.index)
            .filter(|index| CORE_PERSIST_MSRS.contains(index) || self.guest_msrs.contains(index))
            .collect()
    }

    /// Resolves an untrusted snapshot's MSRs against the destination's
    /// declared set.
    ///
    /// Each supplied index must be a declared guest MSR or a core MSR this VM
    /// resets. That set is backend-independent, so the accept/reject contract
    /// is identical across backends, and a snapshot cannot carry a value the
    /// destination does not restore. Returns the entries to write:
    /// the snapshot's value for each reset index, or the baseline where the
    /// snapshot omits it.
    pub fn validate_snapshot(
        &self,
        snapshot: &[MsrEntry],
    ) -> Result<Vec<MsrEntry>, crate::hypervisor::virtual_machine::RegisterError> {
        use crate::hypervisor::virtual_machine::RegisterError;

        let restorable = self.persist_indices();
        for entry in snapshot {
            if !restorable.contains(&entry.index) {
                return Err(RegisterError::InvalidSnapshotMsrIndex { index: entry.index });
            }
        }

        Ok(self
            .baseline
            .iter()
            .map(|base| MsrEntry {
                index: base.index,
                value: snapshot
                    .iter()
                    .find(|entry| entry.index == base.index)
                    .map_or(base.value, |entry| entry.value),
            })
            .collect())
    }
}

pub(crate) const MSR_TSC: u32 = 0x10;
pub(crate) const MSR_TSC_ADJUST: u32 = 0x3B;
pub(crate) const MSR_SPEC_CTRL: u32 = 0x48;
pub(crate) const MSR_UMWAIT_CONTROL: u32 = 0xE1;
pub(crate) const MSR_MPERF: u32 = 0xE7;
pub(crate) const MSR_APERF: u32 = 0xE8;
pub(crate) const MSR_MTRR_CAP: u32 = 0xFE;
pub(crate) const MSR_TSX_CTRL: u32 = 0x122;
pub(crate) const MSR_SYSENTER_CS: u32 = 0x174;
pub(crate) const MSR_SYSENTER_ESP: u32 = 0x175;
pub(crate) const MSR_SYSENTER_EIP: u32 = 0x176;
pub(crate) const MSR_XFD: u32 = 0x1C4;
pub(crate) const MSR_XFD_ERR: u32 = 0x1C5;
pub(crate) const MSR_DEBUGCTL: u32 = 0x1D9;
pub(crate) const MSR_MTRR_FIX64K_00000: u32 = 0x250;
pub(crate) const MSR_PAT: u32 = 0x277;
pub(crate) const MSR_MTRR_DEF_TYPE: u32 = 0x2FF;
pub(crate) const MSR_U_CET: u32 = 0x6A0;
pub(crate) const MSR_S_CET: u32 = 0x6A2;
pub(crate) const MSR_PL0_SSP: u32 = 0x6A4;
pub(crate) const MSR_PL1_SSP: u32 = 0x6A5;
pub(crate) const MSR_PL2_SSP: u32 = 0x6A6;
pub(crate) const MSR_PL3_SSP: u32 = 0x6A7;
pub(crate) const MSR_INTERRUPT_SSP_TABLE_ADDR: u32 = 0x6A8;
pub(crate) const MSR_IA32_SSP: u32 = 0x7A0;
pub(crate) const MSR_TSC_DEADLINE: u32 = 0x6E0;
pub(crate) const MSR_BNDCFGS: u32 = 0xD90;
pub(crate) const MSR_XSS: u32 = 0xDA0;
pub(crate) const MSR_STAR: u32 = 0xC000_0081;
pub(crate) const MSR_LSTAR: u32 = 0xC000_0082;
pub(crate) const MSR_CSTAR: u32 = 0xC000_0083;
pub(crate) const MSR_SFMASK: u32 = 0xC000_0084;
pub(crate) const MSR_KERNEL_GS_BASE: u32 = 0xC000_0102;
pub(crate) const MSR_TSC_AUX: u32 = 0xC000_0103;
pub(crate) const MSR_VIRT_SPEC_CTRL: u32 = 0xC001_011F;

const HYPERV_VARIABLE_MTRR_COUNT: u8 = 16;

// Every guest-writable retained value must be host-readable and host-writable.
// EFER, APIC_BASE, FS_BASE, and GS_BASE are part of the special-register state.
// PRED_CMD (0x49) and FLUSH_CMD (0x10B) are intentionally absent: they are
// write-only commands with no retained state, so they cannot be reset and
// therefore cannot be declared.
const NON_MTRR_RESETTABLE_MSRS: &[u32] = &[
    MSR_SYSENTER_CS,
    MSR_SYSENTER_ESP,
    MSR_SYSENTER_EIP,
    // WHP exposes no writable DEBUGCTL bits under its default feature banks.
    MSR_DEBUGCTL,
    MSR_PAT,
    MSR_STAR,
    MSR_LSTAR,
    MSR_CSTAR,
    MSR_SFMASK,
    MSR_KERNEL_GS_BASE,
    MSR_SPEC_CTRL,
    // AMD virtualized SSBD control. Guest-writable only where the host exposes
    // the legacy VIRT_SPEC_CTRL SSBD mechanism. Host probing omits it otherwise.
    MSR_VIRT_SPEC_CTRL,
    // Host probing omits CET state when CET is unavailable.
    MSR_U_CET,
    MSR_S_CET,
    MSR_PL0_SSP,
    MSR_PL1_SSP,
    MSR_PL2_SSP,
    MSR_PL3_SSP,
    MSR_INTERRUPT_SSP_TABLE_ADDR,
    // MSHV maps XSS through its U_XSS alias.
    MSR_XSS,
    MSR_TSC,
    // Hyper-V stores TSC_ADJUST independently from TSC.
    MSR_TSC_ADJUST,
    // Host probing omits TSC_AUX when RDTSCP is unavailable.
    MSR_TSC_AUX,
    // Hyper-V exposes MPERF and APERF as per-VP counters.
    MSR_MPERF,
    MSR_APERF,
    // Host probing omits TSX_CTRL when TSX control is unavailable.
    MSR_TSX_CTRL,
    // Host probing omits XFD state when XFD is unavailable.
    MSR_XFD,
    MSR_XFD_ERR,
    // Feature MSRs enabled by default on capable hosts. Host probing omits
    // each when its feature is unavailable.
    MSR_UMWAIT_CONTROL,
    MSR_TSC_DEADLINE,
    MSR_BNDCFGS,
];

// State exposed through Hyper-V's VP register API, but not through an
// architectural RDMSR/WRMSR pair suitable for allow_msrs.
const HYPERV_ONLY_RESETTABLE_MSRS: &[u32] = &[MSR_IA32_SSP];

const MTRR_RESET_INDICES: &[u32] = &[
    // Hyper-V accepts fixed-MTRR writes even when MTRRCAP.FIX is clear.
    MSR_MTRR_DEF_TYPE,
    0x200,                 // PHYSBASE0
    0x201,                 // PHYSMASK0
    0x202,                 // PHYSBASE1
    0x203,                 // PHYSMASK1
    0x204,                 // PHYSBASE2
    0x205,                 // PHYSMASK2
    0x206,                 // PHYSBASE3
    0x207,                 // PHYSMASK3
    0x208,                 // PHYSBASE4
    0x209,                 // PHYSMASK4
    0x20A,                 // PHYSBASE5
    0x20B,                 // PHYSMASK5
    0x20C,                 // PHYSBASE6
    0x20D,                 // PHYSMASK6
    0x20E,                 // PHYSBASE7
    0x20F,                 // PHYSMASK7
    0x210,                 // PHYSBASE8
    0x211,                 // PHYSMASK8
    0x212,                 // PHYSBASE9
    0x213,                 // PHYSMASK9
    0x214,                 // PHYSBASEA
    0x215,                 // PHYSMASKA
    0x216,                 // PHYSBASEB
    0x217,                 // PHYSMASKB
    0x218,                 // PHYSBASEC
    0x219,                 // PHYSMASKC
    0x21A,                 // PHYSBASED
    0x21B,                 // PHYSMASKD
    0x21C,                 // PHYSBASEE
    0x21D,                 // PHYSMASKE
    0x21E,                 // PHYSBASEF
    0x21F,                 // PHYSMASKF
    MSR_MTRR_FIX64K_00000, // FIX64K_00000
    0x258,                 // FIX16K_80000
    0x259,                 // FIX16K_A0000
    0x268,                 // FIX4K_C0000
    0x269,                 // FIX4K_C8000
    0x26A,                 // FIX4K_D0000
    0x26B,                 // FIX4K_D8000
    0x26C,                 // FIX4K_E0000
    0x26D,                 // FIX4K_E8000
    0x26E,                 // FIX4K_F0000
    0x26F,                 // FIX4K_F8000
];

/// Whether an MSR carries retained state eligible for the reset set.
pub(crate) fn is_resettable_msr(index: u32) -> bool {
    NON_MTRR_RESETTABLE_MSRS.contains(&index) || is_mtrr_reset_index(index)
}

/// Returns non-MTRR candidates probed by filterless Hyper-V backends.
pub(crate) fn filterless_core_reset_candidates() -> impl Iterator<Item = u32> {
    NON_MTRR_RESETTABLE_MSRS
        .iter()
        .chain(HYPERV_ONLY_RESETTABLE_MSRS)
        .copied()
}

#[cfg(test)]
pub(crate) fn resettable_msr_indices() -> impl Iterator<Item = u32> {
    NON_MTRR_RESETTABLE_MSRS
        .iter()
        .chain(HYPERV_ONLY_RESETTABLE_MSRS)
        .chain(MTRR_RESET_INDICES)
        .copied()
}

pub(crate) fn hyperv_mtrr_reset_indices(
    mtrr_cap: u64,
) -> Result<Vec<u32>, crate::hypervisor::virtual_machine::CreateVmError> {
    use crate::hypervisor::virtual_machine::CreateVmError;

    let advertised = (mtrr_cap & 0xff) as u8;
    if advertised > HYPERV_VARIABLE_MTRR_COUNT {
        return Err(CreateVmError::UnexpectedVariableMtrrCount {
            advertised,
            maximum: HYPERV_VARIABLE_MTRR_COUNT,
        });
    }

    let mut indices = Vec::with_capacity(1 + usize::from(advertised) * 2 + 11);
    indices.push(MSR_MTRR_DEF_TYPE);
    indices.extend((0..u32::from(advertised) * 2).map(|offset| 0x200 + offset));
    indices.extend([
        MSR_MTRR_FIX64K_00000,
        0x258,
        0x259,
        0x268,
        0x269,
        0x26A,
        0x26B,
        0x26C,
        0x26D,
        0x26E,
        0x26F,
    ]);
    Ok(indices)
}

pub(crate) fn is_mtrr_reset_index(index: u32) -> bool {
    MTRR_RESET_INDICES.contains(&index)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::hypervisor::virtual_machine::{CreateVmError, RegisterError};

    fn state() -> MsrResetState {
        MsrResetState {
            baseline: vec![
                MsrEntry {
                    index: MSR_SYSENTER_CS,
                    value: 0x1C,
                },
                MsrEntry {
                    index: MSR_SYSENTER_ESP,
                    value: 0x2C,
                },
                MsrEntry {
                    index: MSR_KERNEL_GS_BASE,
                    value: 0x3C,
                },
            ],
            guest_msrs: vec![MSR_SYSENTER_CS, MSR_SYSENTER_ESP],
        }
    }

    #[test]
    fn hyperv_mtrr_indices_follow_guest_capability() {
        assert_eq!(
            hyperv_mtrr_reset_indices(2).unwrap(),
            [
                MSR_MTRR_DEF_TYPE,
                0x200,
                0x201,
                0x202,
                0x203,
                MSR_MTRR_FIX64K_00000,
                0x258,
                0x259,
                0x268,
                0x269,
                0x26A,
                0x26B,
                0x26C,
                0x26D,
                0x26E,
                0x26F,
            ]
        );
        let fixed_bit_does_not_change_reset_set = hyperv_mtrr_reset_indices(2 | (1 << 8)).unwrap();
        assert_eq!(
            fixed_bit_does_not_change_reset_set,
            hyperv_mtrr_reset_indices(2).unwrap()
        );
    }

    #[test]
    fn hyperv_mtrr_count_rejects_more_than_sixteen_pairs() {
        let indices = hyperv_mtrr_reset_indices(16).unwrap();
        assert!(indices.contains(&0x21F));
        assert_eq!(indices.last(), Some(&0x26F));
        assert!(indices.iter().all(|&index| is_resettable_msr(index)));
        assert!(matches!(
            hyperv_mtrr_reset_indices(17),
            Err(CreateVmError::UnexpectedVariableMtrrCount {
                advertised: 17,
                maximum: 16
            })
        ));
    }

    #[test]
    fn snapshot_msr_validation_accepts_exact_canonical_set() {
        let supplied = vec![
            MsrEntry {
                index: MSR_SYSENTER_CS,
                value: 1,
            },
            MsrEntry {
                index: MSR_SYSENTER_ESP,
                value: 2,
            },
            MsrEntry {
                index: MSR_KERNEL_GS_BASE,
                value: 3,
            },
        ];

        assert_eq!(state().validate_snapshot(&supplied).unwrap(), supplied);
    }

    #[test]
    fn snapshot_msr_validation_baselines_omitted_indices() {
        // A snapshot covering a subset of this VM's reset set is accepted.
        // The omitted index takes the creation-time baseline value.
        let supplied = vec![
            MsrEntry {
                index: MSR_SYSENTER_CS,
                value: 1,
            },
            MsrEntry {
                index: MSR_KERNEL_GS_BASE,
                value: 3,
            },
        ];

        assert_eq!(
            state().validate_snapshot(&supplied).unwrap(),
            vec![
                MsrEntry {
                    index: MSR_SYSENTER_CS,
                    value: 1
                },
                MsrEntry {
                    index: MSR_SYSENTER_ESP,
                    value: 0x2C
                },
                MsrEntry {
                    index: MSR_KERNEL_GS_BASE,
                    value: 3
                },
            ]
        );
    }

    #[test]
    fn snapshot_msr_validation_rejects_index_outside_reset_set() {
        let supplied = vec![
            MsrEntry {
                index: MSR_SYSENTER_CS,
                value: 1,
            },
            MsrEntry {
                index: MSR_SYSENTER_ESP,
                value: 2,
            },
            MsrEntry {
                index: MSR_PAT,
                value: 3,
            },
        ];

        assert!(matches!(
            state().validate_snapshot(&supplied),
            Err(RegisterError::InvalidSnapshotMsrIndex { index }) if index == MSR_PAT
        ));
    }

    #[test]
    fn snapshot_msr_validation_rejects_reset_index_that_is_not_declared() {
        // PAT is in the reset set but neither core nor declared, so a snapshot
        // cannot override it even though the backend resets it. This keeps the
        // accept/reject contract identical on KVM and the Hyper-V backends,
        // whose reset sets differ in size.
        let state = MsrResetState {
            baseline: vec![
                MsrEntry {
                    index: MSR_PAT,
                    value: 1,
                },
                MsrEntry {
                    index: MSR_KERNEL_GS_BASE,
                    value: 2,
                },
                MsrEntry {
                    index: MSR_SYSENTER_CS,
                    value: 3,
                },
            ],
            guest_msrs: vec![MSR_SYSENTER_CS],
        };
        assert!(matches!(
            state.validate_snapshot(&[MsrEntry {
                index: MSR_PAT,
                value: 9,
            }]),
            Err(RegisterError::InvalidSnapshotMsrIndex { index }) if index == MSR_PAT
        ));
        // A declared MSR and a core MSR are both accepted.
        assert!(
            state
                .validate_snapshot(&[
                    MsrEntry {
                        index: MSR_SYSENTER_CS,
                        value: 9,
                    },
                    MsrEntry {
                        index: MSR_KERNEL_GS_BASE,
                        value: 8,
                    },
                ])
                .is_ok()
        );
    }

    #[test]
    fn snapshot_msr_validation_accepts_empty_canonical_set() {
        let state = MsrResetState {
            baseline: Vec::new(),
            guest_msrs: Vec::new(),
        };
        assert!(state.validate_snapshot(&[]).unwrap().is_empty());
    }

    #[test]
    fn persist_indices_covers_guest_msrs_and_core_only() {
        let state = MsrResetState {
            baseline: vec![
                MsrEntry {
                    index: MSR_PAT,
                    value: 1,
                },
                MsrEntry {
                    index: MSR_KERNEL_GS_BASE,
                    value: 2,
                },
                MsrEntry {
                    index: MSR_SYSENTER_CS,
                    value: 3,
                },
            ],
            guest_msrs: vec![MSR_SYSENTER_CS],
        };
        // PAT is neither core nor declared, so it is scrubbed, not saved.
        assert_eq!(
            state.persist_indices(),
            vec![MSR_KERNEL_GS_BASE, MSR_SYSENTER_CS]
        );
    }
}