From daac2311488e8bc3dbd8bf4a076d6ce637030370 Mon Sep 17 00:00:00 2001 From: sysadmin Date: Tue, 9 Jun 2026 21:27:24 +0100 Subject: [PATCH] fix(first-boot): re-apply personalization branding online + defer sm-bootstrap cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VM e2e findings on the real-user desktop: 1. Lock/login screen + wallpaper NOT branded (OEM About WAS) — Windows resets the offline-baked personalization (PersonalizationCSP / default-user wallpaper / FVE) during OOBE, same class as the UAC reset. Fix: stage windows/branding/ into the image and re-run Apply-Branding -Mode Online from SetupComplete (post-OOBE, as SYSTEM) where it sticks. OEM About re-asserted harmlessly. 2. sm-bootstrap account still present after onboarding — TearDownAsync's in-session Remove-LocalUser no-ops (can't delete the account you're logged in as). Fix: keep the best-effort in-session attempt, but DEFER the real removal to a SYSTEM AtStartup scheduled task that runs on next boot (sm-bootstrap not logged on), removes the account + Win32_UserProfile, then deletes itself. (Network 'no adapter' in the VM was a Proxmox NIC-model regression to virtio — fixed by switching the VM to Intel e1000; not a SilverMetal change.) Co-Authored-By: Claude Opus 4.8 --- windows/installer/build.ps1 | 5 +++++ windows/installer/oem/SetupComplete.cmd | 8 ++++++++ .../SilverOS.Welcome.Core/Apply/BootstrapService.cs | 12 ++++++++++++ 3 files changed, 25 insertions(+) diff --git a/windows/installer/build.ps1 b/windows/installer/build.ps1 index b35922c..31e3f4c 100644 --- a/windows/installer/build.ps1 +++ b/windows/installer/build.ps1 @@ -234,6 +234,11 @@ function Invoke-ServiceWim { Copy-Item (Join-Path $PSScriptRoot 'oem\SetupComplete.cmd') $scripts -Force Copy-Item (Join-Path $PSScriptRoot 'oem\Configure-Kiosk.ps1') $scripts -Force Copy-Item (Join-Path $WindowsDir 'hardening\*') (Join-Path $scripts 'hardening') -Recurse -Force + # Stage the branding module so SetupComplete.cmd can re-apply branding ONLINE + # (Windows resets the offline personalization bake during OOBE). + $brandDest = Join-Path $scripts 'branding' + $null = New-Item -ItemType Directory -Force $brandDest + Copy-Item (Join-Path $WindowsDir 'branding\*') $brandDest -Recurse -Force # Stage Welcome app + flavours while the WIM is still mounted. Copy-WelcomePayload diff --git a/windows/installer/oem/SetupComplete.cmd b/windows/installer/oem/SetupComplete.cmd index 53dfa39..a3a67c4 100644 --- a/windows/installer/oem/SetupComplete.cmd +++ b/windows/installer/oem/SetupComplete.cmd @@ -14,6 +14,14 @@ set HARD=C:\Windows\Setup\Scripts\hardening echo [%DATE% %TIME%] SilverMetal first-boot start >> "%LOG%" +REM Re-apply branding ONLINE (lock screen / wallpaper / OEM / FVE). Windows resets +REM the offline-baked personalization during OOBE, so re-assert it here (post-OOBE, +REM as SYSTEM) where it sticks. Idempotent with the offline bake. +if exist "%~dp0branding\Apply-Branding.ps1" ( + echo [%DATE% %TIME%] re-applying SilverMetal branding (online) >> "%LOG%" + powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0branding\Apply-Branding.ps1" -Mode Online >> "%LOG%" 2>&1 +) + if exist "C:\Program Files\SilverOS\Welcome\SilverOS.Welcome.App.exe" ( echo [%DATE% %TIME%] configuring onboarding kiosk >> "%LOG%" powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0Configure-Kiosk.ps1" >> "%LOG%" 2>&1 diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs b/windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs index 64a6922..a31410c 100644 --- a/windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs +++ b/windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs @@ -36,7 +36,19 @@ public sealed class BootstrapService(IProcessRunner runner) : IBootstrapService $"Remove-ItemProperty -Path {w} -Name DefaultUserName -EA SilentlyContinue; " + $"Remove-ItemProperty -Path {w} -Name DefaultDomainName -EA SilentlyContinue", ct); var u = Esc(bootstrapUser); + // Best-effort in-session removal (usually no-ops — you can't delete the account + // you're logged in as), THEN defer the real removal to a SYSTEM startup task that + // runs on next boot, when sm-bootstrap is no longer logged on. It removes the + // account + profile, then deletes itself. Encoded command avoids schtasks quoting. await Ps($"Remove-LocalUser -Name '{u}' -EA SilentlyContinue", ct); + var cleanup = + $"Remove-LocalUser -Name '{u}' -ErrorAction SilentlyContinue; " + + $"Get-CimInstance Win32_UserProfile | Where-Object {{ $_.LocalPath -like '*\\{u}' }} | Remove-CimInstance -ErrorAction SilentlyContinue; " + + "schtasks /delete /tn 'SilverMetalBootstrapCleanup' /f"; + var b64 = Convert.ToBase64String(System.Text.Encoding.Unicode.GetBytes(cleanup)); + await Ps("schtasks /create /tn 'SilverMetalBootstrapCleanup' " + + $"/tr 'powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand {b64}' " + + "/sc onstart /ru SYSTEM /rl HIGHEST /f", ct); } private static string Esc(string s) => s.Replace("'", "''"); private Task Ps(string s, CancellationToken ct) => -- 2.39.5