Prepared for the Coriolis migration pilot (Harvester / SUSE Virtualization destination). 2026-09-30. Working notes.
This walks through producing
virtio-win-2012r2.isostarting from an empty directory: download upstream virtio-win 0.1.189, extract it, copy nine driver components out of their per-component2k12R2directories into one flat directory matching VMDP's layout, unpack the guest agent from its MSI, and build an ISO9660 + Joliet image from the result. The finished tree is 43 files and the image is roughly 10 MB. Everything here is file copying and image building, with no compilation and no re-signing, because re-signing is precisely what is impossible.
SUSE's Virtual Machine Driver Pack ships its Windows Server 2012 R2 drivers signed by SUSE LLC under a Sectigo chain. winload rejects them at boot with 0xc0000428, because kernel-mode code signing on Windows versions before 10 requires a chain terminating at a Microsoft cross-certificate. SUSE's own download page states the distinction plainly: in the community package, the Windows 10 and Server 2016 and later drivers are signed by SUSE and certified by Microsoft, while all other drivers are signed by SUSE only.
Two separate signature checks apply to a Windows driver, and passing the first says nothing about the second:
- Installation (DISM,
pnputil, or an MSI) validates the.catagainst user-mode certificate stores. An untrusted publisher produces a prompt, not a failure, and installers commonly import their own certificate into TrustedPublisher first, so the step completes quietly either way. Coriolis compounds this by invokingdism /add-driverwith/forceunsigned, which suppresses the check entirely. This is why manually mounting an ISO and running an installer appears to succeed. - Boot applies kernel-mode code signing rules in
winload, before the kernel exists. It consults none of the stores the installer populated and has no prompt to dismiss. This is the check that produces0xc0000428, and it is the only one that determines whether the machine starts on a virtio disk.
The two driver sets differ exactly there:
viostor.sys (virtio-win 0.1.189)
CN=Red Hat, Inc.
-> CN=Symantec Class 3 SHA256 Code Signing CA - G2
-> CN=VeriSign Universal Root Certification Authority
-> CN=Microsoft Code Verification Root
pvvxblk.sys (VMDP 2.5.5.1)
CN=SUSE LLC
-> CN=Sectigo Public Code Signing CA EV R36
-> CN=Sectigo Public Code Signing Root R46
-> CN=AAA Certificate Services
The upstream chain reaches Microsoft Code Verification Root, the cross-signing root. The VMDP chain terminates in a commercial CA that kernel-mode code signing does not accept for this Windows generation.
Note
SUSE is not doing anything wrong. Microsoft ended cross-signing on 1 July 2021 and the cross-certificates expired at the same time, so no new kernel-mode signature valid on 2012 R2 can be produced by anyone today. Attestation signing, the replacement, is valid only on Windows 10 and later, which is why VMDP's Server 2016 set is accepted and its 2012 R2 set is not. The 0.1.189 binaries work because they were signed in August 2020, inside the window.
This is also why the problem cannot be solved by building from source. SUSE/vmdp is BSD-2-Clause and compiles cleanly, but the resulting binaries could not be signed acceptably for 2012 R2. Only already-signed binaries from before the cutoff are usable, which leaves repackaging as the only route.
The finished ISO carries a functionally equivalent driver set, component for component, that winload accepts, arranged in VMDP's directory layout so tooling expecting VMDP's structure can consume it without inheriting VMDP's signing problem. Nothing is renamed to VMDP's driver names, which is not possible (see the warning under Assembling the driver directory).
Warning
The VMDP layout this procedure produces is not consumable by Coriolis OS morphing. _add_virtio_drivers in coriolis/osmorphing/windows.py probes <drive>:\Balloon\<osdir>\<arch> to discover which OS directory to use, then builds every component path the same way. Against a flat layout that probe misses for every candidate and the method raises rather than degrading. An ISO intended for windows_virtio_iso_url must keep the upstream per-component structure instead, which means skipping the assembly step below and building the image directly from the extracted upstream tree.
| Platform | Needed for | Notes |
|---|---|---|
| Any | Downloading | ~477 MB over HTTP |
| macOS | Extract and build | hdiutil, built in. dot_clean, built in. |
| Linux | Extract and build | xorriso (or genisoimage / mkisofs), plus loop mount or bsdtar to read the source ISO |
| Windows | Extract and build | Mount-DiskImage in stock PowerShell; oscdimg from the Windows ADK Deployment Tools for the build step |
| Windows or msitools | Guest agent | msiexec /a on Windows, or msiextract from msitools elsewhere |
The guest agent is optional. viostor and vioscsi are the only components that affect boot, so a driver-only ISO is a complete answer to the 0xc0000428 problem on its own.
VMDP itself is not required. It is useful only as a reference layout, for verifying component coverage, or if you want its signed qemu-ga.exe in place of upstream's unsigned one, and it needs a SUSE download (the community build is available without a subscription; current installers need a SUSE Customer Center login).
0.1.189 is the last release carrying 2k12R2 directories, so the version is not a preference. It is dated 10 August 2020, which is what puts it inside the cross-signing window.
curl -LO https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.189-1/virtio-win.iso
The same directory also publishes virtio-win-0.1.189.iso, an identically sized copy under the versioned name, plus the guest tools MSIs and the virtio-win-guest-tools.exe bundle. Only the ISO is needed; it contains the MSIs.
The goal is a directory named virtio-win-0-1-189/ holding the ISO's contents.
macOS:
hdiutil attach virtio-win.iso -nobrowse -readonly -mountpoint /tmp/virtio-src mkdir -p virtio-win-0-1-189 cp -R /tmp/virtio-src/. virtio-win-0-1-189/ hdiutil detach /tmp/virtio-src
Linux:
mkdir -p virtio-win-0-1-189 bsdtar -xf virtio-win.iso -C virtio-win-0-1-189
Or, with root, mount -o loop,ro virtio-win.iso /mnt/virtio-src and copy from there.
Windows:
$img = Mount-DiskImage -ImagePath C:\virtio\virtio-win.iso -PassThru $drive = ($img | Get-Volume).DriveLetter New-Item -ItemType Directory -Force -Path C:\virtio\virtio-win-0-1-189 | Out-Null Copy-Item "${drive}:\*" -Destination C:\virtio\virtio-win-0-1-189 -Recurse -Force Dismount-DiskImage -ImagePath C:\virtio\virtio-win.iso
The result has one top-level directory per component (viostor/, NetKVM/, Balloon/, and so on), each subdivided by OS and architecture, plus guest-agent/ holding the two qemu-ga MSIs.
VMDP and virtio-win implement the same virtio device classes under different driver names. pvvx is SUSE's combined virtio and Xen driver family. This table is what the copy step below is executing:
| VMDP | virtio-win | 0.1.189 source path | Device class |
|---|---|---|---|
pvvxblk |
viostor |
viostor/2k12R2/amd64/ |
virtio-blk storage. Boot-critical. |
pvvxscsi |
vioscsi |
vioscsi/2k12R2/amd64/ |
virtio-scsi storage. Boot-critical. |
pvvxnet |
netkvm |
NetKVM/2k12R2/amd64/ |
virtio-net network adapter |
pvvxbn |
balloon |
Balloon/2k12R2/amd64/ |
memory balloon |
fwcfg |
qemufwcfg |
qemufwcfg/2k16/amd64/ |
QEMU fw_cfg firmware configuration interface |
pvcrash_notify |
pvpanic |
pvpanic/2k12R2/amd64/ |
guest crash notification to the host |
pvvxsvc.exe |
blnsvr.exe |
Balloon/2k12R2/amd64/ |
user-mode service companion |
virtio_fs |
viofs |
viofs/2k12R2/amd64/ |
virtio-fs shared filesystem |
virtio_rng |
viorng |
viorng/2k12R2/amd64/ |
virtio-rng entropy source |
virtio_serial |
vioser |
vioserial/2k12R2/amd64/ |
virtio-serial channel |
Two entries in that table are not straightforward substitutions:
qemufwcfghas no2k12R2directory at all. The files come from2k16/amd64/, and that package is INF and CAT only with no.sys, which is upstream's own packaging rather than an incomplete copy. Untested on 2012 R2.pvvxbnhas no clean equivalent. Its source indicates ballooning, but it registers in theBoot Bus Extendergroup and its directory also carries hypervisor detection code, suggesting it doubles as VMDP's paravirtual bus enumerator. Upstream has no counterpart: virtio devices are exposed as ordinary PCI devices claimed directly by their function drivers, with no intermediate bus node.
README.md covers the remaining mapping caveats and the confidence attached to each one.
Within the Windows 8 generation, upstream ships byte-identical binaries under w8, w8.1, 2k12 and 2k12R2, so the choice of 2k12R2 is for clarity rather than because the others differ.
Every component's files go into one flat directory, merged/Server2012r2-Win8.1/x64/, which is the structure VMDP uses. The only files excluded are the .pdb debug symbols and NetKVM's readme.doc, neither of which is referenced by an INF or hashed by a catalog.
Four of the components ship an identical copy of WdfCoInstaller01011.dll, so the copy is done with a no-clobber flag and one copy serves all four.
macOS or Linux, run from the parent directory (/Users/stephen/virtio):
SRC=virtio-win-0-1-189 DEST=merged/Server2012r2-Win8.1/x64 mkdir -p "$DEST" for c in viostor/2k12R2 vioscsi/2k12R2 NetKVM/2k12R2 Balloon/2k12R2 \ pvpanic/2k12R2 viofs/2k12R2 viorng/2k12R2 vioserial/2k12R2 \ qemufwcfg/2k16; do find "$SRC/$c/amd64" -maxdepth 1 -type f \ ! -name '*.pdb' ! -name 'readme.doc' -exec cp -n {} "$DEST/" \; done ls -1 "$DEST" | wc -l # expect 32
Windows:
$Src = "C:\virtio\virtio-win-0-1-189" $Dest = "C:\virtio\merged\Server2012r2-Win8.1\x64" New-Item -ItemType Directory -Force -Path $Dest | Out-Null $Components = @( "viostor\2k12R2", "vioscsi\2k12R2", "NetKVM\2k12R2", "Balloon\2k12R2", "pvpanic\2k12R2", "viofs\2k12R2", "viorng\2k12R2", "vioserial\2k12R2", "qemufwcfg\2k16" ) foreach ($c in $Components) { Get-ChildItem (Join-Path $Src "$c\amd64") -File | Where-Object { $_.Extension -ne ".pdb" -and $_.Name -ne "readme.doc" } | ForEach-Object { $target = Join-Path $Dest $_.Name if (-not (Test-Path -LiteralPath $target)) { Copy-Item $_.FullName $target } } } (Get-ChildItem $Dest -File).Count # expect 32
The 32 files are the INF, CAT and SYS triplets for the nine components (qemufwcfg contributing only two), plus six supporting files that are easy to overlook because upstream keeps them in the per-component directories rather than alongside the drivers:
| File | Needed by | Why |
|---|---|---|
WdfCoInstaller01011.dll |
balloon, pvpanic, viorng, vioser |
KMDF co-installer, named in SourceDisksFiles and hashed by each catalog. A package missing it fails to install. |
viorngci.dll, viorngum.dll |
viorng |
Named in viorng.inf. |
virtiofs.exe |
viofs |
User-mode service. The driver installs without it but virtio-fs does not function. |
netkvmco.dll |
netkvm |
Not referenced by the INF. Upstream ships it for adapter property configuration. |
blnsvr.exe |
balloon |
The balloon service, and the counterpart to VMDP's pvvxsvc.exe. |
Note
Neither boot-critical driver depends on any of these. viostor and vioscsi are plain SCSI miniports with no KMDF dependency and reference only their own .sys, so the storage path is complete as soon as those six files land, whatever happens with the rest.
Important
Signatures survive copying but not editing. The .cat hashes both the .inf and the binaries it covers, so whole directories may be relocated freely, while any modification to a file inside one invalidates the catalog and drops the package to unsigned. In particular the upstream files cannot be renamed to VMDP's driver names, which is why this procedure reproduces VMDP's directory structure but not its filenames.
Upstream publishes no standalone executable installer. guest-agent/ on the ISO contains only qemu-ga-i386.msi and qemu-ga-x86_64.msi, and the only .exe on the ISO is virtio-win-guest-tools.exe, a WiX Burn bundle covering the whole guest tools set.
Carrying the unpacked files rather than the MSI means the agent installs the same way the drivers do, by copying files and registering a service.
Windows. An administrative install writes the payload out without installing anything:
msiexec /a C:\virtio\virtio-win-0-1-189\guest-agent\qemu-ga-x86_64.msi TARGETDIR=C:\virtio\qga-extract /qn
The files land under TARGETDIR in the directory structure the MSI would have installed into, so locate them with a recursive listing rather than assuming a path:
Get-ChildItem C:\virtio\qga-extract -Recurse -File | Select-Object FullName
macOS or Linux, using msitools (brew install msitools, or dnf install msitools):
msiextract -C qga-extract virtio-win-0-1-189/guest-agent/qemu-ga-x86_64.msi
Either way, 11 payload files come out. Split them into VMDP's two-folder convention:
mkdir -p merged/qemu-ga/x64 merged/qemu-ga/mingw64
merged/qemu-ga/x64/ |
merged/qemu-ga/mingw64/ |
|---|---|
qemu-ga.exe |
libgcc_s_seh-1.dll |
qga-vss.dll |
libglib-2.0-0.dll |
qga-vss.tlb |
libintl-8.dll |
libssp-0.dll |
|
libwinpthread-1.dll |
|
iconv.dll |
|
gspawn-win64-helper.exe |
|
gspawn-win64-helper-console.exe |
Note
The x64 and mingw64 split is a distribution convention, not a runtime one. The MSI installs every file into a single directory (%ProgramFiles%\Qemu-ga), so a manual install on the guest should put them all in one place rather than reproducing the two folders on disk.
The two runtime sets are not identical to VMDP's. VMDP ships libiconv.dll where upstream ships iconv.dll, VMDP additionally carries libpcre2-8-0.dll and libstdc++-6.dll that upstream's build does not need, and VMDP has no counterpart to the two gspawn helpers. These reflect different mingw packaging, not a missing file on either side.
Neither qemu-ga.exe nor qga-vss.dll imports any of the mingw DLLs; both are statically linked, and the DLLs form a separate group (libglib-2.0-0.dll imports libintl-8.dll). The executable will therefore start without them. They are shipped because glib code paths can load helpers at runtime, so keeping them is the safe default.
Important
Upstream's agent binaries carry no Authenticode signature, and neither does the MSI. VMDP's qemu-ga.exe is signed by SUSE LLC. Nothing enforces this for a user-mode service, so the unsigned files install and run normally, but an environment with a policy requiring signed executables should use VMDP's agent instead. The kernel-mode signing constraint that rules out VMDP's drivers does not apply here, so mixing VMDP's agent with upstream's drivers is sound.
Replicating what the MSI does, from a directory holding all 11 files:
qemu-ga.exe -s vss-install
sc create QEMU-GA binPath= "C:\Program Files\Qemu-ga\qemu-ga.exe -d --retry-path" DisplayName= "QEMU Guest Agent" start= auto
sc start QEMU-GA
The service name, display name, start type and arguments above are the values the MSI itself writes. The VSS provider registration is a separate step because it is a custom action in the MSI rather than part of the service installation.
Where the MSI is available, it remains the simplest path, since it registers the service itself:
msiexec /i qemu-ga-x86_64.msi /qn /norestart
accepting exit code 0 or 3010.
At this point merged/ holds 43 files: 32 drivers plus 11 agent files.
find merged -type f | wc -l # expect 43
The image needs ISO9660 with Joliet extensions. Plain ISO9660 truncates names to 8.3 uppercase, which breaks the filename relationships the catalogs depend on, and a catalog that no longer matches its files is an unsigned package. Nothing here needs to be bootable, so no El Torito options are involved.
macOS, using the built-in hdiutil:
dot_clean -m merged && find merged -name '.DS_Store' -delete hdiutil makehybrid -o virtio-win-2012r2.iso -iso -joliet -default-volume-name "VIRTIO-WIN" merged
hdiutil skips .DS_Store on its own, but AppleDouble sidecars (._name) written when copying from SMB or removable media are included, which is what dot_clean is clearing first.
Linux, using xorriso (or genisoimage / mkisofs, which take the same options):
xorriso -as mkisofs -J -joliet-long -V VIRTIO-WIN -o virtio-win-2012r2.iso merged
-J enables Joliet and -joliet-long lifts the 64-character path limit, which matters if the tree is ever restructured into the deeper per-component layout.
Windows, using oscdimg from the Windows ADK Deployment Tools:
oscdimg -j1 -m -lVIRTIO-WIN C:\virtio\merged C:\virtio\virtio-win-2012r2.iso
-j1 writes Joliet names alongside ISO9660 ones, -m lifts the default size ceiling, and -l takes the volume label with no space after it. Without the ADK, the IMAPI2FS COM interface builds an image from stock PowerShell; coriolis-worker-build.ps1 in the migration repo carries a working New-IsoFile implementation.
The result is roughly 10 MB.
Confirm the file count matches the source tree and that names came through with their casing intact. A lowercase or truncated name means Joliet did not take, and the catalogs no longer match their files.
# macOS hdiutil attach virtio-win-2012r2.iso -nobrowse -readonly find /Volumes/VIRTIO-WIN -type f | wc -l hdiutil detach /Volumes/VIRTIO-WIN
Tip
If a previous mount is still attached, macOS mounts the new image as /Volumes/VIRTIO-WIN 1 and a verification reading the fixed path reports zero files on a perfectly good image. Check with ls -d /Volumes/VIRTIO-WIN* before concluding anything, and detach any stragglers.
# Linux
xorriso -indev virtio-win-2012r2.iso -find# Windows $img = Mount-DiskImage -ImagePath C:\virtio\virtio-win-2012r2.iso -PassThru $drive = ($img | Get-Volume).DriveLetter Get-ChildItem "${drive}:\" -Recurse -File | Select-Object FullName Dismount-DiskImage -ImagePath C:\virtio\virtio-win-2012r2.iso
For a stronger check than a file count, compare checksums against the source tree. Every file on the image should be byte-identical to its upstream original, since nothing was modified:
diff <(cd merged && find . -type f | sort) <(cd /Volumes/VIRTIO-WIN && find . -type f | sort)
These are choices, not omissions, but they matter if anything expects a drop-in VMDP replacement:
- No installer. VMDP's
setup.exe,VMDP-WIN-*.exe,lang/,utilities/,v2v_scripts/andvd_agent/have no upstream equivalent. Driver installation is by hand, bypnputil, or bydism /add-driver /recurse. - x86 is absent. VMDP ships
Server2012r2-Win8.1/x86/as well. Only x64 is assembled here; the same procedure against thex86source directories would produce it. - Other OS generations are absent. VMDP ships
Server2012-Win8,Server2016-19-Win10andServer2022-25-Win11trees. Only 2012 R2 has the signing problem this addresses, so only 2012 R2 is built. - No MSI on the image. The agent is carried unpacked, so
qemu-ga-x86_64.msiis not included. It remains available in the extracted upstream tree.
- virtio-win 0.1.189-1 archive directory, Fedora People
- Deprecation of software publisher certificates and commercial release certificates, Microsoft Learn
- SUSE Linux Enterprise Virtual Machine Driver Pack download page, SUSE
- SUSE/vmdp, GitHub
- virtio-win/kvm-guest-drivers-windows, GitHub
- coriolis/osmorphing/windows.py, GitHub
- Oscdimg command-line options, Microsoft Learn
Confirmed by direct observation
- The assembly loop in Step 4 reproduces the 32-file driver directory exactly, verified by SHA-256 against the existing
merged/Server2012r2-Win8.1/x64/tree. - The source file sets in each component's
2k12R2/amd64/directory, and that the only files excluded by the loop are.pdbsymbols andNetKVM/readme.doc. qemufwcfgin 0.1.189 contains only.infand.cat, and has no2k12R2directory.- 0.1.189 is the last upstream release carrying
2k12R2directories, and its files are dated 10 August 2020. - The signing chains quoted above, extracted from the PE certificate tables of
viostor.sys,vioscsi.sysandpvvxblk.sys. - VMDP's own
qemu-ga/tree uses thex64andmingw64split this procedure reproduces. - The MSI installs to
%ProgramFiles%\Qemu-gaand registers serviceQEMU-GA("QEMU Guest Agent") asLocalSystemwith arguments-d --retry-path, and registers the VSS provider through a custom action runningqemu-ga.exe -s vss-install. - The agent's 11 payload files and their division between the two folders.
- Neither
qemu-ga.exenorqga-vss.dllimports any mingw DLL. - Coriolis probes
Balloon\<osdir>\<arch>and raises when no candidate directory exists. - The upstream download URL in Step 1 resolves and serves a 477 MB image.
Confirmed by vendor documentation
- Cross-signing ended on 1 July 2021 and the cross-certificates expired, so no new kernel-mode signature valid on 2012 R2 can be produced. Attestation signing is valid only on Windows 10 and later.
- SUSE's download page states that only the Windows 10 and Server 2016 and later drivers in the community package are certified by Microsoft, with the rest signed by SUSE only.
oscdimgsyntax:-j1for Joliet alongside ISO9660,-mto lift the size ceiling,-l<label>with no space.
Reasoned, not yet validated
- The
msiextractinvocation in Step 5. msitools is not installed on the machine these notes were written on; the files currently inmerged/qemu-ga/were produced by extracting the MSI's MSZIP cabinet directly and renaming to the File table's long names. The administrative-install route on Windows is the documented mechanism and is the recommended path. - The component mapping for
fwcfg,pvcrash_notify,virtio_fs,virtio_rngandvirtio_serial, derived from matching source directories in the two projects rather than from testing.
Needs live validation
- Whether a migrated guest boots using the drivers in this ISO, as opposed to a guest prepared before migration.
- Whether the KubeVirt and Harvester plugin uses the open-core driver path logic, which determines whether the flat VMDP layout is usable at all.
- Whether
qemufwcfgfrom the2k16package functions on 2012 R2.