Skip to content

Add win-fix-bcd - #157

Open
Marcus Ferreira (mvaferreira) wants to merge 2 commits into
Azure:mainfrom
mvaferreira:rsl-win-fix-bcd
Open

Marcus Ferreira (mvaferreira) wants to merge 2 commits into
Azure:mainfrom
mvaferreira:rsl-win-fix-bcd

Conversation

@mvaferreira

Copy link
Copy Markdown

What this adds

win-fix-bcd — a single repair scenario, 1025 lines, plus its own map.json entry.

Repairs the boot configuration of an offline Windows disk so the firmware can find and start Windows again. Targets the "Boot Configuration Data" family of failures, not a damaged partition.

The catalog entry a support engineer reads when choosing it:

Repairs a boot configuration that stops Windows starting (0xC000000E, 0xC000000F, 0xC0000225, 0xC0000034) - a BCD entry pointing at a partition that no longer exists after a disk swap, a wrong loader path or systemroot, a default entry that starts Windows Setup, an imcdevice value that bugchecks 0x67, or a missing store that has to be rebuilt with bcdboot. Pass detectOnly=true to report only, revert=true to restore the previous store, or rebuild=true to force a full bcdboot rebuild instead of a targeted repair. NOTE: use option --run-on-repair.

How it works

Runs against the broken OS disk attached to a rescue VM by "az vm repair create". It answers the question "why can the boot manager not start this Windows installation" using only evidence read from the offline disk, and then changes only what the evidence names.

The script repairs in two tiers, and which tier runs is decided by the evidence:

Tier 1, targeted. The BCD store is structurally sound and individual values are wrong, for
example the loader points at a partition that no longer exists after a disk swap. Each wrong
value is corrected with its own "bcdedit /set". Everything the store carries that is not a
finding is left exactly as it was, so serial console settings, custom timeouts and any other
deliberate configuration survive the repair.

Tier 2, rebuild. The store is missing, unreadable, or carries no Windows Boot Loader entry at
all, so there is nothing to correct. The store is rebuilt with bcdboot and the Azure serial
console settings are applied to the fresh store. This is deliberately the second choice: a
rebuild discards any customisation the old store held.

Parameters

Parameter Effect
detectOnly "true" to report the findings and make no writes at all. Defaults to "false".
rebuild "true" to force the tier 2 rebuild even when the store could be repaired in place. Use it when the store is so inconsistent that targeted repair is not trusted. Defaults to "false", because a rebuild discards customisation that a targeted repair preserves.
revert "true" to restore the BCD store this script backed up on its last run and undo the Active flag change, using the manifest it wrote. Defaults to "false".
windowsDrive Drive letter of the offline Windows installation, for example "F". Only needed when more than one Windows installation is attached and the automatically selected one is not the right one.

Conventions followed

  • Dot-sources .\src\windows\common\setup\init.ps1 and returns $STATUS_SUCCESS or $STATUS_ERROR.
  • Logging goes through the logger functions only; no Write-Host.
  • The detect summary is printed after the per-finding list, because az vm run-command keeps only the last 4096 characters of the output stream, so a summary printed first is the first thing a long run loses.
  • Evidence-driven: findings are gathered first and only what the evidence names is changed, so a healthy image produces no writes.
  • PSScriptAnalyzer 1.24.0: zero errors, 3 warnings, all PSUseShouldProcessForStateChangingFunctions on internal helper functions. The script is invoked non-interactively by az vm repair run, so -WhatIf/-Confirm on an internal function are unreachable; the reporting-only mode is the detectOnly parameter instead.

Testing

Exercised through the az vm repair run --preview product path against real Azure VMs: a lab VM is broken deliberately to create the fault, the repair is run against the attached disk from a rescue VM, and the VM is confirmed to boot and behave afterwards. Detection, restraint on a healthy image, and the repair itself are each checked.

Series

Second wave of four independent scenario PRs, after #149, #150, #151 and #152. The shared helpers
these scripts dot-source are already merged in #143, #146 and #147. This PR adds no helper files and
changes no existing scenario; its only existing-file change is appending this run-id to map.json,
inserted into whatever upstream carries now so every existing entry is preserved byte for byte.

Marcus Ferreira and others added 2 commits September 16, 2026 09:02
Repairs the boot configuration of an offline Windows disk so the firmware can find and start Windows again. Targets the "Boot Configuration Data" family of failures, not a damaged partition.
- Get-BcdEntryFinding consumed Get-BcdInventory without checking EnumSucceeded.
  When bcdedit /enum all fails the helper returns Loaders=@(), which is identical
  to a store with no loader entry, so a store that was merely locked or
  access-denied was renamed and rebuilt, discarding the serial console settings
  and timeouts the script promises to preserve. An unreadable store is now a
  distinct non-repairable finding that reports the bcdedit exit code.
- The boot manager branch escalated any binary that was not IsLikelyMicrosoft to
  a destructive rebuild. Test-OfflineFileSignature returns Confidence='None' with
  IsLikelyMicrosoft=false for a compressed boot stub, which is the ordinary result
  for Gen1 bootmgr, so a healthy Gen1 image could have its store rebuilt and then
  fail the post-repair re-detect. Only a definitive negative (Confidence='High'
  and not Microsoft), a missing file or a zero-byte file now escalates.
- Invoke-Revert acted on absolute paths and a disk number read from a manifest on
  the broken disk with no validation. It now requires the manifest to match the
  store path and disk number this run resolved, and passes both paths through
  Assert-OfflineTarget before copying.
- The revert manifest is written before the first write instead of after the last
  one, so a failure part way through no longer leaves a modified store with
  nothing recorded to put back. The catch block now names the backup.
- bcdboot is invoked directly with an argument array instead of through
  cmd.exe /c, which expanded %VAR% inside the quoted Windows path.
- Refuse to guess when several Windows installations are attached and no
  windowsDrive was supplied, matching win-enable-nvme-boot-driver.
- Added the missing finally: release the drive letters Get-OfflineWindowsDisk
  assigned and flush the log buffer on the failure paths too.
- BootPartitionActive findings are marked repaired after their own attempt.
- Header no longer points at run ids that may not exist in the installed library.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b4446db8-dda7-4210-a934-5b9ae9e3fb15
@mvaferreira

Copy link
Copy Markdown
Author

Review findings addressed — win-fix-bcd

A structured review was run against head 057d5a2a and reported 3 Criticals and 5 Warnings. All 8
were confirmed against the source and all 8 resulted in code changes. Every change was then
validated end to end on a throwaway Azure VM through the real az vm repair path before this was
pushed.

Final tested head: cdf4a13

Fixes implemented

ID Finding Change
C1 Get-BcdInventory output consumed without an EnumSucceeded guard, so a store that could not be read was treated as empty and destroyed a failed enumeration now raises BcdStoreUnreadable with -Tier 'None'; the script refuses rather than rebuilding over a store it could not read
C2 Revert manifest values used unvalidated — arbitrary overwrite, and the rescue VM's own active flag could be set Invoke-Revert validates the manifest before acting: StorePath and DiskNumber must match the disk in hand, and Assert-OfflineTarget guards both write paths
C3 The -not $signature.IsLikelyMicrosoft branch escalated an inconclusive signature check to a full store Rebuild that branch removed; an inconclusive signature no longer justifies a rebuild
W1 No finally: drive letters leaked and the log was not flushed on failure finally added, with $backupPath hoisted so it is in scope
W2 Dangling run-id references (win-fix-boot-partition, win-fix-code-integrity) corrected to ids that exist in map.json
W3 Revert manifest written after the repair, and the backup path was not named in the catch Save-RevertManifest -ActivatedPartition 0 is written before the first write; the catch names the backup file
W4 Multiple Windows candidates were not refused refuses when @($offline.Candidates).Count -gt 1 rather than guessing
W5 bcdboot invoked through cmd.exe, exposing the command line to percent expansion invoked directly as & bcdboot.exe … 2>&1, no cmd.exe

Validation

Break → detect → repair → detect → az vm repair restore → confirm the guest boots, all through
the product path with --preview pointing at the candidate branch.

Image Generation / layout Build Result
WindowsServer:2022-datacenter-g2 Gen2 / UEFI 20348 11/11 passed

The two changes most likely to over-tighten were specifically exercised:

  • C3 did not suppress a legitimate rebuild. With the BCD store genuinely missing, the script
    still escalated correctly — "Repair would rebuild the store with bcdboot, because there is no
    sound entry left to correct"
    — rebuilt it, and the post-repair detect reported "the boot
    configuration on this disk is consistent"
    . The VM booted.
  • W5 produced a working store. Calling bcdboot directly rather than through cmd.exe yielded
    a bootable UEFI configuration on a Gen2 disk.
Provenance
  • Reviewed head: 057d5a2a375e92b1540f00576beb694e602535fd
  • Final tested head: cdf4a13
  • Review prompt: PR-Review-Agent.md, SHA256 5451BA5C325F61E34FE63B9AF99322A17D70E55210CAF23F5CE27E0F839D8F70
  • Counts derived from the report tables: 3 Criticals, 5 Warnings (7 Info items not itemised here)
  • The original agent report is tied to head 057d5a2a and is historical: the code has changed since.
  • Quality gates on the final head: PowerShell 7 and 5.1 parse clean, PSSAnalyzer at parity with the
    reviewed head, repository pre-commit hook passing, CRLF line endings preserved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant