@@ -103,6 +103,42 @@ impl Hypervisor {
103103 }
104104}
105105
106+ #[ derive( Clone , Debug , PartialEq , Eq , Serialize , Deserialize ) ]
107+ pub ( super ) struct CpuVendor ( String ) ;
108+
109+ impl CpuVendor {
110+ /// The vendor identifier of the running host.
111+ pub ( super ) fn current ( ) -> Self {
112+ #[ cfg( target_arch = "x86_64" ) ]
113+ {
114+ // SAFETY: CPUID leaf 0 is always available on x86_64.
115+ // TODO: Remove the `unsafe`/allow when MSRV is raised above
116+ // 1.89. On Rust 1.89 `__cpuid` requires `unsafe`; on newer
117+ // compilers it is safe and clippy flags it as unnecessary.
118+ #[ allow( unused_unsafe) ]
119+ let r = unsafe { core:: arch:: x86_64:: __cpuid ( 0 ) } ;
120+ let mut bytes = [ 0u8 ; 12 ] ;
121+ bytes[ 0 ..4 ] . copy_from_slice ( & r. ebx . to_le_bytes ( ) ) ;
122+ bytes[ 4 ..8 ] . copy_from_slice ( & r. edx . to_le_bytes ( ) ) ;
123+ bytes[ 8 ..12 ] . copy_from_slice ( & r. ecx . to_le_bytes ( ) ) ;
124+ Self ( String :: from_utf8_lossy ( & bytes) . into_owned ( ) )
125+ }
126+ #[ cfg( all( target_arch = "aarch64" , target_os = "linux" ) ) ]
127+ {
128+ let midr: u64 ;
129+ // SAFETY: Linux emulates MIDR_EL1 reads from EL0.
130+ unsafe { core:: arch:: asm!( "mrs {}, MIDR_EL1" , out( reg) midr) } ;
131+ let implementer = ( midr >> 24 ) & 0xff ;
132+ // `0x` prefix padded to width 4, e.g. Apple `0x61`, Arm `0x41`.
133+ Self ( format ! ( "{implementer:#04x}" ) )
134+ }
135+ }
136+
137+ pub ( super ) fn as_str ( & self ) -> & str {
138+ & self . 0
139+ }
140+ }
141+
106142// --- Config JSON shape ----------------------------------------------
107143
108144/// Top-level Hyperlight snapshot config JSON. Lives at
@@ -123,6 +159,8 @@ pub(super) struct OciSnapshotConfig {
123159 /// Memory blob ABI version. See `SNAPSHOT_ABI_VERSION`.
124160 pub ( super ) abi_version : u32 ,
125161 pub ( super ) hypervisor : Hypervisor ,
162+ /// CPU vendor captured at snapshot time. Checked on load.
163+ pub ( super ) cpu_vendor : CpuVendor ,
126164 /// Top of the guest stack, in guest virtual address space.
127165 pub ( super ) stack_top_gva : u64 ,
128166 /// Guest virtual address the loader resumes the paused call at.
@@ -331,6 +369,16 @@ impl OciSnapshotConfig {
331369 self . hyperlight_version
332370 ) ) ;
333371 }
372+ let current_vendor = CpuVendor :: current ( ) ;
373+ if self . cpu_vendor != current_vendor {
374+ return Err ( crate :: new_error!(
375+ "snapshot CPU vendor mismatch: file was created on {} but the current CPU is {} \
376+ (snapshot produced by hyperlight {})",
377+ self . cpu_vendor. as_str( ) ,
378+ current_vendor. as_str( ) ,
379+ self . hyperlight_version
380+ ) ) ;
381+ }
334382 // Bound memory size early so the subsequent file-size check
335383 // does not have to deal with absurd values.
336384 if self . memory_size == 0 || self . memory_size > SandboxMemoryLayout :: MAX_MEMORY_SIZE as u64 {
@@ -574,4 +622,23 @@ mod tests {
574622 assert_eq ! ( back, r, "return type {:?} did not round-trip" , r) ;
575623 }
576624 }
625+
626+ /// `CpuVendor::current` returns the expected host vendor. Ignored
627+ /// by default and run explicitly in CI, where the runner hardware
628+ /// is known. Extend the allowlist when new runner hardware is
629+ /// added.
630+ #[ test]
631+ #[ ignore = "hardware-specific; run explicitly in CI" ]
632+ fn cpu_vendor_current_is_recognized ( ) {
633+ let vendor = CpuVendor :: current ( ) ;
634+ let v = vendor. as_str ( ) ;
635+ #[ cfg( target_arch = "x86_64" ) ]
636+ assert ! (
637+ matches!( v, "GenuineIntel" | "AuthenticAMD" ) ,
638+ "unrecognized x86_64 CPU vendor: {v:?}"
639+ ) ;
640+ #[ cfg( all( target_arch = "aarch64" , target_os = "linux" ) ) ]
641+ // MIDR_EL1 implementer byte for Apple silicon.
642+ assert_eq ! ( v, "0x61" , "unexpected aarch64 CPU implementer" ) ;
643+ }
577644}
0 commit comments