From 92d5a08d49b7b1bfb17adada90b6f1505d7c3583 Mon Sep 17 00:00:00 2001 From: Kristian Date: Tue, 29 Sep 2026 19:46:08 +0200 Subject: [PATCH] Fix WaaSMedicSvc restoration using direct registry write (#5096) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix WaaSMedicSvc restoration using direct registry write Set-Service fails on WaaSMedicSvc with Access Denied since it's a protected service (LaunchProtected=2). FirstLogon.ps1 silently swallowed this failure via -ErrorAction SilentlyContinue, permanently leaving WaaSMedicSvc disabled after setup. Removes WaaSMedicSvc from the Set-Service restoration loop and restores it via a direct registry write instead, matching the same technique already used to disable it in WinUtil-PostInstall.ps1. Added a test verifying the old Set-Service-based restoration is gone and the new registry-write fix is present. Addresses the WaaSMedicSvc portion of #5095; the broader AppX/Store failure investigation is out of scope for this change. * Assert exact registry-write contract for WaaSMedicSvc test Per CodeRabbit review: the previous assertion only checked that Set-ItemProperty and WaaSMedicSvc appeared near each other, which would pass even with a wrong -Value or -Type. Now asserts the complete command including -Value 3 and -Type DWord. * Surface service restoration failures in FirstLogon.log BITS, wuauserv, UsoSvc, and WaaSMedicSvc restoration all used -ErrorAction SilentlyContinue, which suppresses errors before they reach the output stream FirstLogon.ps1 redirects to its log file (*>&1 ... >> FirstLogon.log). This meant any restoration failure — not just the WaaSMedicSvc one already fixed — was invisible even in the log. Changed -ErrorAction SilentlyContinue to Continue on both the Set-Service loop and the WaaSMedicSvc registry write, so failures still don't halt the script but now actually land in FirstLogon.log for troubleshooting. Addresses the logging portion of the reporter's suggestion in #5095. * Assert -ErrorAction Continue in WaaSMedicSvc test Per CodeRabbit review: the existing test only checked the Set-ItemProperty command and value, not the -ErrorAction Continue change made for logging. Extended the same test to also assert both the Set-ItemProperty and Set-Service lines use Continue instead of SilentlyContinue. * Extend WaaSMedicSvc test to cover full ErrorAction Continue Per CodeRabbit review: the registry-write assertion stopped at -Type DWord, so a regression back to -ErrorAction SilentlyContinue would still pass. Extended the pattern to include -ErrorAction Continue at the end of the command. * Exercise FirstLogon service restoration behavior --------- Co-authored-by: Chris Titus --- pester/win11creator.Tests.ps1 | 79 +++++++++++++++++++++++++++++++++++ tools/autounattend.xml | 5 ++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/pester/win11creator.Tests.ps1 b/pester/win11creator.Tests.ps1 index baf666bb..62631757 100644 --- a/pester/win11creator.Tests.ps1 +++ b/pester/win11creator.Tests.ps1 @@ -1120,4 +1120,83 @@ Describe "Win11 Creator setup media" { $exportRunIndex | Should -BeGreaterThan $exportDialogIndex $script:exportFunction | Should -Match ([regex]::Escape('return')) } + + Context "FirstLogon update service restoration" { + BeforeAll { + [xml]$unattend = Get-Content -LiteralPath $script:autoUnattendPath -Raw + $firstLogon = $unattend.SelectSingleNode("//*[local-name()='File' and @path='C:\Windows\Setup\Scripts\FirstLogon.ps1']").InnerText + $tokens = $null + $parseErrors = $null + $ast = [System.Management.Automation.Language.Parser]::ParseInput($firstLogon, [ref]$tokens, [ref]$parseErrors) + if ($parseErrors.Count) { throw 'FirstLogon script failed to parse.' } + $blocks = @($ast.FindAll({ + param($node) + $node -is [System.Management.Automation.Language.ScriptBlockExpressionAst] -and + $node.ScriptBlock.Find({ + param($command) + $command -is [System.Management.Automation.Language.CommandAst] -and + $command.GetCommandName() -eq 'Set-Service' + }, $false) + }, $true)) + if ($blocks.Count -ne 1) { throw 'Expected exactly one service restoration block.' } + # Never execute the surrounding FirstLogon cleanup, downloads, or installer. + $commands = $blocks[0].ScriptBlock.FindAll({ + param($node) + $node -is [System.Management.Automation.Language.CommandAst] + }, $true) + foreach ($command in $commands) { + if ($command.GetCommandName() -notin @('reg.exe', 'Set-Service', 'Set-ItemProperty')) { + throw "Unexpected command in restoration block: $($command.Extent.Text)" + } + } + $script:restoreServices = $blocks[0].ScriptBlock.GetScriptBlock() + function reg.exe { param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments) } + } + + BeforeEach { + Mock reg.exe { } + Mock Set-Service { } + Mock Set-ItemProperty { } + } + + It "restores ordinary services and writes the protected Medic startup value directly" { + & $script:restoreServices + + Should -Invoke Set-Service -Times 3 -Exactly + Should -Invoke Set-Service -Times 1 -Exactly -ParameterFilter { $Name -eq 'BITS' -and $StartupType -eq 'Manual' -and $ErrorAction -eq 'Continue' } + Should -Invoke Set-Service -Times 1 -Exactly -ParameterFilter { $Name -eq 'wuauserv' -and $StartupType -eq 'Manual' -and $ErrorAction -eq 'Continue' } + Should -Invoke Set-Service -Times 1 -Exactly -ParameterFilter { $Name -eq 'UsoSvc' -and $StartupType -eq 'Automatic' -and $ErrorAction -eq 'Continue' } + Should -Invoke Set-Service -Times 0 -Exactly -ParameterFilter { $Name -eq 'WaaSMedicSvc' } + Should -Invoke Set-ItemProperty -Times 1 -Exactly + Should -Invoke Set-ItemProperty -Times 1 -Exactly -ParameterFilter { + $Path -eq 'HKLM:\SYSTEM\CurrentControlSet\Services\WaaSMedicSvc' -and + $Name -eq 'Start' -and $Value -eq 3 -and $Type -eq 'DWord' -and $ErrorAction -eq 'Continue' + } + } + + It "keeps failures observable while attempting the remaining restoration work" { + $script:restorationCalls = [System.Collections.Generic.List[string]]::new() + Mock Set-Service { + param($Name, $ErrorAction) + $script:restorationCalls.Add($Name) + if ($script:restorationCalls.Count -eq 1) { + Write-Error 'simulated service restoration failure' -ErrorAction $ErrorAction + } + } + Mock Set-ItemProperty { + param($ErrorAction) + $script:restorationCalls.Add('Medic registry') + Write-Error 'simulated Medic registry failure' -ErrorAction $ErrorAction + } + + $output = @(& $script:restoreServices 2>&1) + $failures = @($output | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] }) + $failures.Count | Should -Be 2 + $failures[0].Exception.Message | Should -Be 'simulated service restoration failure' + $failures[1].Exception.Message | Should -Be 'simulated Medic registry failure' + $script:restorationCalls.Count | Should -Be 4 + @($script:restorationCalls | Select-Object -First 3 | Sort-Object) | Should -Be @('BITS', 'UsoSvc', 'wuauserv') + $script:restorationCalls[3] | Should -Be 'Medic registry' + } + } } diff --git a/tools/autounattend.xml b/tools/autounattend.xml index d237be10..dde53ae7 100644 --- a/tools/autounattend.xml +++ b/tools/autounattend.xml @@ -453,10 +453,11 @@ $scripts = @( reg.exe delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" /v DODownloadMode /f; reg.exe add "HKLM\Software\Policies\Microsoft\Windows\OneDrive" /v DisableFileSyncNGSC /t REG_DWORD /d 0 /f; reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\GameDVR" /v AppCaptureEnabled /t REG_DWORD /d 0 /f; - $services = @{ BITS = 'Manual'; wuauserv = 'Manual'; UsoSvc = 'Automatic'; WaaSMedicSvc = 'Manual' }; + $services = @{ BITS = 'Manual'; wuauserv = 'Manual'; UsoSvc = 'Automatic' }; foreach ($name in $services.Keys) { - Set-Service -Name $name -StartupType $services[$name] -ErrorAction SilentlyContinue; + Set-Service -Name $name -StartupType $services[$name] -ErrorAction Continue; } + Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\WaaSMedicSvc' -Name 'Start' -Value 3 -Type DWord -ErrorAction Continue; }; { reg.exe add "HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\Education" /f;