@@ -36,6 +36,10 @@ pub struct VmDiagnostics {
3636 pub cpuid_hypervisor_vendor : Option < String > ,
3737 /// Whether the Hyper-V guest integration services registry key exists.
3838 pub hyperv_guest_integration : bool ,
39+ /// VM guest services found in the Windows service registry.
40+ pub guest_services_found : Vec < String > ,
41+ /// Display adapter description from the GPU class registry.
42+ pub display_adapter : Option < String > ,
3943 /// Architecture of the running process.
4044 pub arch : & ' static str ,
4145}
@@ -79,6 +83,8 @@ pub fn collect_vm_diagnostics() -> VmDiagnostics {
7983 registry_values : vec ! [ ] ,
8084 cpuid_hypervisor_vendor : None ,
8185 hyperv_guest_integration : false ,
86+ guest_services_found : vec ! [ ] ,
87+ display_adapter : None ,
8288 arch : std:: env:: consts:: ARCH ,
8389 }
8490 }
@@ -174,8 +180,9 @@ fn collect_vm_diagnostics_windows() -> VmDiagnostics {
174180 . collect ( ) ;
175181
176182 let hyperv_guest = hyperv_guest_parameters_exist ( ) ;
177-
178183 let cpuid_vendor = cpuid_hypervisor_vendor ( ) ;
184+ let guest_services = detect_guest_services ( ) ;
185+ let display_adapter = detect_display_adapter ( ) ;
179186
180187 // Build the joined string from identity-relevant registry values for vm_string_signal.
181188 // Use only the first 9 values (manufacturer/product/bios strings, not disk/processor).
@@ -191,118 +198,202 @@ fn collect_vm_diagnostics_windows() -> VmDiagnostics {
191198 . find ( |( label, _) | label == "Disk\\ Enum\\ 0" )
192199 . and_then ( |( _, v) | v. as_deref ( ) ) ;
193200
201+ let make_result = |vm_detected : bool , detection_reason : String | VmDiagnostics {
202+ vm_detected,
203+ detection_reason,
204+ registry_values : registry_values. clone ( ) ,
205+ cpuid_hypervisor_vendor : cpuid_vendor. clone ( ) ,
206+ hyperv_guest_integration : hyperv_guest,
207+ guest_services_found : guest_services. clone ( ) ,
208+ display_adapter : display_adapter. clone ( ) ,
209+ arch : std:: env:: consts:: ARCH ,
210+ } ;
211+
194212 // --- Detection logic ---
195213
196214 // 1. Registry identity strings (manufacturer, product, BIOS, baseboard)
197215 if vm_string_signal ( & identity_joined) {
198- return VmDiagnostics {
199- vm_detected : true ,
200- detection_reason : format ! ( "registry VM marker: {identity_joined}" ) ,
201- registry_values,
202- cpuid_hypervisor_vendor : cpuid_vendor,
203- hyperv_guest_integration : hyperv_guest,
204- arch : std:: env:: consts:: ARCH ,
205- } ;
216+ return make_result ( true , format ! ( "registry VM marker: {identity_joined}" ) ) ;
206217 }
207218
208219 // 2. Disk device name (virtual disk controllers)
209220 if let Some ( disk) = disk_device {
210221 if vm_string_signal ( disk) {
211- return VmDiagnostics {
212- vm_detected : true ,
213- detection_reason : format ! ( "virtual disk device: {disk}" ) ,
214- registry_values,
215- cpuid_hypervisor_vendor : cpuid_vendor,
216- hyperv_guest_integration : hyperv_guest,
217- arch : std:: env:: consts:: ARCH ,
218- } ;
222+ return make_result ( true , format ! ( "virtual disk device: {disk}" ) ) ;
223+ }
224+ }
225+
226+ // 3. VM guest services present (vmicheartbeat, VBoxGuest, vmci, etc.)
227+ if !guest_services. is_empty ( ) {
228+ let svc_list = guest_services. join ( ", " ) ;
229+ return make_result ( true , format ! ( "VM guest services installed: {svc_list}" ) ) ;
230+ }
231+
232+ // 4. Virtual display adapter
233+ if let Some ( ref adapter) = display_adapter {
234+ if vm_display_signal ( adapter) {
235+ return make_result ( true , format ! ( "virtual display adapter: {adapter}" ) ) ;
219236 }
220237 }
221238
222- // 3 . CPUID hypervisor vendor
239+ // 5 . CPUID hypervisor vendor
223240 if let Some ( ref vendor) = cpuid_vendor {
224241 if vm_string_signal ( vendor) && !vendor. eq_ignore_ascii_case ( "Microsoft Hv" ) {
225- return VmDiagnostics {
226- vm_detected : true ,
227- detection_reason : format ! ( "CPUID hypervisor vendor: {vendor}" ) ,
228- registry_values,
229- cpuid_hypervisor_vendor : cpuid_vendor,
230- hyperv_guest_integration : hyperv_guest,
231- arch : std:: env:: consts:: ARCH ,
232- } ;
242+ return make_result ( true , format ! ( "CPUID hypervisor vendor: {vendor}" ) ) ;
233243 }
234244 // "Microsoft Hv" — reported by both VBS on physical hardware and Hyper-V guests.
235245 if vendor. eq_ignore_ascii_case ( "Microsoft Hv" ) {
236- // 4 . Microsoft Hv + "Microsoft Corporation" manufacturer = Hyper-V guest
246+ // 6 . Microsoft Hv + "Microsoft Corporation" manufacturer = Hyper-V guest
237247 if identity_joined
238248 . to_ascii_lowercase ( )
239249 . contains ( "microsoft corporation" )
240250 {
241- return VmDiagnostics {
242- vm_detected : true ,
243- detection_reason : format ! (
251+ return make_result (
252+ true ,
253+ format ! (
244254 "Hyper-V guest: Microsoft Hv CPUID + Microsoft Corporation manufacturer ({identity_joined})"
245255 ) ,
246- registry_values,
247- cpuid_hypervisor_vendor : cpuid_vendor,
248- hyperv_guest_integration : hyperv_guest,
249- arch : std:: env:: consts:: ARCH ,
250- } ;
256+ ) ;
251257 }
252- // 5. Microsoft Hv + Hyper-V guest integration services = VDI on Hyper-V
253- // (catches CyberArk, Citrix, etc. with non-standard manufacturer)
258+ // 7. Microsoft Hv + Hyper-V guest integration services = VDI on Hyper-V
254259 if hyperv_guest {
255- return VmDiagnostics {
256- vm_detected : true ,
257- detection_reason : format ! (
260+ return make_result (
261+ true ,
262+ format ! (
258263 "Hyper-V VDI: Microsoft Hv CPUID + guest integration services present ({identity_joined})"
259264 ) ,
260- registry_values,
261- cpuid_hypervisor_vendor : cpuid_vendor,
262- hyperv_guest_integration : hyperv_guest,
263- arch : std:: env:: consts:: ARCH ,
264- } ;
265+ ) ;
265266 }
266- return VmDiagnostics {
267- vm_detected : false ,
268- detection_reason : format ! (
267+ return make_result (
268+ false ,
269+ format ! (
269270 "hypervisor bit set without VM indicators: {vendor} (manufacturer: {identity_joined})"
270271 ) ,
271- registry_values,
272- cpuid_hypervisor_vendor : cpuid_vendor,
273- hyperv_guest_integration : hyperv_guest,
274- arch : std:: env:: consts:: ARCH ,
275- } ;
272+ ) ;
276273 }
277274 }
278275
279- // 6 . No CPUID hypervisor, but check Hyper-V guest integration (ARM64 path)
276+ // 8 . No CPUID hypervisor, but check Hyper-V guest integration (ARM64 path)
280277 if hyperv_guest {
281- return VmDiagnostics {
282- vm_detected : true ,
283- detection_reason : format ! (
278+ return make_result (
279+ true ,
280+ format ! (
284281 "Hyper-V guest integration services present without CPUID hypervisor ({identity_joined})"
285282 ) ,
286- registry_values,
287- cpuid_hypervisor_vendor : cpuid_vendor,
288- hyperv_guest_integration : hyperv_guest,
289- arch : std:: env:: consts:: ARCH ,
290- } ;
283+ ) ;
291284 }
292285
293- VmDiagnostics {
294- vm_detected : false ,
295- detection_reason : if identity_joined. is_empty ( ) {
296- "no VM registry markers, no CPUID hypervisor vendor, no Hyper-V guest integration"
297- . into ( )
286+ make_result (
287+ false ,
288+ if identity_joined. is_empty ( ) {
289+ "no VM indicators detected (no registry markers, no CPUID hypervisor, no guest services, no virtual display)" . into ( )
298290 } else {
299291 format ! ( "no VM indicators detected: {identity_joined}" )
300292 } ,
301- registry_values,
302- cpuid_hypervisor_vendor : cpuid_vendor,
303- hyperv_guest_integration : hyperv_guest,
304- arch : std:: env:: consts:: ARCH ,
293+ )
294+ }
295+
296+ /// Check for known VM guest service drivers in the Windows service registry.
297+ /// These services are only installed by hypervisor guest tools — never on bare metal.
298+ #[ cfg( target_os = "windows" ) ]
299+ fn detect_guest_services ( ) -> Vec < String > {
300+ use windows:: Win32 :: System :: Registry :: { RegCloseKey , RegOpenKeyExW , HKEY , KEY_READ } ;
301+
302+ const VM_SERVICES : & [ ( & str , & str ) ] = & [
303+ // Hyper-V guest integration
304+ ( "vmicheartbeat" , "Hyper-V Heartbeat" ) ,
305+ ( "vmicshutdown" , "Hyper-V Shutdown" ) ,
306+ ( "vmickvpexchange" , "Hyper-V KVP Exchange" ) ,
307+ ( "vmicguestinterface" , "Hyper-V Guest Service Interface" ) ,
308+ ( "vmicvss" , "Hyper-V VSS" ) ,
309+ ( "vmictimesync" , "Hyper-V Time Sync" ) ,
310+ // VMware Tools
311+ ( "vmci" , "VMware VMCI" ) ,
312+ ( "vmhgfs" , "VMware Host-Guest Filesystem" ) ,
313+ ( "vmxnet" , "VMware vmxnet" ) ,
314+ ( "vmxnet3" , "VMware vmxnet3" ) ,
315+ ( "vmvss" , "VMware VSS" ) ,
316+ ( "VMTools" , "VMware Tools" ) ,
317+ // VirtualBox Guest Additions
318+ ( "VBoxGuest" , "VirtualBox Guest" ) ,
319+ ( "VBoxSF" , "VirtualBox Shared Folders" ) ,
320+ ( "VBoxMouse" , "VirtualBox Mouse" ) ,
321+ ( "VBoxVideo" , "VirtualBox Video" ) ,
322+ // KVM/virtio (Red Hat)
323+ ( "vioscsi" , "virtio SCSI" ) ,
324+ ( "viostor" , "virtio Storage" ) ,
325+ ( "netkvm" , "virtio Network" ) ,
326+ ( "vioinput" , "virtio Input" ) ,
327+ ( "vioser" , "virtio Serial" ) ,
328+ ( "balloon" , "virtio Balloon" ) ,
329+ // QEMU Guest Agent
330+ ( "QEMU-GA" , "QEMU Guest Agent" ) ,
331+ ( "qemu-ga" , "QEMU Guest Agent" ) ,
332+ // Parallels
333+ ( "prl_strg" , "Parallels Storage" ) ,
334+ ( "prl_tg" , "Parallels Tools Gate" ) ,
335+ ( "prl_eth" , "Parallels Network" ) ,
336+ // Xen
337+ ( "xenevtchn" , "Xen Event Channel" ) ,
338+ ( "xenvbd" , "Xen Block Device" ) ,
339+ ( "xennet" , "Xen Network" ) ,
340+ ( "xenvif" , "Xen Virtual Interface" ) ,
341+ ] ;
342+
343+ let mut found = Vec :: new ( ) ;
344+ for ( svc_name, label) in VM_SERVICES {
345+ let subkey = format ! ( "SYSTEM\\ CurrentControlSet\\ Services\\ {svc_name}" ) ;
346+ let subkey_wide = wide_null ( & subkey) ;
347+ let mut hkey = HKEY :: default ( ) ;
348+ // SAFETY: Standard registry probe — open for read and immediately close.
349+ let status = unsafe {
350+ RegOpenKeyExW (
351+ HKEY_LOCAL_MACHINE ,
352+ windows:: core:: PCWSTR ( subkey_wide. as_ptr ( ) ) ,
353+ Some ( 0 ) ,
354+ KEY_READ ,
355+ & mut hkey,
356+ )
357+ } ;
358+ if status. is_ok ( ) {
359+ unsafe {
360+ let _ = RegCloseKey ( hkey) ;
361+ }
362+ found. push ( format ! ( "{svc_name} ({label})" ) ) ;
363+ }
305364 }
365+ found
366+ }
367+
368+ /// Read the primary display adapter description from the GPU class registry.
369+ #[ cfg( target_os = "windows" ) ]
370+ fn detect_display_adapter ( ) -> Option < String > {
371+ // The display adapter class GUID is {4d36e968-e325-11ce-bfc1-08002be10318}.
372+ // Subkey \0000 is the primary adapter.
373+ registry_string (
374+ "SYSTEM\\ CurrentControlSet\\ Control\\ Class\\ {4d36e968-e325-11ce-bfc1-08002be10318}\\ 0000" ,
375+ "DriverDesc" ,
376+ )
377+ }
378+
379+ /// Check if a display adapter description indicates a virtual GPU.
380+ #[ cfg_attr( not( target_os = "windows" ) , allow( dead_code) ) ]
381+ fn vm_display_signal ( adapter : & str ) -> bool {
382+ let lower = adapter. to_ascii_lowercase ( ) ;
383+ [
384+ "microsoft hyper-v video" ,
385+ "vmware svga" ,
386+ "vmware soda" ,
387+ "virtualbox graphics" ,
388+ "red hat qxl" ,
389+ "virtio gpu" ,
390+ "citrix indirect display" ,
391+ "parallels display" ,
392+ "qxl" ,
393+ "xen display" ,
394+ ]
395+ . iter ( )
396+ . any ( |needle| lower. contains ( needle) )
306397}
307398
308399#[ cfg_attr( not( target_os = "windows" ) , allow( dead_code) ) ]
@@ -526,6 +617,23 @@ mod tests {
526617 assert ! ( !vm_string_signal( "Seagate Barracuda" ) ) ;
527618 }
528619
620+ #[ test]
621+ fn vm_display_signal_detects_virtual_adapters ( ) {
622+ assert ! ( vm_display_signal( "Microsoft Hyper-V Video" ) ) ;
623+ assert ! ( vm_display_signal( "VMware SVGA 3D" ) ) ;
624+ assert ! ( vm_display_signal( "VirtualBox Graphics Adapter" ) ) ;
625+ assert ! ( vm_display_signal( "Red Hat QXL controller" ) ) ;
626+ assert ! ( vm_display_signal( "Citrix Indirect Display Adapter" ) ) ;
627+ }
628+
629+ #[ test]
630+ fn vm_display_signal_rejects_real_gpus ( ) {
631+ assert ! ( !vm_display_signal( "NVIDIA GeForce RTX 4090" ) ) ;
632+ assert ! ( !vm_display_signal( "AMD Radeon RX 7900 XTX" ) ) ;
633+ assert ! ( !vm_display_signal( "Intel UHD Graphics 770" ) ) ;
634+ assert ! ( !vm_display_signal( "Intel Iris Xe Graphics" ) ) ;
635+ }
636+
529637 #[ test]
530638 fn collect_vm_diagnostics_returns_valid_struct ( ) {
531639 let diag = collect_vm_diagnostics ( ) ;
0 commit comments