feat(windows): force legacy Setup on 24H2 to fix hands-off install
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (push) Successful in 4m15s

VM test proved Win11 24H2 redesigned "ConX" Setup ignores the windowsPE pass of
autounattend.xml (manual language/keyboard/region prompts). Deep-research-verified
fix: patch sources\boot.wim index 2 to launch the legacy installer.

build.ps1 stage 2b: mount boot.wim idx2, load offline SYSTEM hive, set
HKLM\SYSTEM\Setup\CmdLine=X:\sources\setup.exe, unload, commit. Also place
autounattend.xml in \sources as well as ISO root. Legacy engine consumes all
four passes -> fully hands-off. Documented in iso-builder.md §3a (incl. rejected
winpeshl.ini / RunSynchronous alternatives + ConX-may-change caveat).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sysadmin
2026-06-08 23:20:37 +01:00
parent b4d303cbaa
commit 5e6303d48e
2 changed files with 60 additions and 4 deletions

View File

@@ -51,8 +51,9 @@ $oscdimg = Resolve-Tool 'oscdimg.exe' @(
'C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\*\Oscdimg\oscdimg.exe')
$m = Get-Content $Manifest -Raw | ConvertFrom-Json
$isoRoot = Join-Path $WorkDir 'iso' # writable copy of ISO contents
$mount = Join-Path $WorkDir 'mount' # WIM mount point
$null = New-Item -ItemType Directory -Force -Path $WorkDir,$mount,(Split-Path $OutputIso)
$mount = Join-Path $WorkDir 'mount' # install.wim mount point
$bootmnt = Join-Path $WorkDir 'bootmnt' # boot.wim mount point
$null = New-Item -ItemType Directory -Force -Path $WorkDir,$mount,$bootmnt,(Split-Path $OutputIso)
# --- 1. Verify input -------------------------------------------------------
function Invoke-VerifyInput {
@@ -82,6 +83,36 @@ function Invoke-Extract {
attrib -r "$isoRoot\*" /s /d 2>$null # clear read-only carried from the ISO
}
# --- 2b. Force legacy Setup (bypass the 24H2 "ConX" front-end) -------------
function Invoke-ForceLegacySetup {
Write-Stage 'Stage 2b: force legacy Setup (patch sources\boot.wim)'
# Win11 24H2/25H2 ship a redesigned WinPE Setup front-end (setup.exe ->
# SetupHost.exe -> SetupPrep.exe) that IGNORES the windowsPE pass of
# autounattend.xml (manual language/keyboard/region prompts). No answer-file
# element toggles it; the verified fix is to point WinPE's shell at the
# LEGACY installer via HKLM\SYSTEM\Setup\CmdLine in boot.wim index 2 (the
# Setup image). The legacy engine consumes all four passes -> hands-off.
$bootwim = Join-Path $isoRoot 'sources\boot.wim'
if (-not (Test-Path $bootwim)) { throw "boot.wim not found: $bootwim" }
Mount-WindowsImage -ImagePath $bootwim -Index 2 -Path $bootmnt | Out-Null
try {
$cmdline = 'X:\sources\setup.exe'
if (-not (Test-Path (Join-Path $bootmnt 'sources\setup.exe')) -and
(Test-Path (Join-Path $bootmnt 'setup.exe'))) { $cmdline = 'X:\setup.exe' }
$hive = Join-Path $bootmnt 'Windows\System32\config\SYSTEM'
& reg load 'HKLM\SM_BOOT' $hive | Out-Null
try {
& reg add 'HKLM\SM_BOOT\Setup' /v CmdLine /t REG_SZ /d $cmdline /f | Out-Null
Write-Host " WinPE Setup\CmdLine = $cmdline (legacy Setup forced)"
} finally {
[gc]::Collect(); Start-Sleep -Seconds 2
& reg unload 'HKLM\SM_BOOT' | Out-Null
}
} finally {
Dismount-WindowsImage -Path $bootmnt -Save | Out-Null
}
}
# --- 3. Service the WIM offline (DISM) -------------------------------------
function Invoke-ServiceWim {
Write-Stage 'Stage 3: offline-service install.wim'
@@ -131,8 +162,11 @@ function Invoke-ServiceWim {
# --- 4. Inject answer file -------------------------------------------------
function Invoke-InjectUnattend {
Write-Stage 'Stage 4: inject autounattend.xml at ISO root'
Copy-Item (Join-Path $PSScriptRoot 'autounattend\autounattend.xml') (Join-Path $isoRoot 'autounattend.xml') -Force
Write-Stage 'Stage 4: inject autounattend.xml (ISO root + \sources)'
$src = Join-Path $PSScriptRoot 'autounattend\autounattend.xml'
Copy-Item $src (Join-Path $isoRoot 'autounattend.xml') -Force
# Also place in \sources (the implicit search location the windowsPE pass uses).
Copy-Item $src (Join-Path $isoRoot 'sources\autounattend.xml') -Force
}
# --- 5. Brand --------------------------------------------------------------
@@ -175,6 +209,7 @@ function Invoke-Attest {
# --- orchestrate -----------------------------------------------------------
Invoke-VerifyInput
Invoke-Extract
Invoke-ForceLegacySetup
Invoke-ServiceWim
Invoke-InjectUnattend
Invoke-Brand

View File

@@ -36,6 +36,7 @@ Orchestrated by [`installer/build.ps1`](installer/build.ps1). Runs on a **Window
1. **Verify input** — assert the source ISO SHA-256 matches the pinned manifest (supply-chain integrity).
2. **Extract** — expand the ISO to a working directory.
2b. **Force legacy Setup** — patch `sources\boot.wim` (index 2) so WinPE launches the legacy installer (see note below). *Required on 24H2/25H2 for a hands-off install.*
3. **Service the WIM offline (DISM)**:
- Select the IoT Enterprise LTSC image index.
- `/Add-Driver` the GPD Pocket 4 driver pack (Strix Point GPU, sensors/auto-rotate, Wi-Fi, fingerprint).
@@ -51,6 +52,26 @@ Orchestrated by [`installer/build.ps1`](installer/build.ps1). Runs on a **Window
6. **Repack**`oscdimg` produces a UEFI-bootable ISO (efisys boot image, El Torito).
7. **Attest** — emit output ISO SHA-256, an **SBOM** (every component + version), and a signed build attestation (design-principle #4).
### 3a. Windows 11 24H2/25H2: forcing legacy Setup (critical)
Windows 11 **24H2 (build 26100) and 25H2** ship a **redesigned "ConX" Setup front-end** (`setup.exe → SetupHost.exe → SetupPrep.exe`, launched in WinPE) that **does not honor the `windowsPE` pass** of a root `autounattend.xml`. Symptom (confirmed on our VM test): the new *Select language/keyboard* pages and the OOBE *region* prompt require key presses despite the answer file. **No answer-file element toggles legacy vs new Setup.**
The verified, scriptable fix — applied by `build.ps1` stage 2b — is to point WinPE's shell at the **legacy** installer in `sources\boot.wim` **index 2** (the Setup image; index 1 is WinPE):
```
DISM /Mount-Image sources\boot.wim index 2 → mount
reg load HKLM\SM_BOOT <mount>\Windows\System32\config\SYSTEM
reg add HKLM\SM_BOOT\Setup /v CmdLine /t REG_SZ /d X:\sources\setup.exe /f
reg unload HKLM\SM_BOOT
DISM /Unmount-Image /commit
```
WinPE's winlogon launches whatever `HKLM\SYSTEM\Setup\CmdLine` holds; overriding it to `X:\sources\setup.exe` runs the legacy engine, which consumes all four passes (windowsPE/offlineServicing/specialize/oobeSystem) → fully hands-off. The answer file is placed at **both** the ISO root and `\sources\`.
**Rejected alternatives** (community-tested, unreliable): `winpeshl.ini` `[LaunchApp]` variants and the answer-file-embedded `RunSynchronous` reg trick (the latter only works if the answer file contains *nothing else*, else it reboot-loops WinPE).
**Caveat:** this is ConX-specific behaviour that Microsoft may change in a future cumulative update / ADK refresh — re-verify against the exact base media before each batch. Sources: ElevenForum/NTLite 24H225H2 threads + MS Learn setup-automation docs (the Learn pages predate the redesign and describe only the legacy mechanism). Open question: whether MS later ships a supported way to pre-answer the ConX front-end.
## 4. Where each hardening control is applied
The control domains in [`hardening-spec.md`](hardening-spec.md) split across three layers: