From 25b02d20ffa08937b9730fc3807cbbebf4969955 Mon Sep 17 00:00:00 2001 From: sysadmin Date: Tue, 9 Jun 2026 11:47:38 +0100 Subject: [PATCH 1/2] =?UTF-8?q?fix(welcome):=20eject=20optical=20install?= =?UTF-8?q?=20media=20before=20BitLocker=20enroll=20(it=20refuses=20TPM+PI?= =?UTF-8?q?N=20with=20bootable=20media=20present=20=E2=80=94=20found=20in?= =?UTF-8?q?=20live=20e2e)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/SilverOS.Welcome.Core/Apply/BitLockerService.cs | 5 +++++ .../tests/SilverOS.Welcome.Tests/ApplyServicesTests.cs | 3 +++ 2 files changed, 8 insertions(+) diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Apply/BitLockerService.cs b/windows/welcome/src/SilverOS.Welcome.Core/Apply/BitLockerService.cs index d6e0e1c..2551ccd 100644 --- a/windows/welcome/src/SilverOS.Welcome.Core/Apply/BitLockerService.cs +++ b/windows/welcome/src/SilverOS.Welcome.Core/Apply/BitLockerService.cs @@ -14,6 +14,11 @@ public sealed class BitLockerService(IProcessRunner runner) : IBitLockerService // 3. Remove any TPM-only protector (only once a TPM+PIN protector is confirmed present) // so the device actually requires the PIN at pre-boot. var script = string.Concat( + // Eject optical install media first — BitLocker -TpmAndPinProtector refuses to enroll + // while bootable CD/DVD media is present ("detected bootable media in the computer"). + "try { $s=New-Object -ComObject Shell.Application; ", + "$s.Namespace(17).Items() | Where-Object { $_.Type -match 'CD|DVD' } | ForEach-Object { try { $_.InvokeVerb('Eject') } catch {} } } catch {}; ", + "Start-Sleep -Seconds 3; ", "$fve='HKLM:\\SOFTWARE\\Policies\\Microsoft\\FVE'; ", "New-Item -Path $fve -Force | Out-Null; ", "New-ItemProperty -Path $fve -Name UseAdvancedStartup -Value 1 -PropertyType DWord -Force | Out-Null; ", diff --git a/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServicesTests.cs b/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServicesTests.cs index 133f2b2..8bf4a0f 100644 --- a/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServicesTests.cs +++ b/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServicesTests.cs @@ -81,6 +81,9 @@ public class ApplyServicesTests // Removes any TPM-only protector so the device requires the PIN at pre-boot. run.Verify(r => r.RunAsync("powershell.exe", It.Is(s => s.Contains("Remove-BitLockerKeyProtector")), It.IsAny())); + // Ejects optical install media first (BitLocker refuses to enroll with bootable media present). + run.Verify(r => r.RunAsync("powershell.exe", It.Is(s => + s.Contains("Shell.Application") && s.Contains("Eject")), It.IsAny())); } [Fact] -- 2.39.5 From bf21eababe35ed48bf7688cbc991d165eefbcdc9 Mon Sep 17 00:00:00 2001 From: sysadmin Date: Tue, 9 Jun 2026 12:15:56 +0100 Subject: [PATCH 2/2] fix(welcome): make bootstrap teardown best-effort (LogonCount=1 already disables auto-logon; cleanup must not fail the apply) --- .../Apply/BootstrapService.cs | 27 +++++++++---------- .../ApplyServicesTests.cs | 9 +++---- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs b/windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs index e367dcf..827d515 100644 --- a/windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs +++ b/windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs @@ -1,24 +1,21 @@ namespace SilverOS.Welcome.Core.Apply; public sealed class BootstrapService(IProcessRunner runner) : IBootstrapService { + // Teardown is BEST-EFFORT (unlike Account/BitLocker which are strict): the answer file's + // AutoLogon LogonCount=1 already neutralises auto-logon after the first logon (Windows clears + // AutoAdminLogon itself), so these Winlogon cleanups must not fail the whole apply. The op that + // matters — removing the sm-bootstrap account — runs regardless and is tolerant too. public async Task TearDownAsync(string bootstrapUser, CancellationToken ct = default) { - const string key = "'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon'"; - // DefaultPassword may legitimately be absent → keep its per-cmdlet -EA SilentlyContinue. - await Ps($"Set-ItemProperty {key} -Name AutoAdminLogon -Value 0; " + - $"Remove-ItemProperty {key} -Name DefaultPassword -EA SilentlyContinue", "Disable auto-logon", ct); + const string w = "'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon'"; + await Ps($"Set-ItemProperty -Path {w} -Name AutoAdminLogon -Value '0' -EA SilentlyContinue; " + + $"Remove-ItemProperty -Path {w} -Name DefaultPassword -EA SilentlyContinue; " + + $"Remove-ItemProperty -Path {w} -Name DefaultUserName -EA SilentlyContinue; " + + $"Remove-ItemProperty -Path {w} -Name DefaultDomainName -EA SilentlyContinue", ct); var u = Esc(bootstrapUser); - await Ps($"Remove-LocalUser -Name '{u}' -EA SilentlyContinue", "Remove bootstrap account", ct); + await Ps($"Remove-LocalUser -Name '{u}' -EA SilentlyContinue", ct); } private static string Esc(string s) => s.Replace("'", "''"); - - // $ErrorActionPreference='Stop' surfaces unexpected hard errors (e.g. a bad registry path); - // the intentional per-cmdlet -EA SilentlyContinue above still overrides it for the known - // best-effort cleanups. - private async Task Ps(string s, string operation, CancellationToken ct) - { - var r = await runner.RunAsync("powershell.exe", - $"-NoProfile -ExecutionPolicy Bypass -Command \"$ErrorActionPreference='Stop'; {s}\"", ct); - r.EnsureSuccess(operation); - } + private Task Ps(string s, CancellationToken ct) => + runner.RunAsync("powershell.exe", $"-NoProfile -ExecutionPolicy Bypass -Command \"{s}\"", ct); } diff --git a/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServicesTests.cs b/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServicesTests.cs index 8bf4a0f..c600977 100644 --- a/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServicesTests.cs +++ b/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServicesTests.cs @@ -33,12 +33,9 @@ public class ApplyServicesTests new BitLockerService(Fail().Object).EnableAsync("123456")); } - [Fact] - public async Task BootstrapService_throws_on_nonzero_exit() - { - await Assert.ThrowsAsync(() => - new BootstrapService(Fail().Object).TearDownAsync("sm-bootstrap")); - } + // Note: BootstrapService is intentionally best-effort (teardown cleanups must not fail the + // apply — auto-logon is already neutralised by the answer file's LogonCount=1), so it does + // NOT throw on a non-zero exit. [Fact] public async Task AccountService_creates_standard_daily_and_admin() -- 2.39.5