Why does explorer.exe crash with a "stack-based buffer overrun" error after a Windows update, and is there a workaround?
Some users on Windows 11 24H2 and 25H2 VDI/RDS sessions are getting stuck at a blank desktop after logon — the wallpaper and taskbar never appear — following installation of a recent Windows cumulative update. This article explains why it happens, how to tell it apart from the related ShellHost.exe popup, and two candidate mitigations currently being validated — neither is yet confirmed to fully resolve the issue on its own, so this article will keep being updated as that changes.
📄 Contents
Error Message
Affected sessions log an Application Popup event (Event ID 26) at logon, at or near the moment the desktop should appear:
explorer.exe - System Error The system detected an overrun of a stack-based buffer in this application. This overrun could potentially allow a malicious user to gain control of this application.
It is Windows' standard message for a stack-cookie (/GS) protection check tripping inside explorer.exe itself. Explorer terminates and the desktop shell (wallpaper, taskbar, Start) never loads. Users can reach the desktop by manually starting
explorer.exe from Task Manager, but the issue recurs on the next logon for that profile.Why It's Intermittent
Reports vary from "every logon fails" to "most logons are fine, a few aren't" — both are consistent with the same underlying condition. Liquidware's crash-dump analysis of an affected session traced the fault to a stack buffer inside explorer.exe's own code, not to any Liquidware component. A stack buffer overrun of this kind typically fires only when the data being copied into that buffer exceeds a fixed size — so sessions are affected only when something in the user's shell/profile state (Start layout, pinned items, jump lists, and similar per-user shell data) is large or complex enough to cross that threshold.
That also explains why this shows up disproportionately in VDI/RDS environments running profile management software — ProfileUnity, FSLogix, Citrix UPM, and Omnissa DEM are all reported hitting this — the software isn't at fault, but roaming or rehydrating that same class of per-user shell state makes crossing the threshold more likely. It is not limited to Windows 11 25H2: 25H2 ships as an enablement package on top of the 24H2 servicing branch, so the same explorer.exe code — and the same regression — is present on updated 24H2 systems as well.
Likely Cause
A recent Windows cumulative update appears to have introduced a regression in how explorer.exe consumes the modern shell/Start UI framework — the WinUI3/XAML packages MicrosoftWindows.Client.CBS, Microsoft.UI.Xaml.CBS, and MicrosoftWindows.Client.Core. This is the same package family behind the separate, generally self-recovering ShellHost.exe framework-loading failure documented in the related article below. In explorer.exe's case, a mismatch or stale registration state for those packages after the update appears to drive Explorer into a code path that overflows a stack buffer instead of failing gracefully.
Possible Fix #1: Microsoft's Known Issue Rollback
Microsoft has acknowledged a related issue and released a Known Issue Rollback (KIR) — an official Group Policy–based mitigation, distributed as an MSI that registers ADMX templates you enable via Group Policy. Citrix documents this specifically for Desktop OS VDAs in CTX697101 — Issues with Microsoft Windows September 2026 Update, under "Black Screens on Login after installing KB5124008 / KB5120998 on Desktop OS VDAs." The relevant packages Citrix references are Windows 11 24H2, Windows 11 25H2 and Windows Server 2025 — KB5124008 260912_00472 Group Policy, and Windows 11 22H2 — KB5122880 260912_00473 Group Policy.
This is worth testing first since it's the Microsoft-sanctioned path — but treat it as a candidate, not a confirmed resolution. See Citrix's article (linked above) or Microsoft's own Known Issue Rollback documentation for the exact MSI download and Group Policy deployment steps for your OS version.
Possible Fix #2: Shell Package Re-Registration
Re-registering the three shell UI packages at logon, before Explorer starts, is a second candidate mitigation — also still being validated, and not yet confirmed to fully resolve the issue on its own. It may be worth testing on its own, or alongside the KIR above, particularly on sessions where the KIR alone hasn't held up. Deploy it as a ProfileUnity Application Launcher (or User Defined Script) rule with Timing set to During Configuration Execution, ahead of shell startup.
Like the KIR above, this is still being validated and is not yet confirmed to fully resolve the issue. Test it in a pilot pool against a known-affected golden image before rolling it out broadly, and keep monitoring for recurrence — a clean pilot run alone is not proof the underlying regression is resolved.
ProfileUnity's Application Launcher and User Defined Scripts can run elevated — under the logged-on user's own UAC-elevated (admin) token, not a plain standard-user token. If a script simply calls
Start-Process to relaunch the shell in that state, the child process inherits that same elevated token — silently handing the user an elevated Explorer with no UAC prompt. That's a privilege-escalation and compliance concern, not a cosmetic one, especially in regulated environments. Preferred fix: check "Execute without Elevation" on the Application Launcher rule itself (see step 2 below). With that checked, ProfileUnity runs the script as the user's normal, non-elevated token from the start, so there's nothing to strip out afterward.
Built-in safety net: the script also checks its own elevation at runtime and only de-elevates when it detects it's actually needed — e.g. the checkbox was missed, or a User Defined Script trigger that doesn't expose the option. In that case it de-elevates itself before relaunching the shell (see
Start-AsStandardUser in the code) using a one-shot Scheduled Task registered with /RL LIMITED, which Task Scheduler always runs at the user's normal (non-elevated) integrity level regardless of the creating process's own elevation. When the launcher is already non-elevated (checkbox set correctly), this step is skipped entirely and the shell is relaunched directly. Confirm your own environment's launcher context matches (the script logs identity, elevation state, and session ID on every run) and validate this behavior in a controlled, isolated test environment before deployment — log on as a standard (non-admin) test user, run the script, and confirm in the log and in Task Manager that the relaunched Explorer is running at the user's normal integrity level, not elevated, before rolling out to any pilot pool.
1. Create the script — save the following as Repair-ShellXamlPackages.ps1. Alongside the package re-registration, this version watches for explorer.exe with a sustained-alive check (so a brief flash-then-crash isn't mistaken for success), guards against overlapping runs, logs identity and elevation state, can log to a central share for easier fleet-wide troubleshooting, and de-elevates before relaunching the shell so the user doesn't end up with an elevated Explorer:
param(
[string]$LogShare = $null,
[int]$WaitSeconds = 15,
[int]$PollIntervalMs = 500,
[int]$AliveConfirmSeconds = 8,
[int]$GuardMinutes = 5
)
# Logging: central share with local fallback
$Log = $null
if ($LogShare) {
try {
$LogDir = "$LogShare\$($env:COMPUTERNAME)_$($env:USERNAME)"
if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir -Force -ErrorAction Stop | Out-Null }
$Log = "$LogDir\XamlFix.log"
} catch { $Log = $null }
}
if (-not $Log) { $Log = "$env:TEMP\XamlFix.log" }
Start-Transcript $Log -Append
Write-Output ("{0} log={1}" -f (Get-Date).ToString('HH:mm:ss.fff'), $Log)
# Identity / elevation diagnostic -- also confirms "Execute without Elevation" took effect
$IsAdmin = $false
try {
$Identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$Principal = New-Object Security.Principal.WindowsPrincipal($Identity)
$IsAdmin = $Principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
$SessId = (Get-Process -Id $PID).SessionId
Write-Output ("{0} identity={1} elevated={2} sessionId={3}" -f (Get-Date).ToString('HH:mm:ss.fff'), $Identity.Name, $IsAdmin, $SessId)
} catch { Write-Output ("{0} identity check failed: {1}" -f (Get-Date).ToString('HH:mm:ss.fff'), $_.Exception.Message) }
# Re-entrancy guard: skip if we already ran recently
$Guard = "$env:TEMP\XamlFix.guard"
if (Test-Path $Guard) {
$Age = (Get-Date) - (Get-Item $Guard).LastWriteTime
if ($Age.TotalMinutes -lt $GuardMinutes) {
Write-Output ("{0} second pass within {1:N1} min, exiting" -f (Get-Date).ToString('HH:mm:ss.fff'), $Age.TotalMinutes)
Stop-Transcript
exit
}
}
Set-Content -Path $Guard -Value (Get-Date).ToString('o') -Force
function Start-AsStandardUser {
# Launches a process at the user's normal (medium) integrity level even
# when this script itself is running elevated, via a one-shot Scheduled
# Task registered with /RL LIMITED (Task Scheduler always runs those at
# the user's standard token, regardless of the creator's own elevation).
param([Parameter(Mandatory)][string]$FilePath)
$TaskName = "LWL-DeElevate-$([guid]::NewGuid().ToString('N').Substring(0,8))"
$CurrentUser = "$env:USERDOMAIN\$env:USERNAME"
try {
& schtasks.exe /Create /TN $TaskName /TR "`"$FilePath`"" /SC ONCE /ST 23:59 `
/RU $CurrentUser /IT /RL LIMITED /F 2&1 | Out-Null
if ($LASTEXITCODE -ne 0) { throw "schtasks /Create exited $LASTEXITCODE" }
& schtasks.exe /Run /TN $TaskName 2&1 | Out-Null
if ($LASTEXITCODE -ne 0) { throw "schtasks /Run exited $LASTEXITCODE" }
Start-Sleep -Seconds 2
return $true
} catch {
Write-Output ("{0} de-elevated launch failed: {1}" -f (Get-Date).ToString('HH:mm:ss.fff'), $_.Exception.Message)
return $false
} finally {
& schtasks.exe /Delete /TN $TaskName /F 2$null | Out-Null
}
}
# Step 1: Re-register the shell UI packages
$Packages = @(
"C:\Windows\SystemApps\MicrosoftWindows.Client.CBS_cw5n1h2txyewy\AppxManifest.xml",
"C:\Windows\SystemApps\Microsoft.UI.Xaml.CBS_8wekyb3d8bbwe\AppxManifest.xml",
"C:\Windows\SystemApps\MicrosoftWindows.Client.Core_cw5n1h2txyewy\AppxManifest.xml"
)
foreach ($Package in $Packages) {
if (Test-Path $Package) {
Write-Output ("{0} registering {1}" -f (Get-Date).ToString('HH:mm:ss.fff'), $Package)
try { Add-AppxPackage -Register $Package -DisableDevelopmentMode -ErrorAction Stop }
catch { Write-Output ("{0} failed: {1}" -f (Get-Date).ToString('HH:mm:ss.fff'), $_.Exception.Message) }
} else {
Write-Output ("{0} not found, skipping: {1}" -f (Get-Date).ToString('HH:mm:ss.fff'), $Package)
}
}
# Step 2: Watch for explorer.exe, distinguishing never-started / died / alive
$Deadline = (Get-Date).AddSeconds($WaitSeconds)
$SeenAt = $null
$Action = 'timeout'
while ((Get-Date) -lt $Deadline) {
$p = Get-Process explorer -ErrorAction SilentlyContinue
if ($p) {
if (-not $SeenAt) {
$SeenAt = Get-Date
Write-Output ("{0} explorer started (pid {1})" -f $SeenAt.ToString('HH:mm:ss.fff'), ($p | Select-Object -First 1).Id)
}
if (((Get-Date) - $SeenAt).TotalSeconds -ge $AliveConfirmSeconds) { $Action = 'alive'; break }
} elseif ($SeenAt) {
$Action = 'died'; break
}
Start-Sleep -Milliseconds $PollIntervalMs
}
switch ($Action) {
'alive' { Write-Output ("{0} explorer stayed running for {1}s+, no action needed" -f (Get-Date).ToString('HH:mm:ss.fff'), $AliveConfirmSeconds) }
'died' { Write-Output ("{0} explorer started then exited, relaunching shell" -f (Get-Date).ToString('HH:mm:ss.fff')) }
'timeout' { Write-Output ("{0} explorer never appeared within {1}s, launching shell" -f (Get-Date).ToString('HH:mm:ss.fff'), $WaitSeconds) }
}
# Step 3: Relaunch the shell if it isn't confirmed alive
if ($Action -ne 'alive') {
if ($IsAdmin) {
Write-Output ("{0} running elevated -- de-elevating before relaunch (check 'Execute without Elevation' to avoid this hop)" -f (Get-Date).ToString('HH:mm:ss.fff'))
$DeElevated = Start-AsStandardUser -FilePath "$env:SystemRoot\System32\userinit.exe"
if (-not $DeElevated) {
Write-Output ("{0} WARNING: de-elevated relaunch failed; falling back to direct Start-Process (may inherit elevation)" -f (Get-Date).ToString('HH:mm:ss.fff'))
Start-Process "$env:SystemRoot\System32\userinit.exe"
}
} else {
Start-Process "$env:SystemRoot\System32\userinit.exe"
}
Start-Sleep -Seconds 5
$running = [bool](Get-Process explorer -ErrorAction SilentlyContinue)
Write-Output ("{0} after relaunch, explorer running: {1}" -f (Get-Date).ToString('HH:mm:ss.fff'), $running)
if (-not $running) {
Write-Output ("{0} WARNING: explorer still not detected after relaunch attempt" -f (Get-Date).ToString('HH:mm:ss.fff'))
}
}
Stop-Transcript2. Add it as a New Application Launcher Setting
- In the ProfileUnity Management Console, open the applicable configuration, go to Application Launcher, and add a New Application Launcher Setting.
- Set Filespec to
powershell.exeand Arguments to-ExecutionPolicy Bypass -File "Repair-ShellXamlPackages.ps1"— add-LogShare "\\yourserver\yourshare\dumps"to centralize logs across sessions instead of using local%TEMP%, or adjust-WaitSeconds/-AliveConfirmSecondsto tune timing. - Set Timing to During Configuration Execution and Frequency to Every Logon.
- Check Run Asynchronously. By default ProfileUnity waits for the application to terminate before continuing; this script can run for up to
WaitSecondsand would otherwise hold up logon while ProfileUnity waits on it. - Check Execute without Elevation — the preferred fix for the elevation warning above. Without it, the script's own built-in safety net still de-elevates before relaunching the shell, but checking this box avoids that extra step entirely.
- Deploy the script itself via the configuration's file/module distribution so it lands locally before the rule fires.
- Save and apply the configuration to a pilot desktop pool only first.
Check
XamlFix.log (in %TEMP% or your -LogShare path) on a test session after logon to confirm the packages registered successfully, and whether explorer was seen starting, dying, or never appearing at all — before expanding to more pools.Alternative: Watchdog-Only Script (No Re-Registration)
If you'd rather not re-register the AppX shell packages — for example, you're relying on the KIR above and just want a safety net, or package re-registration isn't necessary/desired in your environment — the same detection-and-relaunch logic is available on its own, without the package-registration step. Everything else (sustained-alive detection, re-entrancy guard, logging, and the same de-elevated relaunch described in the elevation warning above) works the same way.
Save the following as Watch-ExplorerShell.ps1. Its log location is a plain parameter rather than a fixed path — point it at a local folder such as C:\Temp (the default) or a UNC share of your choice, as long as the account running the script has write rights there:
param(
[string]$LogDir = "C:\Temp",
[int]$WaitSeconds = 15,
[int]$PollIntervalMs = 500,
[int]$AliveConfirmSeconds = 8,
[int]$GuardMinutes = 5
)
# Logging: chosen path (local folder or UNC share of your choice), with local %TEMP% fallback
$Log = $null
try {
if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir -Force -ErrorAction Stop | Out-Null }
$Log = Join-Path $LogDir "ShellWatch_$($env:COMPUTERNAME)_$($env:USERNAME).log"
} catch { $Log = "$env:TEMP\ShellWatch.log" }
Start-Transcript $Log -Append
Write-Output ("{0} log={1}" -f (Get-Date).ToString('HH:mm:ss.fff'), $Log)
# Identity / elevation diagnostic -- also confirms "Execute without Elevation" took effect
$IsAdmin = $false
try {
$Identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$Principal = New-Object Security.Principal.WindowsPrincipal($Identity)
$IsAdmin = $Principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
$SessId = (Get-Process -Id $PID).SessionId
Write-Output ("{0} identity={1} elevated={2} sessionId={3}" -f (Get-Date).ToString('HH:mm:ss.fff'), $Identity.Name, $IsAdmin, $SessId)
} catch { Write-Output ("{0} identity check failed: {1}" -f (Get-Date).ToString('HH:mm:ss.fff'), $_.Exception.Message) }
# Re-entrancy guard: skip if we already ran recently
$Guard = "$env:TEMP\ShellWatch.guard"
if (Test-Path $Guard) {
$Age = (Get-Date) - (Get-Item $Guard).LastWriteTime
if ($Age.TotalMinutes -lt $GuardMinutes) {
Write-Output ("{0} second pass within {1:N1} min, exiting" -f (Get-Date).ToString('HH:mm:ss.fff'), $Age.TotalMinutes)
Stop-Transcript
exit
}
}
Set-Content -Path $Guard -Value (Get-Date).ToString('o') -Force
function Start-AsStandardUser {
# Launches a process at the user's normal (medium) integrity level even
# when this script itself is running elevated, via a one-shot Scheduled
# Task registered with /RL LIMITED (Task Scheduler always runs those at
# the user's standard token, regardless of the creator's own elevation).
param([Parameter(Mandatory)][string]$FilePath)
$TaskName = "LWL-DeElevate-$([guid]::NewGuid().ToString('N').Substring(0,8))"
$CurrentUser = "$env:USERDOMAIN\$env:USERNAME"
try {
& schtasks.exe /Create /TN $TaskName /TR "`"$FilePath`"" /SC ONCE /ST 23:59 `
/RU $CurrentUser /IT /RL LIMITED /F 2&1 | Out-Null
if ($LASTEXITCODE -ne 0) { throw "schtasks /Create exited $LASTEXITCODE" }
& schtasks.exe /Run /TN $TaskName 2&1 | Out-Null
if ($LASTEXITCODE -ne 0) { throw "schtasks /Run exited $LASTEXITCODE" }
Start-Sleep -Seconds 2
return $true
} catch {
Write-Output ("{0} de-elevated launch failed: {1}" -f (Get-Date).ToString('HH:mm:ss.fff'), $_.Exception.Message)
return $false
} finally {
& schtasks.exe /Delete /TN $TaskName /F 2$null | Out-Null
}
}
# Watch for explorer.exe, distinguishing never-started / died / alive
$Deadline = (Get-Date).AddSeconds($WaitSeconds)
$SeenAt = $null
$Action = 'timeout'
while ((Get-Date) -lt $Deadline) {
$p = Get-Process explorer -ErrorAction SilentlyContinue
if ($p) {
if (-not $SeenAt) {
$SeenAt = Get-Date
Write-Output ("{0} explorer started (pid {1})" -f $SeenAt.ToString('HH:mm:ss.fff'), ($p | Select-Object -First 1).Id)
}
if (((Get-Date) - $SeenAt).TotalSeconds -ge $AliveConfirmSeconds) { $Action = 'alive'; break }
} elseif ($SeenAt) {
$Action = 'died'; break
}
Start-Sleep -Milliseconds $PollIntervalMs
}
switch ($Action) {
'alive' { Write-Output ("{0} explorer stayed running for {1}s+, no action needed" -f (Get-Date).ToString('HH:mm:ss.fff'), $AliveConfirmSeconds) }
'died' { Write-Output ("{0} explorer started then exited, relaunching shell" -f (Get-Date).ToString('HH:mm:ss.fff')) }
'timeout' { Write-Output ("{0} explorer never appeared within {1}s, launching shell" -f (Get-Date).ToString('HH:mm:ss.fff'), $WaitSeconds) }
}
# Relaunch the shell if it isn't confirmed alive
if ($Action -ne 'alive') {
if ($IsAdmin) {
Write-Output ("{0} running elevated -- de-elevating before relaunch (check 'Execute without Elevation' to avoid this hop)" -f (Get-Date).ToString('HH:mm:ss.fff'))
$DeElevated = Start-AsStandardUser -FilePath "$env:SystemRoot\System32\userinit.exe"
if (-not $DeElevated) {
Write-Output ("{0} WARNING: de-elevated relaunch failed; falling back to direct Start-Process (may inherit elevation)" -f (Get-Date).ToString('HH:mm:ss.fff'))
Start-Process "$env:SystemRoot\System32\userinit.exe"
}
} else {
Start-Process "$env:SystemRoot\System32\userinit.exe"
}
Start-Sleep -Seconds 5
$running = [bool](Get-Process explorer -ErrorAction SilentlyContinue)
Write-Output ("{0} after relaunch, explorer running: {1}" -f (Get-Date).ToString('HH:mm:ss.fff'), $running)
if (-not $running) {
Write-Output ("{0} WARNING: explorer still not detected after relaunch attempt" -f (Get-Date).ToString('HH:mm:ss.fff'))
}
}
Stop-TranscriptDeploy it the same way as Repair-ShellXamlPackages.ps1 above — Timing During Configuration Execution, Frequency Every Logon, with Run Asynchronously and Execute without Elevation both checked — substituting -File "Watch-ExplorerShell.ps1" and, if you want central logging, -LogDir "\\yourserver\yourshare\dumps" in place of the local C:\Temp default.
Recovering a Stuck Session
If a user is already stuck at a blank desktop:
- Press Ctrl+Alt+Delete and open Task Manager.
- Select File > Run new task, enter
explorer.exe, and confirm. - The desktop should load normally for the remainder of that session.
This does not prevent recurrence on the user's next logon. The Repair-ShellXamlPackages.ps1 script above already includes this recovery step (it waits and force-launches explorer.exe if needed), so sites running it as an Application Launcher rule get this fallback automatically.
Related Articles
- Citrix CTX697101 — Issues with Microsoft Windows September 2026 Update (documents the same black-screen-on-login issue on Desktop OS VDAs, and the official Microsoft KIR)
- ShellHost.exe System Error — "Overrun of a Stack-Based Buffer" Popup at Logon (the related, generally self-recovering variant of this same shell-package issue)
- ProfileUnity: How to Enable Debug Logging
- How to Collect Logs Using the Diagnostic Tool
- Enabling User-Mode Dumps in a ProfileUnity Configuration
| Date | Update |
|---|---|
| 9-14-26 | Initial publication. Interim workaround (shell UI package re-registration) added based on early field reports; not yet confirmed as a complete fix. |
| 9-14-26 (later) | Added: Microsoft/Citrix acknowledgment (CTX697101) and the Known Issue Rollback (KIR) as a second possible fix, alongside the shell package re-registration script. Reframed both as candidate mitigations still in validation — neither is currently confirmed to fully resolve the issue on its own. Field reports on the KIR are mixed; an out-of-band patch is reportedly pending. |
| 9-15-26 | Added a watchdog-only alternative script (Watch-ExplorerShell.ps1) for sites that want the detection/relaunch safety net without the AppX package re-registration step. |
| 9-15-26 (later) | Important correction: both scripts now de-elevate before relaunching the shell. ProfileUnity's Application Launcher runs under the logged-on user's UAC-elevated token, and without this fix the relaunched Explorer would silently inherit that elevation. Both scripts also now log identity/elevation/session state for verification. |
| 9-15-26 (later still) | Updated deployment steps to reference ProfileUnity's own Execute without Elevation and Run Asynchronously Application Launcher checkboxes (Timing: During Configuration Execution) as the preferred way to avoid elevation issues, with both scripts' built-in de-elevation logic now a self-checking safety net rather than the primary mechanism. |
| Product | Liquidware ProfileUnity with FlexApp |
| Applies To | Windows 11 24H2 / 25H2 VDI or RDS sessions, ProfileUnity 6.8.7.x and later |
| Updated | September 15, 2026 |