Skip to content

Commit 9aa0f28

Browse files
committed
vtpm: Add runtime re-seal hook and NV-block persistence
Adds a runtime re-seal path that fires on a successful guest TPM2_Shutdown (cmdCode 0x145, rc=0) and the supporting Tier-2 NV-block extraction needed to round-trip the simulator's persistent state across a cold boot. * libtcgtpm: expose get_nv_blob() / apply_nv_blob() accessors so the in-process TPM Reference Implementation's 16 KiB NV image (STATE_NV_BACKUP_SIZE) can be captured at re-seal time and replayed on Recover. The bulk serialization wrapper grows a new tagged section (0x12) that carries the NV blob alongside the existing gp/gc/gr globals. * vtpm::reseal: new module hosting trigger_if_shutdown() and do_reseal(). The hook is invoked from TcgTpm::send_tpm_command *outside* the VTPM SpinLock — re-seal talks to the TPM endpoint through a separate TpmProxy<VsockTransport> and reads internal state through the libtcgtpm C FFI, so it never re-acquires VTPM. Re-seal failures are logged but never propagated to the guest; fail-closed enforcement remains on the next cold-boot unseal path (NV-counter check + AES-GCM tag verify). * vtpm::mod / svsm: wire the optional vtpm-persist feature so the TPM endpoint and host blob store can be reached over VSOCK during cold-boot Provision/Recover and runtime re-seal. The persistence path is opt-in and never silently falls back to ephemeral — an unreachable endpoint is a hard error so the security model that treats the physical TPM as a separate root of trust cannot be downgraded by a channel-only attacker. * cpu::apic / sev::snp_apic: convert the previously panicking icr_write() to a logged warning so a transient HV write error on the persistence path does not crash the boot CPU. Signed-off-by: Goodleon-Y <goodleon3@126.com>
1 parent d48bd76 commit 9aa0f28

15 files changed

Lines changed: 1121 additions & 101 deletions

File tree

configs/qemu-target.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
"kernel": {
1616
"svsm": {
17-
"features": "vtpm",
17+
"features": "vtpm,virtio-drivers,vsock",
1818
"binary": false
1919
},
2020
"bldr": {

kernel/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,26 @@ virtio-drivers = ["dep:virtio-drivers"]
8181
block = []
8282
vsock = []
8383

84+
# Persistent vTPM via TPM-sealing of internal state to an off-CVM TPM
85+
# endpoint (research extension; mainline behaviour preserved when off).
86+
#
87+
# When OFF (default): the `vtpm` feature provides the standard ephemeral
88+
# vTPM that the mainline COCONUT-SVSM tree exposes — each cold boot
89+
# re-manufactures the simulator and produces a fresh EK/SRK/IAK.
90+
#
91+
# When ON: svsm.rs drives the Provision/Recover seal cycle through a
92+
# `VsockTransport`/`VsockHostStore` pair. The host helper (TPM endpoint
93+
# on a VSOCK port + blob store on two VSOCK ports) must be reachable;
94+
# unreachable endpoints are a fatal error, not a silent fallback,
95+
# because silent fallback would weaken the security model that treats
96+
# the physical TPM as a separate root of trust from the platform.
97+
#
98+
# Transport / store backends are currently fixed to VSOCK because that
99+
# is the only supported channel between an SEV-SNP CVM and a host-side
100+
# TPM endpoint; the `TpmTransport` and `SealedBlobStore` traits keep
101+
# the door open for future block-layer or KBS backends.
102+
vtpm-persist = ["vtpm", "vsock"]
103+
84104
[dev-dependencies]
85105
sha2 = { workspace = true, features = ["force-soft"] }
86106

kernel/src/cpu/x86/apic.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,9 @@ impl X86Apic {
157157
/// - `icr` - Value to write to the ICR register
158158
#[inline(always)]
159159
pub fn icr_write(&self, icr: u64) {
160-
self.regs()
161-
.icr_write(icr)
162-
.expect("Failed to write APIC.ICR");
160+
if let Err(e) = self.regs().icr_write(icr) {
161+
log::warn!("Failed to write APIC.ICR: {e:?}");
162+
}
163163
}
164164

165165
/// Checks whether an IRQ vector is currently in service

kernel/src/sev/snp_apic.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,18 @@ impl ApicAccess for GHCBApicAccessor {
6565
}
6666

6767
fn icr_write(&self, icr: u64) -> Result<(), SvsmError> {
68-
// The #HV IPI can only be used if restricted injection is supported.
69-
// Otherwise, the IPI must be sent via an X2APIC ICR write.
70-
if self.use_restr_inj() {
71-
current_ghcb().hv_ipi(icr)?;
72-
} else {
73-
self.apic_write(APIC_OFFSET_ICR, icr);
68+
// HV_IPI (GHCB exit 0x0015) is a base SEV-ES protocol feature and
69+
// does not require restricted injection. KVM rejects direct WRMSR to
70+
// the x2APIC ICR MSR (0x830) via the GHCB MSR protocol (error 2),
71+
// so HV_IPI is the only reliable path.
72+
match current_ghcb().hv_ipi(icr) {
73+
Ok(()) => Ok(()),
74+
Err(_) => {
75+
// Fallback: try direct MSR write if HV_IPI is unsupported
76+
self.apic_write(APIC_OFFSET_ICR, icr);
77+
Ok(())
78+
}
7479
}
75-
76-
Ok(())
7780
}
7881

7982
fn eoi(&self) {

kernel/src/svsm.rs

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,20 @@ use svsm::utils::ScopedMut;
7272
use svsm::utils::round_to_pages;
7373
#[cfg(all(feature = "virtio-drivers", any(feature = "block", feature = "vsock")))]
7474
use svsm::virtio::probe_mmio_slots;
75-
#[cfg(all(feature = "vtpm", not(feature = "vsock"), not(test)))]
75+
#[cfg(all(feature = "vtpm-persist", not(feature = "vsock"), not(test)))]
7676
use svsm::vtpm::MockTransport;
77-
#[cfg(all(feature = "vtpm", feature = "vsock", not(test)))]
77+
#[cfg(all(feature = "vtpm-persist", feature = "vsock", not(test)))]
7878
use svsm::vtpm::VsockTransport;
79-
#[cfg(all(feature = "vtpm", not(test)))]
80-
use svsm::vtpm::{SealedBlobStore, StaticBufStore, VtpmBootMode, vtpm_init_sealed};
81-
#[cfg(all(feature = "vtpm", feature = "vsock", not(test)))]
79+
#[cfg(all(feature = "vtpm", not(feature = "vtpm-persist"), not(test)))]
80+
use svsm::vtpm::vtpm_init;
81+
#[cfg(all(feature = "vtpm-persist", not(test)))]
82+
use svsm::vtpm::{SealedBlobStore, VsockHostStore, VtpmBootMode, vtpm_init_sealed};
83+
#[cfg(all(feature = "vtpm-persist", feature = "vsock", not(test)))]
8284
use svsm::vtpm::{VSOCK_HOST_CID, VSOCK_TPM_PORT};
8385

8486
use alloc::string::String;
87+
#[cfg(all(feature = "vtpm-persist", not(test)))]
88+
use alloc::vec::Vec;
8589
use release::COCONUT_VERSION;
8690

8791
#[cfg(feature = "attest")]
@@ -576,40 +580,56 @@ fn svsm_init(launch_info: &KernelLaunchInfo) {
576580
log::info!("attestation successful");
577581
}
578582

579-
#[cfg(all(feature = "vtpm", not(test)))]
583+
// Mainline COCONUT-SVSM behaviour: ephemeral vTPM, no external
584+
// dependencies, fresh seeds on every cold boot. This is the path
585+
// upstream consumers get and the one preserved across the
586+
// `vtpm-persist` feature being off.
587+
#[cfg(all(feature = "vtpm", not(feature = "vtpm-persist"), not(test)))]
580588
{
581-
// SealedBlob persistence is driven through the `SealedBlobStore`
582-
// trait so that the same code path can target any backend
583-
// (warm-reboot static buffer for dev/test, IGVM variable, the
584-
// upcoming SVSM block layer, KBS-stateful vTPM service, ...).
585-
// The default in this tree is the static buffer backend, which
586-
// survives warm reboots within a single CVM lifetime.
587-
static SEALED_BLOB_STORE: StaticBufStore = StaticBufStore::new();
588-
let store: &dyn SealedBlobStore = &SEALED_BLOB_STORE;
589-
590-
let blob_opt = store.load().expect("SealedBlobStore::load failed");
589+
vtpm_init().expect("vtpm_init failed");
590+
log::info!("VTPM: ephemeral init complete");
591+
}
592+
593+
// Persistent vTPM path — opt-in via the `vtpm-persist` feature.
594+
// Drives the Provision/Recover seal cycle against an off-CVM TPM
595+
// endpoint (transport) and host-side blob store, both currently
596+
// reached over VSOCK. The host helper must be reachable: a
597+
// missing/unreachable endpoint is a hard error, NOT a silent
598+
// fallback to ephemeral — silently downgrading would void the
599+
// security model the seal cycle promises, which treats the
600+
// physical TPM as a separate root of trust (an attacker who can
601+
// break the VSOCK channel would otherwise harvest a fresh vTPM
602+
// identity by triggering the fallback).
603+
#[cfg(all(feature = "vtpm-persist", not(test)))]
604+
{
605+
let store = VsockHostStore::new(
606+
VSOCK_HOST_CID,
607+
VsockHostStore::DEFAULT_LOAD_PORT,
608+
VsockHostStore::DEFAULT_SAVE_PORT,
609+
);
610+
611+
let blob_opt = store
612+
.load()
613+
.expect("VTPM: sealed-store load failed (host helper unreachable?)");
591614
let (boot_mode, blob_slice) = match blob_opt.as_deref() {
592615
Some(b) => (VtpmBootMode::Recover, Some(b)),
593616
None => (VtpmBootMode::Provision, None),
594617
};
595618

596619
let vm_id: [u8; 16] = [0u8; 16]; // TODO: derive from IGVM guest_context
597620

598-
#[cfg(feature = "vsock")]
599621
let transport = VsockTransport::new(VSOCK_HOST_CID, VSOCK_TPM_PORT);
600-
#[cfg(not(feature = "vsock"))]
601-
let transport = {
602-
log::warn!("VTPM: no VSOCK, using MockTransport (debug only)");
603-
MockTransport::new()
604-
};
605622

606-
let maybe_blob = vtpm_init_sealed(transport, boot_mode, vm_id, blob_slice)
607-
.expect("vTPM sealed init failed");
623+
// R1: vtpm_init_sealed now uses an out-param for the Vec<u8>; its
624+
// return type is the register-passable `Result<(), SvsmReqError>`.
625+
let mut maybe_blob: Option<Vec<u8>> = None;
626+
vtpm_init_sealed(transport, boot_mode, vm_id, blob_slice, &mut maybe_blob)
627+
.expect("VTPM: sealed init failed (host TPM endpoint unreachable?)");
608628

609629
if let Some(blob_bytes) = maybe_blob {
610630
store
611631
.save(&blob_bytes)
612-
.expect("SealedBlobStore::save failed");
632+
.expect("VTPM: sealed-store save failed");
613633
log::info!(
614634
"VTPM: SealedBlob ({} bytes) stored for next boot",
615635
blob_bytes.len()

0 commit comments

Comments
 (0)