vmix.nix/wip/win10-update.session.md
Git Sagar b6a080af4b WIP: add Windows Update template with online COM API updates
Add essentials.windowsUpdate template that boots Audit Mode, uses the
Windows Update COM API to search/download/install all available updates
(cumulative, .NET, Defender), handles multi-round reboots with Audit
Mode preservation, and compacts the image afterward.

Known issues being worked:
- Audit Mode preservation after update reboot needs verification
- Install takes ~60-90 min with 4GB RAM on slow machines

Includes full session notes in wip/ with detailed test log, build
commands, issue analysis, timing data, and Claude memory files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-09 13:17:27 +05:30

25 KiB

Windows Update Template - Full Development Session Log

Original Request

User wanted a reproducible way to update Windows images, inspired by https://massgrave.dev/update-windows-iso. The upstream Win10 LTSC 2021 ISO is stored in a git-lfs repo at git.sagar.ch.

Approach Evolution

Phase 1: ISO Update Approach (abandoned)

Initially explored creating an updated ISO by integrating cumulative updates. Considered:

  1. UUP Dump approach: The massgrave page links to uupdump.net which provides download scripts for building fresh ISOs from Microsoft's UUP (Unified Update Platform) files. The user received a bash script from UUP dump that:

    • Uses aria2c to download UUP .cab files from Microsoft CDN
    • Uses cabextract, wimlib-imagex, chntpw to build an ISO
    • Uses genisoimage/mkisofs to create the final ISO
    • Problem: downloads core;professional (Home/Pro), NOT LTSC
    • Problem: download URLs contain auth tokens that expire — not reproducible
  2. Offline DISM approach: Considered creating a Nix derivation that:

    • Boots a Windows worker VM from the existing upstream image
    • Mounts the original ISO inside the VM
    • Uses DISM /Image to service install.wim offline (mount WIM, apply .cab updates, unmount)
    • Extracts the updated install.wim via guestfs
    • Rebuilds the ISO with genisoimage on the Linux host
    • Problem: very complex, requires managing WIM indexes, boot.wim, etc.
    • Problem: needs a "worker" Windows VM just to run DISM
  3. Offline .msu approach: Considered downloading specific .msu files from Microsoft Update Catalog:

    • Search catalog.update.microsoft.com for cumulative update for windows 10 version 21H2 x64
    • Found latest: KB5087544 (May 2026), KB5088859 (.NET), KB2267602 (Defender)
    • Problem: catalog uses JavaScript popups for downloads — can't scrape URLs
    • Could use curl POST to DownloadDialog.aspx with update GUIDs, but GUIDs are hard to extract
    • The download URLs from catalog.s.download.windowsupdate.com are stable (don't expire) but finding them requires browser interaction

Phase 2: customizeImage Template (adopted)

User said: "instead of iso, let's create a customizeImage template that basically updates the existing image and runs qcow2 compact on it"

This is much simpler — apply updates to the running Windows image during Audit Mode, not to the ISO.

Phase 3: Online vs Offline Updates

Initially built the template with two modes:

  • Offline mode: user provides .msu/.cab files as fetchurl derivations, applied via DISM /Online /Add-Package
  • Online mode: triggers Windows Update service directly via COM API

User said "just do online updates" — removed offline mode entirely.

What was built

New files

  • lib/images/windows/templates/essentials/windows-update.nix — template that boots into Audit Mode, runs Windows Update via COM API, handles reboots automatically, compacts image

Modified files

  • lib/images/windows/helpers/customizeImage.nix — added compact, qemuTimeout parameters (committed in 3b454b7 on master alongside other changes)
  • lib/images/windows/templates/default.nix — wired windowsUpdate into essentials
  • lib/images/windows/win10/images.nix — added updated step between upstream and basic
  • lib/images/windows/win11/images.nix — same

New customizeImage parameters

compact ? false

Added to customizeImage.nix. When true, runs qemu-img convert -O qcow2 after the audit boot to flatten the COW chain into a standalone qcow2 with no backing file dependency. This is important for the update template because the updates add several GB of data to the COW overlay.

Implementation in builderCommand:

${lib.optionalString compact ''
echo "=== vmix: compacting image ==="
qemu-img convert -O qcow2 ${resultImg} compact.qcow2
mv compact.qcow2 ${resultImg}
''}
mv ${resultImg} $out

qemuTimeout ? 1800

Added to customizeImage.nix. Replaces the hardcoded timeout 1800 in the QEMU boot command. The Windows Update template sets this to 7200 (2 hours) because update installation with reboots can take a long time.

Both timeout invocations in the builder (primary and SDL-fallback) now use ${toString qemuTimeout}.

How the template works

The template is a function that takes { maxRounds ? 3 } and returns a customizeImage-compatible attrset:

{
  name = "windows-update";
  compact = true;
  memSize = 4096;
  qemuTimeout = 7200;
  auditScript = "...";
}

Audit script flow

  1. Round tracking: Uses C:\vmix-update-round.txt to track which round we're on across reboots. First run creates the file with "1", subsequent runs read and increment.

  2. Service initialization:

    net start wuauserv 2>nul
    sc config wuauserv start= auto
    reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /f 2>nul
    reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /f 2>nul
    

    The LTSC image may have update-blocking group policies. We delete them. We also wait 30 seconds on first round for the service to initialize and sync with Microsoft.

  3. PowerShell COM API: The core update logic uses Microsoft.Update.Session:

    $session = New-Object -ComObject Microsoft.Update.Session
    $searcher = $session.CreateUpdateSearcher()
    $result = $searcher.Search('IsInstalled=0')
    # ... accept EULAs, download, install ...
    if ($installResult.RebootRequired) { exit 3010 } else { exit 0 }
    
    • Exit code 3010 = reboot required
    • Exit code 0 = no reboot needed (or no updates found)
    • Exit code 1 = error (caught by try/catch)
    • Per-update results are logged with HResult codes
  4. Component cleanup: After updates, runs dism /Online /Cleanup-Image /StartComponentCleanup /ResetBase /Quiet to reclaim space from superseded update components.

  5. Reboot handling (if exit code 3010 and round < maxRounds):

    :: Copy script to survive wrapper deletion
    copy /y "%~f0" "C:\vmix-update-continue.cmd" >nul
    :: Register for next boot
    reg add "HKLM\...\RunOnce" /v vmixUpdate /d "cmd /c C:\vmix-update-continue.cmd" /f
    :: Preserve Audit Mode
    reg add "HKLM\SYSTEM\Setup\Status\AuditBoot" /v AuditBoot /t REG_DWORD /d 1 /f
    reg add "HKLM\SYSTEM\Setup" /v AuditInProgress /t REG_DWORD /d 1 /f
    :: Reboot
    shutdown /r /f /t 0
    exit /b
    
  6. Final shutdown: When all rounds complete (or no more updates), the script shuts down:

    del /q "%ROUND_FILE%" 2>nul
    del /q "C:\vmix-update-continue.cmd" 2>nul
    echo === vmix: Windows Update complete ===
    shutdown /s /f /t 10 /c "vmix: windows-update complete"
    

Pipeline integration

win10/images.nix

ltsc = rec {
  upstream = makeImage { ... };
  updated = customizeImage upstream (templates.essentials.windowsUpdate {});
  basic = customizeImageFold updated (with templates; [ ... ]);
  # ... withApps, withAMDGPU ...
};
laptopUpstream = makeImage { ... useAHCI = true; };
laptopUpdated = customizeImage laptopUpstream (templates.essentials.windowsUpdate {});
laptopSlim = customizeImageFold laptopUpdated templates.bundles.laptopSlim;
laptop = customizeImageFold laptopUpdated templates.bundles.laptop;

win11/images.nix

Same pattern — updated step inserted between upstream and basic, laptopUpdated between laptopUpstream and laptopSlim/laptop.

Usage examples

# Online mode (default) - triggers Windows Update service
essentials.windowsUpdate {}
essentials.windowsUpdate { maxRounds = 5; }

# In image pipeline
updated = customizeImage upstream (templates.essentials.windowsUpdate {});
basic = customizeImageFold updated [ ... ];

# Override VNC display and disable compact for debugging
vmixLib.windows.customizeImage upstream (windowsUpdate // { vncDisplay = ":55"; compact = false; })

Detailed Test Log

Build 1: First attempt (no VNC, wrong attr path)

nix build --print-build-logs --impure --option sandbox relaxed --expr '
  let vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
  in vmixLib.images.win10.ltsc.updated
'

Result: Error — attribute 'images' missing. The correct path is vmixLib.windows.images.win10.ltsc.updated, not vmixLib.images.win10.ltsc.updated.

Build 2: Correct path, VNC :1, original script (no wuauserv start)

nix build ... --expr '
  let vmixLib = ...;
      upstream = vmixLib.windows.images.win10.ltsc.upstream;
      windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate {};
  in vmixLib.windows.customizeImage upstream (windowsUpdate // { vncDisplay = ":1"; })
'

Result: Build "succeeded" but image size identical to upstream (5.03 GB vs 5.02 GB). The COM API found 0 updates because:

  • Windows Update service (wuauserv) wasn't running in Audit Mode
  • No time given for service to initialize
  • Possible update-blocking policies active

Build log showed: QEMU booted, script ran, shut down — no errors visible in nix log (VNC output isn't captured).

Build 3: Added wuauserv start, policy removal, 30s wait

Updated windows-update.nix to add:

net start wuauserv 2>nul
sc config wuauserv start= auto
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /f 2>nul
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /f 2>nul
timeout /t 30 /nobreak >nul

Also added EULA acceptance and try/catch error handling to the PowerShell block.

Build with VNC :55, chained virtio tools first:

nix build ... --expr '
  let vmixLib = ...;
      upstream = vmixLib.windows.images.win10.ltsc.upstream;
      withVirtio = vmixLib.windows.customizeImage upstream vmixLib.windows.templates.essentials.virtioTools;
      windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate {};
  in vmixLib.windows.customizeImage withVirtio (windowsUpdate // { vncDisplay = ":55"; compact = false; })
'

VNC observations (screenshots taken with gvnccapture localhost:55 /tmp/screenshot.png):

  • T+2min: Boot screen "Please wait"
  • T+3min: CMD window showing:
    === vmix audit: windows-update ===
    === vmix: Windows Update round 1 of 3 ===
    The Windows Update service is starting.
    The Windows Update service was started successfully.
    [SC] ChangeServiceConfig SUCCESS
    Waiting for Windows Update service to initialize...
    Searching for updates...
    
  • T+7min: Found 6 updates:
    Found 6 updates
     - 2022-02 Cumulative Update Preview for .NET Framework 3.5 and 4.8 for Windows 10 Version 21H2 for x64 (KB5010472)
     - Microsoft .NET Framework 4.8.1 for Windows 10 Version 21H2 for x64 (KB5011048)
     - Windows Malicious Software Removal Tool x64 - v5.141 (KB890830)
     - 2026-05 Cumulative Update for .NET Framework 3.5, 4.8 and 4.8.1 for Windows 10 Version 21H2 for x64 (KB5088859)
     - Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.451.326.0) - Current Channel (Broad)
     - 2026-05 Cumulative Update for Windows 10 Version 21H2 for x64-based Systems (KB5087544)
    Downloading...
    
  • T+12min: "Installing..."
  • T+20min through T+55min: Still "Installing..." — KB5087544 is a massive cumulative update spanning Oct 2021 → May 2026
  • T+58min: Sysprep dialog showing on desktop — the machine rebooted after update install and landed in Audit Mode desktop, but the round 2 script didn't run

Issue discovered: The wrapper script (vmix-audit-wrapper.cmd) deletes C:\vmix-audit-script.cmd after the audit script returns but before the reboot completes. The RunOnce entry points to the deleted file.

Build 4: Added script copy fix + shutdown fix

Updated script to:

  1. Copy itself to C:\vmix-update-continue.cmd before rebooting
  2. Register cmd /c C:\vmix-update-continue.cmd in RunOnce (not the original path)
  3. Always call shutdown /s /f /t 10 when done (handles both wrapper and RunOnce invocation)

Same build command as Build 3.

VNC observations:

  • T+3min: Round 1 started, same 6 updates found
  • T+7min: Downloading...
  • T+12min: Installing...
  • T+55min: TianoCore UEFI boot screen — machine rebooted!
  • T+57min: "Working on updates — 90% complete" — Windows finalizing update installation after reboot
  • T+62min: "Working on updates — 19% complete" (different counter — this is the post-reboot finalization)
  • T+70min: Sysprep dialog... but NO round 2 script running

Issue discovered: After reboot, Windows exited Audit Mode and entered OOBE ("Choose your keyboard layout" screen) instead of staying in Audit Mode. The cumulative update reset the Audit Mode flags during its finalization pass.

Wait — actually on this build we saw the Sysprep dialog (which IS Audit Mode). The issue was the RunOnce not firing. On a subsequent build, we saw the keyboard layout screen (OOBE). The behavior is inconsistent.

Build 5: Added Audit Mode preservation

Added registry keys before reboot to preserve Audit Mode:

reg add "HKLM\SYSTEM\Setup\Status\AuditBoot" /v AuditBoot /t REG_DWORD /d 1 /f
reg add "HKLM\SYSTEM\Setup" /v AuditInProgress /t REG_DWORD /d 1 /f

Same build command. VNC observations:

  • T+3min: Round 1 started, same 6 updates found
  • T+70min: Still on "Installing..." — round 1 didn't finish in 70 min on this machine
  • T+100min: "Working on updates — 19% complete" — rebooted, finalizing
  • T+110min: "Choose your keyboard layout" — OOBE screen again!

Session ended here — the Audit Mode preservation fix hasn't been verified as working yet. The machine was moved to a faster machine for continued testing.

Detailed Issue Analysis

Issue 1: Windows Update service not running

Root cause: In Audit Mode, the Windows Update service (wuauserv) has Start=3 (manual) and isn't auto-started. The COM API requires the service to be running to search for updates.

Fix: Explicitly start the service and set it to auto-start:

net start wuauserv 2>nul
sc config wuauserv start= auto

Also remove any group policies that block updates (LTSC may have these pre-configured):

reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /f 2>nul
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /f 2>nul

And wait 30 seconds for the service to initialize and sync with Microsoft servers on first round.

Issue 2: Wrapper deletes script before reboot completes

Root cause: The customizeImage.nix wrapper script flow:

call C:\vmix-audit-script.cmd    ← our update script
del /q C:\vmix-audit-script.cmd  ← DELETE happens here
shutdown /s /t 5                 ← may override our shutdown /r
del /q C:\vmix-audit-wrapper.cmd

When our script calls shutdown /r /f /t 0 and then exit /b, control returns to the wrapper. The wrapper deletes the script file. Even though shutdown /r /t 0 was called, the batch file continues executing and the delete happens before the system actually reboots.

The RunOnce entry points to C:\vmix-audit-script.cmd which no longer exists after reboot.

Additionally, the wrapper's shutdown /s /t 5 may override our shutdown /r /t 0 (though in practice the /t 0 reboot usually wins).

Fix: Copy the script to a separate path before rebooting:

copy /y "%~f0" "C:\vmix-update-continue.cmd" >nul
reg add "HKLM\...\RunOnce" /v vmixUpdate /d "cmd /c C:\vmix-update-continue.cmd" /f

The wrapper only knows about C:\vmix-audit-script.cmd and C:\vmix-audit-wrapper.cmd. It doesn't know about C:\vmix-update-continue.cmd, so that file survives.

Issue 3: Audit Mode lost after update reboot

Root cause: Windows Audit Mode is maintained by registry keys:

  • HKLM\SYSTEM\Setup\Status\AuditBoot (AuditBoot = 1)
  • HKLM\SYSTEM\Setup (AuditInProgress = 1)

Some cumulative updates include "specialize" or "generalize" passes during their finalization that can clear these keys, causing Windows to transition from Audit Mode to OOBE on the next boot.

Fix: Explicitly set the Audit Mode registry keys right before rebooting:

reg add "HKLM\SYSTEM\Setup\Status\AuditBoot" /v AuditBoot /t REG_DWORD /d 1 /f
reg add "HKLM\SYSTEM\Setup" /v AuditInProgress /t REG_DWORD /d 1 /f

Status: NOT YET VERIFIED. The last build with this fix was still in the install phase when the session ended. The fix may or may not be sufficient — some updates may clear these keys AFTER the reboot during the "Working on updates X% complete" phase, which would happen after our registry writes.

Alternative approaches if the fix doesn't work:

  1. Use C:\Windows\System32\Sysprep\sysprep.exe /audit /reboot /quiet instead of shutdown /r — this explicitly tells Windows to re-enter Audit Mode
  2. Set up a SetupComplete.cmd or Specialize unattend pass that forces Audit Mode
  3. Use a scheduled task instead of RunOnce (scheduled tasks persist across mode transitions)
  4. Accept single-round updates only (maxRounds = 1) — no reboot needed if updates don't require it

Issue 4: No shutdown after round 2

Root cause: When the script is invoked via RunOnce (round 2+), it runs directly — not through the wrapper. The wrapper is what normally calls shutdown /s /t 5 after the script completes. Without the wrapper, the script finishes and the machine just sits at the Audit Mode desktop indefinitely (until the 2-hour QEMU timeout).

Fix: The script itself calls shutdown /s /f /t 10 when all rounds are complete. This is harmless when called from the wrapper (two shutdown commands — the second one either fails silently or is a no-op since shutdown is already pending).

All Build Commands Used

Quick evaluation (check attribute paths)

nix eval --impure --expr '
  let vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
  in builtins.attrNames vmixLib.windows.images.win10.ltsc
'

Build updated image directly (no VNC, with compact)

nix build --print-build-logs --impure --option sandbox relaxed --expr '
  let
    vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
  in vmixLib.windows.images.win10.ltsc.updated
'

Build with VNC monitoring on port 5901 (display :1)

nix build --print-build-logs --impure --option sandbox relaxed --expr '
  let
    vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
    upstream = vmixLib.windows.images.win10.ltsc.upstream;
    windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate {};
  in vmixLib.windows.customizeImage upstream (windowsUpdate // { vncDisplay = ":1"; })
'

Build with virtio tools + VNC :55 + no compact (debugging)

nix build --print-build-logs --impure --option sandbox relaxed --expr '
  let
    vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
    upstream = vmixLib.windows.images.win10.ltsc.upstream;
    withVirtio = vmixLib.windows.customizeImage upstream vmixLib.windows.templates.essentials.virtioTools;
    windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate {};
  in vmixLib.windows.customizeImage withVirtio (windowsUpdate // { vncDisplay = ":55"; compact = false; })
'

Build with more RAM (8GB) for faster installs

nix build --print-build-logs --impure --option sandbox relaxed --expr '
  let
    vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
    upstream = vmixLib.windows.images.win10.ltsc.upstream;
    windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate {};
  in vmixLib.windows.customizeImage upstream (windowsUpdate // { vncDisplay = ":55"; memSize = 8192; })
'

Build with more rounds

nix build --print-build-logs --impure --option sandbox relaxed --expr '
  let
    vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
    upstream = vmixLib.windows.images.win10.ltsc.upstream;
    windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate { maxRounds = 5; };
  in vmixLib.windows.customizeImage upstream (windowsUpdate // { vncDisplay = ":55"; })
'

Full pipeline test (upstream → updated → basic → generalize)

nix build --print-build-logs --impure --option sandbox relaxed --expr '
  let
    vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
  in vmixLib.windows.images.win10.ltsc.basic.generalize {
    username = "User"; password = ""; hostname = "WIN-VM";
    vncDisplay = ":55";
  }
'

VNC monitoring commands

# Take a screenshot
nix-shell -p gtk-vnc --run 'gvnccapture localhost:55 /tmp/screenshot.png'

# Connect with a VNC viewer
vncviewer localhost:5955

# Check if QEMU is running
ps aux | grep 'qemu-system' | grep -v grep

# Check nix build log for a store path
nix log /nix/store/HASH-windows-update-....qcow2

# Check image info
nix-shell -p qemu --run 'qemu-img info /nix/store/HASH-....qcow2'

Image sizes observed

Image disk size virtual size
upstream (win10-ltsc-2021) 5.02 GB 64 GB
updated (killed build, round 1 only) 8.33 GB 64 GB
upstream (compacted, no updates) 5.03 GB 64 GB

Updates found by Windows Update (Win10 LTSC 2021 → May 2026)

  1. 2022-02 Cumulative Update Preview for .NET Framework 3.5 and 4.8 (KB5010472)
  2. Microsoft .NET Framework 4.8.1 for Windows 10 21H2 x64 (KB5011048)
  3. Windows Malicious Software Removal Tool x64 v5.141 (KB890830)
  4. 2026-05 Cumulative Update for .NET Framework 3.5, 4.8 and 4.8.1 (KB5088859)
  5. Security Intelligence Update for Microsoft Defender Antivirus (KB2267602, Version 1.451.326.0+)
  6. 2026-05 Cumulative Update for Windows 10 Version 21H2 (KB5087544) — the big one, ~870 MB

Timing observations (4 vCPU, 4GB RAM machine)

  • Nix evaluation: ~30 seconds
  • VirtIO tools step (cached): instant
  • Windows boot to Audit Mode desktop: ~60 seconds
  • Windows Update search: ~2-3 minutes
  • Download all 6 updates: ~3-5 minutes
  • Install all updates (especially KB5087544): 50-70 minutes
  • Reboot + "Working on updates" finalization: ~10-15 minutes
  • Total for round 1: ~70-90 minutes
  • DISM cleanup: ~2-5 minutes
  • qemu-img convert (compact): ~2-3 minutes

Remaining TODO (for next session on faster machine)

  • CRITICAL: Verify the Audit Mode preservation fix works (AuditBoot + AuditInProgress registry keys)
  • If Audit Mode fix doesn't work, try sysprep /audit /reboot /quiet instead of shutdown /r
  • Test round 2 → round 3 flow (find additional updates after cumulative? likely Defender definition updates)
  • Test with compact = true to verify final standalone image
  • Test full pipeline: upstream → updated → basic → generalize
  • Test win11 images
  • Consider using 8GB RAM (memSize = 8192) for faster update installs
  • Consider: should windowsUpdate go before or after virtioTools? Currently before in the pipeline, but the explicit build command chains virtio first for better I/O
  • The template is impure (downloads from Microsoft during build) — this is intentional but should be documented
  • Consider maxRounds = 1 mode for builds where you only want non-reboot updates
  • The compact step doesn't compress — consider adding -c flag to qemu-img convert for compressed qcow2
  • The wrapper's shutdown /s /t 5 may still race with the script's shutdown /r /t 0 — consider using shutdown /a (abort) before shutdown /r to cancel any pending shutdown

Architecture notes

How customizeImage works (for context)

  1. Creates a COW overlay on the original image: qemu-img create -f qcow2 -b ${originalImage} -F qcow2 disk.qcow2
  2. Optionally resizes: qemu-img resize disk.qcow2 ${diskSize}
  3. Merges offline registry entries via virt-win-reg --merge
  4. Injects audit script + wrapper via virt-customize --upload
  5. Adds RunOnce registry entry for the wrapper
  6. Boots QEMU with the image (user networking, UEFI, VirtIO or AHCI)
  7. The wrapper runs the audit script, then shuts down
  8. Optionally compacts: qemu-img convert -O qcow2
  9. Moves result to $out

The wrapper problem

The wrapper (vmix-audit-wrapper.cmd) is designed for simple, single-boot templates:

call C:\vmix-audit-script.cmd
del /q C:\vmix-audit-script.cmd
shutdown /s /t 5
del /q C:\vmix-audit-wrapper.cmd

This is fine for templates that don't reboot. But the Windows Update template needs multiple reboots, which conflicts with the wrapper's assumptions. The workarounds (copy script, self-shutdown) are necessary because modifying the wrapper would affect all templates.

A future improvement might be to add a multiboot flag to customizeImage that changes the wrapper behavior for templates that need reboots.

Why QEMU user networking works for Windows Update

QEMU's -nic user (SLIRP) provides NAT networking. The guest gets DHCP and can reach the internet via the host. Windows Update uses HTTPS to Microsoft's servers, which works through NAT. No special firewall rules or port forwarding needed.

Why VirtIO tools are chained before updates (in test builds)

The upstream image uses VirtIO storage (if=virtio) but doesn't have VirtIO guest tools installed. The update template doesn't need guest tools to work, but having them improves:

  • Disk I/O performance (VirtIO balloon, better driver)
  • Memory management
  • Guest agent for monitoring

In the pipeline (win10/images.nix), updates are applied directly to upstream without virtio tools, because virtio tools installation is part of the basic step. For testing, we chain them explicitly.