diff --git a/task instructions/cleanup-virtio.md b/task instructions/cleanup-virtio.md new file mode 100644 index 0000000..f27d9a6 --- /dev/null +++ b/task instructions/cleanup-virtio.md @@ -0,0 +1,317 @@ +# Cleanup VirtIO/QEMU/Oracle/Red Hat Windows Drivers + +Use this task note when a Windows VM needs VirtIO, QEMU guest agent, Oracle VirtIO, or Red Hat VirtIO remnants removed. Keep the scope to one explicitly named host. + +## Safety Rules + +- Confirm the hostname and IP before making changes. +- Remove only VirtIO/QEMU/Oracle/Red Hat components. +- Do not remove Microsoft, VMware, Intel, LSI, PVSCSI, or normal Windows storage/network drivers. +- Use exact allowlists for service keys and driver package names. +- Do not delete service keys by broad regex. In particular, never match on `orc` alone because unrelated Windows keys such as `EhStorClass` can be caught. +- If `pnputil` reports that a reboot is needed, reboot during an approved window and re-run verification afterward. +- Some Windows builds do not support `pnputil /remove-device` and may not have `Remove-PnpDevice`. For phantom device instances, use exact instance IDs and SetupAPI instead of broad registry deletion. + +## Discovery + +Run from an elevated PowerShell session on the target VM: + +```powershell +hostname +Get-NetIPAddress -AddressFamily IPv4 | Select-Object IPAddress,InterfaceAlias + +Write-Host "`n=== Installed products ===" +Get-Package '*virtio*','*VirtIO*','*QEMU*','*Oracle*','*Red Hat*' -ErrorAction SilentlyContinue | + Format-Table Name, Version, ProviderName -AutoSize + +Write-Host "`n=== Uninstall registry entries ===" +$uninstallRoots = @( + 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', + 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' +) +Get-ItemProperty $uninstallRoots -ErrorAction SilentlyContinue | + Where-Object { + $_.DisplayName -match 'VirtIO|virtio|QEMU|Oracle Windows VirtIO|Red Hat' + } | + Select-Object DisplayName, DisplayVersion, PSChildName, UninstallString, QuietUninstallString + +Write-Host "`n=== Driver Store ===" +pnputil /enum-drivers | + Select-String -Pattern 'viostor|vioscsi|balloon|netkvm|vioser|vioinput|viofs|Oracle|Red Hat|QEMU|VirtIO' -Context 6,0 + +Write-Host "`n=== Devices ===" +Get-PnpDevice -PresentOnly:$false | + Where-Object { + $_.FriendlyName -match 'VirtIO|QEMU|Oracle|Red Hat' -or + $_.Manufacturer -match 'VirtIO|QEMU|Oracle|Red Hat' + } | + Format-Table Status, Class, FriendlyName, InstanceId -AutoSize + +Write-Host "`n=== Problem or phantom target devices ===" +Get-PnpDevice -PresentOnly:$false | + Where-Object { + ($_.FriendlyName -match 'VirtIO|QEMU|Oracle|Red Hat' -or $_.InstanceId -match 'VEN_1AF4') -and + ($_.Status -ne 'OK' -or $_.Class -eq 'Unknown' -or $_.Class -eq 'Other') + } | + Select-Object Status,Class,FriendlyName,InstanceId,Problem | + Format-List + +Write-Host "`n=== Network adapters ===" +Get-NetAdapter -IncludeHidden | + Format-Table Name, InterfaceDescription, Status, MacAddress -AutoSize + +Write-Host "`n=== Driver binaries ===" +Get-ChildItem C:\Windows\System32\drivers\vio*.sys, + C:\Windows\System32\drivers\balloon*.sys, + C:\Windows\System32\drivers\netkvm*.sys, + C:\Windows\System32\drivers\pvpanic*.sys, + C:\Windows\System32\drivers\qemufwcfg*.sys ` + -ErrorAction SilentlyContinue | + Format-Table Name, Length, LastWriteTime -AutoSize +``` + +## Cleanup Sequence + +1. Stop and remove only exact QEMU/Oracle/VirtIO service names if present. + +```powershell +$targetServices = @( + 'qemu-ga', + 'QEMU-GA', + 'Oracle BalloonService', + 'Oracle VirtIO Service', + 'vgpusrvorc' +) + +foreach ($svc in $targetServices) { + $service = Get-Service -Name $svc -ErrorAction SilentlyContinue + if ($service) { + Stop-Service -Name $svc -Force -ErrorAction SilentlyContinue + sc.exe delete $svc + } +} +``` + +2. Uninstall matching MSI packages found during discovery. Use the product codes discovered on that VM, not stale codes from another machine. + +```powershell +msiexec.exe /x '{PRODUCT-CODE-FROM-DISCOVERY}' /qn /norestart +``` + +3. If the Oracle Windows VirtIO driver package has an uninstall string, run its silent uninstall command from the registry entry. + +4. Delete only exact matching DriverStore packages. First map `Published Name` to `Original Name` and `Provider Name` from `pnputil /enum-drivers`, then delete only approved packages. + +Approved original names/providers: + +```text +vioscsi.inf Red Hat, Inc. +viostor.inf Red Hat, Inc. +viostororc.inf Oracle, Inc. +netkvmorc.inf Oracle, Inc. +vioscsiorc.inf Oracle, Inc. +vioserorc.inf Oracle, Inc. +pvpanicorc.inf Oracle, Inc. +qemufwcfgorc.inf QEMU +viogpudorc.inf Oracle, Inc. +balloonorc.inf Oracle, Inc. +``` + +Delete each approved published name: + +```powershell +pnputil /delete-driver oem##.inf /uninstall /force +``` + +Do not delete VMware `pvscsi.inf`, Microsoft storage drivers, Intel NIC drivers, or any package whose original name/provider is not in the approved list. + +5. Remove only exact target service keys if they remain. + +```powershell +$targetServiceKeys = @( + 'viostor', + 'vioscsi', + 'viostororc', + 'vioscsiorc', + 'balloon', + 'balloonorc', + 'netkvm', + 'netkvmorc', + 'vioser', + 'vioserorc', + 'vioinput', + 'viofs', + 'pvpanic', + 'pvpanicorc', + 'qemufwcfg', + 'qemufwcfgorc', + 'qemu-ga', + 'QEMU-GA', + 'Oracle BalloonService', + 'Oracle VirtIO Service', + 'vgpusrvorc' +) + +foreach ($keyName in $targetServiceKeys) { + $key = "HKLM:\SYSTEM\CurrentControlSet\Services\$keyName" + if (Test-Path $key) { + Remove-Item $key -Recurse -Force + } +} +``` + +6. Remove leftover binaries and install folders that match the target stack. + +```powershell +Remove-Item C:\Windows\System32\drivers\vio*.sys, + C:\Windows\System32\drivers\balloon*.sys, + C:\Windows\System32\drivers\netkvm*.sys, + C:\Windows\System32\drivers\pvpanic*.sys, + C:\Windows\System32\drivers\qemufwcfg*.sys ` + -Force -ErrorAction SilentlyContinue + +Remove-Item 'C:\Program Files\Virtio-Win', + 'C:\Program Files\QEMU-ga', + 'C:\Program Files\qemu-ga', + 'C:\Program Files (x86)\Oracle Corporation\Oracle Windows VirtIO Drivers' ` + -Recurse -Force -ErrorAction SilentlyContinue +``` + +Remove InstallShield cache directories only when discovery proves they belong to Oracle Windows VirtIO. + +7. Remove non-present phantom target devices only by exact instance ID. + +If Device Manager still shows target devices under `Other devices`, first verify the exact `InstanceId`, `FriendlyName`, and hardware IDs. On older Windows builds, `pnputil /remove-device` may not exist and the `Remove-PnpDevice` cmdlet may not be available. In that case, use SetupAPI against exact target instance IDs. + +Example for exact phantom Oracle VirtIO SCSI instances: + +```powershell +$source = @" +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; + +public static class DeviceRemover +{ + private const int DIGCF_ALLCLASSES = 0x00000004; + private const int DIF_REMOVE = 0x00000005; + private const int DI_REMOVEDEVICE_GLOBAL = 0x00000001; + + [StructLayout(LayoutKind.Sequential)] + private struct SP_DEVINFO_DATA + { + public int cbSize; + public Guid ClassGuid; + public int DevInst; + public IntPtr Reserved; + } + + [StructLayout(LayoutKind.Sequential)] + private struct SP_CLASSINSTALL_HEADER + { + public int cbSize; + public int InstallFunction; + } + + [StructLayout(LayoutKind.Sequential)] + private struct SP_REMOVEDEVICE_PARAMS + { + public SP_CLASSINSTALL_HEADER ClassInstallHeader; + public int Scope; + public int HwProfile; + } + + [DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr SetupDiGetClassDevs(IntPtr ClassGuid, string Enumerator, IntPtr hwndParent, int Flags); + + [DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool SetupDiOpenDeviceInfo(IntPtr DeviceInfoSet, string DeviceInstanceId, IntPtr hwndParent, int OpenFlags, ref SP_DEVINFO_DATA DeviceInfoData); + + [DllImport("setupapi.dll", SetLastError = true)] + private static extern bool SetupDiSetClassInstallParams(IntPtr DeviceInfoSet, ref SP_DEVINFO_DATA DeviceInfoData, ref SP_REMOVEDEVICE_PARAMS ClassInstallParams, int ClassInstallParamsSize); + + [DllImport("setupapi.dll", SetLastError = true)] + private static extern bool SetupDiCallClassInstaller(int InstallFunction, IntPtr DeviceInfoSet, ref SP_DEVINFO_DATA DeviceInfoData); + + [DllImport("setupapi.dll", SetLastError = true)] + private static extern bool SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet); + + public static void RemoveExact(string instanceId) + { + IntPtr set = SetupDiGetClassDevs(IntPtr.Zero, null, IntPtr.Zero, DIGCF_ALLCLASSES); + if (set == IntPtr.Zero || set.ToInt64() == -1) + throw new Win32Exception(Marshal.GetLastWin32Error(), "SetupDiGetClassDevs failed"); + + try + { + SP_DEVINFO_DATA data = new SP_DEVINFO_DATA(); + data.cbSize = Marshal.SizeOf(typeof(SP_DEVINFO_DATA)); + + if (!SetupDiOpenDeviceInfo(set, instanceId, IntPtr.Zero, 0, ref data)) + throw new Win32Exception(Marshal.GetLastWin32Error(), "SetupDiOpenDeviceInfo failed for " + instanceId); + + SP_REMOVEDEVICE_PARAMS remove = new SP_REMOVEDEVICE_PARAMS(); + remove.ClassInstallHeader.cbSize = Marshal.SizeOf(typeof(SP_CLASSINSTALL_HEADER)); + remove.ClassInstallHeader.InstallFunction = DIF_REMOVE; + remove.Scope = DI_REMOVEDEVICE_GLOBAL; + remove.HwProfile = 0; + + if (!SetupDiSetClassInstallParams(set, ref data, ref remove, Marshal.SizeOf(typeof(SP_REMOVEDEVICE_PARAMS)))) + throw new Win32Exception(Marshal.GetLastWin32Error(), "SetupDiSetClassInstallParams failed for " + instanceId); + + if (!SetupDiCallClassInstaller(DIF_REMOVE, set, ref data)) + throw new Win32Exception(Marshal.GetLastWin32Error(), "SetupDiCallClassInstaller DIF_REMOVE failed for " + instanceId); + } + finally + { + SetupDiDestroyDeviceInfoList(set); + } + } +} +"@ + +Add-Type -TypeDefinition $source + +$targetInstanceIds = @( + 'PCI\VEN_1AF4&DEV_1048&SUBSYS_1100108E&REV_01\4&18E4EEC6&0&0000', + 'PCI\VEN_1AF4&DEV_1004&SUBSYS_0008108E&REV_00\3&13C0B0C5&0&10' +) + +foreach ($id in $targetInstanceIds) { + $dev = Get-PnpDevice -PresentOnly:$false -InstanceId $id -ErrorAction SilentlyContinue + if ($dev) { + [DeviceRemover]::RemoveExact($id) + } +} +``` + +## Final Verification + +Re-run the discovery commands. A clean result should show: + +- No installed packages matching VirtIO/QEMU/Oracle/Red Hat. +- No DriverStore matches for the target VirtIO/QEMU/Oracle/Red Hat packages. +- No QEMU/Oracle/VirtIO services or exact service keys from the allowlist. +- No problem or phantom devices matching `VirtIO`, `QEMU`, `Oracle`, `Red Hat`, or PCI vendor `VEN_1AF4`. +- No `vio*.sys`, `balloon*.sys`, `netkvm*.sys`, `pvpanic*.sys`, or `qemufwcfg*.sys` binaries. +- No `C:\Program Files\Virtio-Win`, QEMU guest agent folder, or Oracle Windows VirtIO folder. +- The active network adapter should still be the expected non-VirtIO adapter. + +## Recovery Note + +If an overly broad cleanup accidentally removes the Windows Enhanced Storage Class service key, restore `EhStorClass`: + +```powershell +$key = 'HKLM:\SYSTEM\CurrentControlSet\Services\EhStorClass' +New-Item -Path $key -Force | Out-Null +New-ItemProperty -Path $key -Name Type -PropertyType DWord -Value 1 -Force | Out-Null +New-ItemProperty -Path $key -Name Start -PropertyType DWord -Value 0 -Force | Out-Null +New-ItemProperty -Path $key -Name ErrorControl -PropertyType DWord -Value 3 -Force | Out-Null +New-ItemProperty -Path $key -Name Group -PropertyType String -Value 'SCSI Class' -Force | Out-Null +New-ItemProperty -Path $key -Name ImagePath -PropertyType ExpandString -Value 'System32\drivers\EhStorClass.sys' -Force | Out-Null +New-ItemProperty -Path $key -Name DisplayName -PropertyType String -Value '@%SystemRoot%\System32\drivers\EhStorClass.sys,-100' -Force | Out-Null +New-ItemProperty -Path $key -Name Description -PropertyType String -Value '@%SystemRoot%\System32\drivers\EhStorClass.sys,-101' -Force | Out-Null +Test-Path C:\Windows\System32\drivers\EhStorClass.sys +``` + +The real fix is prevention: only exact allowlisted names should be removed.