Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 209 additions & 39 deletions src/windows/win-ignoreAllFailures.ps1
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<#
.VERSION
v1.1: [Apr 2026] - Enhanced CIM logic for disk enumeration and partition discovery
v1.0: Initial commit - Sets BCD boot status policy to IgnoreAllFailures to break Automatic Repair loops
.SYNOPSIS
win-ignoreAllFailures.ps1 (v1.2) - Sets BCD bootstatuspolicy to IgnoreAllFailures to break Automatic Repair loops.

.DESCRIPTION
This script runs from a rescue VM to modify the BCD store on an attached faulty OS disk.
Expand All @@ -18,9 +17,17 @@

.NOTES
Name: win-ignoreAllFailures.ps1
Version: 1.1
Author: Tony.Mocanu@Microsoft.com
Version: 1.2
Author: Microsoft Azure Compute Support

.VERSION
v1.2: [May 2026] - Updated the script (current)
- Added guarded nested VM handling to prevent Get-VM failures when Hyper-V module is unavailable.
- Switched partition discovery from inline CIM enumeration to shared Get-Disk-Partitions-v2 helper.
- Added .SYNOPSIS header and aligned metadata versioning/documentation with current script behavior.
v1.1: [Apr 2026] - Enhanced CIM logic for disk enumeration and partition discovery
v1.0: Initial commit - Sets BCD boot status policy to IgnoreAllFailures to break Automatic Repair loops

.SCENARIO_RECREATION
To recreate a testable scenario on a rescue VM with an attached OS disk:
1. Create a test VM in Azure and attach its OS disk to a rescue VM.
Expand All @@ -37,11 +44,15 @@ bcdedit /store <bcdpath> /enum {default}
Expected: bootstatuspolicy = IgnoreAllFailures.

.EXAMPLE
az vm repair run -g <rg> -n <vm> --run-id win-ignoreAllFailures --run-on-repair
# Deploy and run on rescue VM
az vm repair run -g <resource-group> -n <vm-name> --run-id win-ignoreAllFailures --run-on-repair

# Verify BCD changes
Get-ChildItem "$env:USERPROFILE\Desktop\ignoreAllFailures_*.log" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content

.VERIFICATION
1. Check the log file for success:
Get-ChildItem "C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\ignoreAllFailures_*.log" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content
Get-ChildItem "$env:USERPROFILE\Desktop\ignoreAllFailures_*.log" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content
Expected: "BCD AFTER CHANGE" section shows bootstatuspolicy = IgnoreAllFailures, return code 0.
2. Manually verify the BCD store on the attached disk (replace F with the BCD partition letter):
bcdedit /store F:\boot\bcd /enum
Expand All @@ -51,22 +62,99 @@ bcdedit /store <bcdpath> /enum {default}
#>

# Initialization (no Param() block to avoid ParserErrors on legacy PowerShell engines)
. .\src\windows\common\setup\init.ps1
# $PSScriptRoot can be empty when invoked through ScriptBlock execution.
# Fall back to call stack script attribution to resolve the originating file directory.
$resolvedScriptRoot = $PSScriptRoot
if ([string]::IsNullOrEmpty($resolvedScriptRoot)) {
$originFrame = Get-PSCallStack -ErrorAction SilentlyContinue |
Where-Object { $_.ScriptName } |
Select-Object -First 1

if ($originFrame -and $originFrame.ScriptName) {
$resolvedScriptRoot = Split-Path -Parent $originFrame.ScriptName
}
}

if ([string]::IsNullOrEmpty($resolvedScriptRoot)) {
Write-Error 'Cannot determine script directory: PSScriptRoot is empty and call stack provides no path.'
return 1
}

$initPath = Join-Path -Path $resolvedScriptRoot -ChildPath 'common\setup\init.ps1'
$partitionsHelperPath = Join-Path -Path $resolvedScriptRoot -ChildPath 'common\helpers\Get-Disk-Partitions-v2.ps1'

$logDir = "C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension"
if (-not (Test-Path $logDir)) { $null = New-Item -ItemType Directory -Path $logDir -Force }
if (-not (Test-Path -Path $initPath -PathType Leaf)) {
Write-Error "Missing required dependency: $initPath"
return 1
}

. $initPath

if (-not (Test-Path -Path $partitionsHelperPath -PathType Leaf)) {
Log-Error "Missing required dependency: $partitionsHelperPath"
return $STATUS_ERROR
}

. $partitionsHelperPath

$desktopPath = [Environment]::GetFolderPath('Desktop')
if ([string]::IsNullOrWhiteSpace($desktopPath) -or -not (Test-Path $desktopPath)) {
$desktopPath = Join-Path $env:USERPROFILE 'Desktop'
}
if (-not (Test-Path $desktopPath)) {
$desktopPath = 'C:\Windows\Temp'
}
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$logFile = "$logDir\ignoreAllFailures_$timestamp.log"
$logFile = Join-Path $desktopPath "ignoreAllFailures_$timestamp.log"
if (-not (Test-Path $logFile)) { $null = New-Item -ItemType File -Path $logFile -Force }

$successReport = New-Object System.Collections.Generic.List[string]
$script_final_status = $STATUS_ERROR
$assignedEfiLetters = @() # Track EFI partition drive letters for cleanup
$processedDisks = 0
$skippedDisks = 0
$failedDisks = 0
$changedDisks = 0

function Get-FormattedOutput {
param([string]$text)
$time = Get-Date -Format "MM/dd/yyyy HH:mm:ss"
return "[Output $time]$text"
}

function Write-ScriptLog {
param(
[Parameter(Mandatory = $true)]
[string]$Message,
[ValidateSet('Info', 'Warning', 'Error')]
[string]$Level = 'Info'
)

switch ($Level) {
'Info' { Log-Info $Message }
'Warning' { Log-Warning $Message }
'Error' { Log-Error $Message }
}

Add-Content -Path $logFile -Value (Get-FormattedOutput $Message)
}

function Add-CommandOutput {
param(
[Parameter(Mandatory = $true)]
[string]$Header,
[Parameter(Mandatory = $true)]
[object[]]$Output
)

$successReport.Add((Get-FormattedOutput $Header))
foreach ($line in $Output) {
if ($null -ne $line -and -not [string]::IsNullOrWhiteSpace([string]$line)) {
$successReport.Add((Get-FormattedOutput ([string]$line)))
}
}
}

function Get-NextFreeDriveLetter {
$usedLetters = Get-PSDrive -PSProvider FileSystem | Select-Object -ExpandProperty Name
$letters = 90..69 | ForEach-Object { [char]$_ }
Expand All @@ -76,38 +164,58 @@ function Get-NextFreeDriveLetter {
}

try {
Log-Info "Starting IgnoreAllFailures script..." | Tee-Object -FilePath $logFile -Append

# Internal CIM Logic to replace the helper script
$physicalDisks = Get-CimInstance -ClassName Win32_DiskDrive
$partitionlist = foreach ($disk in $physicalDisks) {
$partitions = Get-CimInstance -Query "Associators of {Win32_DiskDrive.DeviceID='$($disk.DeviceID)'} Where AssocClass=Win32_DiskDriveToDiskPartition"
foreach ($partition in $partitions) {
$logicalDisks = Get-CimInstance -Query "Associators of {Win32_DiskPartition.DeviceID='$($partition.DeviceID)'} Where AssocClass=Win32_LogicalDiskToPartition"
foreach ($logicalDisk in $logicalDisks) {
[PSCustomObject]@{
DiskNumber = $disk.Index
DriveLetter = $logicalDisk.DeviceID.Replace(':', '')
Write-ScriptLog -Message "Starting IgnoreAllFailures script..."
Write-ScriptLog -Message "Desktop log file: $logFile"

# Stop nested guest VM if running
# Guard Get-VM if Hyper-V module is not available
try {
if (Get-Module -ListAvailable -Name Hyper-V) {
$guestHyperVVirtualMachine = Get-VM -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
if ($guestHyperVVirtualMachine) {
if ($guestHyperVVirtualMachine.State -eq 'Running') {
Write-ScriptLog -Message "Stopping nested guest VM $($guestHyperVVirtualMachine.VMName)"
try {
Stop-VM $guestHyperVVirtualMachine -ErrorAction Stop -Force
}
catch {
Write-ScriptLog -Level Warning -Message "Failed to stop nested guest VM $($guestHyperVVirtualMachine.VMName): $($_.Exception.Message). Continuing, but operations may have limited success."
}
}
}
} else {
Write-ScriptLog -Message "Hyper-V PowerShell module is not available on this host. Skipping nested VM validation."
}
}
catch {
Write-ScriptLog -Level Warning -Message "Nested VM check encountered an error but will be skipped: $($_.Exception.Message)"
}

$partitionlist = Get-Disk-Partitions

$rescueDrive = $env:SystemDrive -replace ':', ''
Log-Info "Starting deep scan for BCD files..." | Tee-Object -FilePath $logFile -Append
Write-ScriptLog -Message "Starting deep scan for BCD files..."

forEach ($diskGroup in $partitionlist | Group-Object DiskNumber) {
if ($diskGroup.Group.DriveLetter -contains $rescueDrive) { continue }
$processedDisks++
if ($diskGroup.Group.DriveLetter -contains $rescueDrive) {
$skippedDisks++
Write-ScriptLog -Message "Skipping rescue host disk $($diskGroup.Name)"
continue
}

$currentDiskBcdPath = $null
$currentDiskOsFound = $false

# EFI Mounter Logic
$hiddenPartitions = Get-Partition -DiskNumber $diskGroup.Name | Where-Object { $_.DriveLetter -eq 0 -and ($_.GptType -eq "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" -or $_.Type -eq "System") }
# Filter for hidden partitions: check for null/empty DriveLetter or DriveLetter -eq 0
$hiddenPartitions = Get-Partition -DiskNumber $diskGroup.Name | Where-Object { (-not $_.DriveLetter) -and ($_.GptType -eq "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" -or $_.Type -eq "System") }
foreach ($part in $hiddenPartitions) {
$newLetter = Get-NextFreeDriveLetter
Log-Info "Mounting hidden EFI partition on Disk $($diskGroup.Name) to $newLetter`:" | Tee-Object -FilePath $logFile -Append
Write-ScriptLog -Message "Mounting hidden EFI partition on Disk $($diskGroup.Name) to $newLetter`:"
$part | Set-Partition -NewDriveLetter $newLetter -ErrorAction SilentlyContinue
# Track assigned letter for cleanup in finally block
$assignedEfiLetters += @{ Letter = $newLetter; DiskNumber = $diskGroup.Name; Partition = $part }
Start-Sleep -Seconds 2
}

Expand All @@ -125,40 +233,102 @@ try {
}

if ($currentDiskBcdPath -and $currentDiskOsFound) {
$bcdout = bcdedit /store $currentDiskBcdPath /enum bootmgr /v
Write-ScriptLog -Message "Disk $($diskGroup.Name): candidate BCD store found at $currentDiskBcdPath"
$bcdout = bcdedit /store $currentDiskBcdPath /enum bootmgr /v 2>&1
Add-CommandOutput -Header "--- BCD BOOTMGR OUTPUT (Disk $($diskGroup.Name)) ---" -Output $bcdout

if ($LASTEXITCODE -ne 0) {
$failedDisks++
Write-ScriptLog -Level Error -Message "Disk $($diskGroup.Name): failed to query bootmgr from BCD store at $currentDiskBcdPath (exit code $LASTEXITCODE)"
continue
}

$defaultLine = $bcdout | Select-String 'displayorder' | Select-Object -First 1

if ($defaultLine -and ($defaultLine -match '\{([^}]+)\}')) {
$defaultId = $matches[0]

$successReport.Add((Get-FormattedOutput "--- BCD BEFORE CHANGE ---"))
$beforeLines = (bcdedit /store $currentDiskBcdPath /enum $defaultId) -split "`r`n"
foreach ($line in $beforeLines) { if($line) { $successReport.Add((Get-FormattedOutput $line)) } }
$beforeRaw = bcdedit /store $currentDiskBcdPath /enum $defaultId 2>&1
Add-CommandOutput -Header "--- BCD BEFORE CHANGE (Disk $($diskGroup.Name)) ---" -Output $beforeRaw
if ($LASTEXITCODE -ne 0) {
$failedDisks++
Write-ScriptLog -Level Error -Message "Disk $($diskGroup.Name): failed to read BCD entry $defaultId before change (exit code $LASTEXITCODE)"
continue
}

bcdedit /store $currentDiskBcdPath /default $defaultId | Out-Null
$null = bcdedit /store $currentDiskBcdPath /set $defaultId bootstatuspolicy IgnoreAllFailures
$setDefaultOutput = bcdedit /store $currentDiskBcdPath /default $defaultId 2>&1
Add-CommandOutput -Header "--- BCD SET DEFAULT OUTPUT (Disk $($diskGroup.Name)) ---" -Output $setDefaultOutput
if ($LASTEXITCODE -ne 0) {
$failedDisks++
Write-ScriptLog -Level Error -Message "Disk $($diskGroup.Name): failed to set default entry to $defaultId (exit code $LASTEXITCODE)"
continue
}

$setPolicyOutput = bcdedit /store $currentDiskBcdPath /set $defaultId bootstatuspolicy IgnoreAllFailures 2>&1
Add-CommandOutput -Header "--- BCD SET BOOTSTATUSPOLICY OUTPUT (Disk $($diskGroup.Name)) ---" -Output $setPolicyOutput
if ($LASTEXITCODE -ne 0) {
$failedDisks++
Write-ScriptLog -Level Error -Message "Disk $($diskGroup.Name): failed to set bootstatuspolicy IgnoreAllFailures on $defaultId (exit code $LASTEXITCODE)"
continue
}

$successReport.Add((Get-FormattedOutput "--- BCD AFTER CHANGE ---"))
$afterLines = (bcdedit /store $currentDiskBcdPath /enum $defaultId) -split "`r`n"
foreach ($line in $afterLines) { if($line) { $successReport.Add((Get-FormattedOutput $line)) } }
$afterRaw = bcdedit /store $currentDiskBcdPath /enum $defaultId 2>&1
Add-CommandOutput -Header "--- BCD AFTER CHANGE (Disk $($diskGroup.Name)) ---" -Output $afterRaw
if ($LASTEXITCODE -ne 0) {
$failedDisks++
Write-ScriptLog -Level Error -Message "Disk $($diskGroup.Name): failed to read BCD entry $defaultId after change (exit code $LASTEXITCODE)"
continue
}

$changedDisks++
$script_final_status = $STATUS_SUCCESS
Write-ScriptLog -Message "Disk $($diskGroup.Name): successfully set bootstatuspolicy IgnoreAllFailures for default entry $defaultId"
}
else {
$failedDisks++
Write-ScriptLog -Level Warning -Message "Disk $($diskGroup.Name): unable to identify default boot entry from displayorder"
}
}
else {
$skippedDisks++
Write-ScriptLog -Message "Disk $($diskGroup.Name): skipped because BCD store or OS loader was not found"
}
}
}
catch {
Log-Error "An error occurred: $($_.Exception.Message)" | Tee-Object -FilePath $logFile -Append
Write-ScriptLog -Level Error -Message "An error occurred: $($_.Exception.Message)"
$script_final_status = $STATUS_ERROR
}
finally {
# Cleanup: Remove temporarily assigned EFI drive letters to avoid leaving orphaned mounts on rescue host
if ($assignedEfiLetters.Count -gt 0) {
Write-ScriptLog -Message "Cleaning up temporarily assigned EFI partition drive letters..."
foreach ($efiMount in $assignedEfiLetters) {
try {
Write-ScriptLog -Message "Removing drive letter $($efiMount.Letter): from EFI partition (Disk $($efiMount.DiskNumber))"
$efiMount.Partition | Set-Partition -NewDriveLetter $null -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
}
catch {
Write-ScriptLog -Level Warning -Message "Failed to remove drive letter $($efiMount.Letter): from EFI partition, but continuing cleanup: $($_.Exception.Message)"
}
}
}

# Final logging of report via Log-Info (NOT Write-Output)
if ($successReport.Count -gt 0) {
foreach ($reportLine in $successReport) {
Log-Info $reportLine | Tee-Object -FilePath $logFile -Append
Write-ScriptLog -Message $reportLine
}
}
Log-Info "Script completed with status: $script_final_status" | Tee-Object -FilePath $logFile -Append

if ($changedDisks -eq 0) {
$script_final_status = $STATUS_ERROR
}

Write-ScriptLog -Message "Summary: processed=$processedDisks, skipped=$skippedDisks, failed=$failedDisks, changed=$changedDisks"
Write-ScriptLog -Message "Script completed with status: $script_final_status"
Write-ScriptLog -Message "Desktop log file: $logFile"
}

# Proper return for Azure Telemetry
Expand Down