mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
fix(desktop): recover Windows GPU sandbox 0x80000003 startup crashes
Grant ALL APPLICATION PACKAGES RX on the unpacked app and stick a boot marker so fatal Chromium sandbox deaths relaunch with --no-sandbox (#38216).
This commit is contained in:
parent
e5afc0d93b
commit
e53d2dfccb
3 changed files with 395 additions and 0 deletions
|
|
@ -144,6 +144,17 @@ import {
|
|||
getVenvSitePackagesEntries,
|
||||
resolveVenvHermesCommand
|
||||
} from './windows-hermes-path'
|
||||
import {
|
||||
alreadyHasNoSandbox,
|
||||
buildNoSandboxRelaunchArgs,
|
||||
grantAllApplicationPackagesAcl,
|
||||
markerAfterSuccessfulBoot,
|
||||
nextSandboxMarkerAfterLaunchDecision,
|
||||
readSandboxMarker,
|
||||
shouldEnableWindowsNoSandbox,
|
||||
shouldRelaunchForGpuSandboxCrash,
|
||||
writeSandboxMarker
|
||||
} from './windows-sandbox-fallback'
|
||||
import { installWindowsSystemCaTrust } from './windows-system-ca'
|
||||
import { readWindowsUserEnvVar } from './windows-user-env'
|
||||
import { isPackagedInstallPath as isPackagedInstallPathUnderRoots } from './workspace-cwd'
|
||||
|
|
@ -204,6 +215,88 @@ if (IS_WSL && !REMOTE_DISPLAY_REASON && fs.existsSync('/dev/dxg')) {
|
|||
console.log('[hermes] WSL GPU passthrough (/dev/dxg) detected; enabling GPU acceleration')
|
||||
}
|
||||
|
||||
// Windows sandbox / GPU breakpoint crash recovery (#38216).
|
||||
//
|
||||
// Some hosts (AMD RX 6000 drivers, orphan AppContainer SIDs under %LOCALAPPDATA%,
|
||||
// missing S-1-15-2-2 ACEs) kill Chromium's sandboxed GPU/renderer children with
|
||||
// 0x80000003. After enough GPU deaths the browser process FATAL-exits before the
|
||||
// UI is usable. Must run before app `ready` so `--no-sandbox` applies to child
|
||||
// processes. The sticky marker recovers Start Menu / shortcut launches that
|
||||
// never go through `hermes desktop`.
|
||||
let windowsSandboxFallbackActive = false
|
||||
let windowsNoSandboxRelaunchAttempted = false
|
||||
|
||||
if (IS_WINDOWS) {
|
||||
const windowsUserData = app.getPath('userData')
|
||||
const exeDir = path.dirname(process.execPath)
|
||||
|
||||
// Best-effort ACL repair first — may be enough without disabling the sandbox.
|
||||
for (const target of [exeDir, windowsUserData]) {
|
||||
const acl = grantAllApplicationPackagesAcl(target, { execFileSync })
|
||||
|
||||
if (acl.ok) {
|
||||
console.log(`[hermes] granted ALL APPLICATION PACKAGES RX on ${target}`)
|
||||
} else if (acl.error && acl.error !== 'missing-target-or-exec') {
|
||||
console.warn(`[hermes] AppContainer ACL grant failed on ${target}: ${acl.error}`)
|
||||
}
|
||||
}
|
||||
|
||||
const priorMarker = readSandboxMarker(windowsUserData)
|
||||
const sandboxDecision = shouldEnableWindowsNoSandbox({
|
||||
argv: process.argv,
|
||||
env: process.env,
|
||||
marker: priorMarker
|
||||
})
|
||||
|
||||
windowsSandboxFallbackActive = sandboxDecision.enable
|
||||
|
||||
if (sandboxDecision.enable && sandboxDecision.reason !== 'already-enabled') {
|
||||
app.commandLine.appendSwitch('no-sandbox')
|
||||
process.env.ELECTRON_DISABLE_SANDBOX = '1'
|
||||
console.log(
|
||||
`[hermes] Windows sandbox fallback enabled (${sandboxDecision.reason}); launching with --no-sandbox (#38216)`
|
||||
)
|
||||
}
|
||||
|
||||
writeSandboxMarker(windowsUserData, nextSandboxMarkerAfterLaunchDecision({
|
||||
enabledNoSandbox: windowsSandboxFallbackActive
|
||||
}))
|
||||
|
||||
// Catch the first GPU breakpoint death and relaunch before Chromium's
|
||||
// "GPU process isn't usable" FATAL abort ends the process with no recovery.
|
||||
app.on('child-process-gone', (_event, details) => {
|
||||
if (
|
||||
!shouldRelaunchForGpuSandboxCrash({
|
||||
details,
|
||||
alreadyNoSandbox: windowsSandboxFallbackActive || alreadyHasNoSandbox(process.argv, process.env),
|
||||
relaunchAttempted: windowsNoSandboxRelaunchAttempted
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
windowsNoSandboxRelaunchAttempted = true
|
||||
windowsSandboxFallbackActive = true
|
||||
|
||||
try {
|
||||
writeSandboxMarker(app.getPath('userData'), { state: 'fallback' })
|
||||
} catch {
|
||||
void 0
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`[hermes] Windows GPU sandbox crashed (exit=${details?.exitCode}); relaunching once with --no-sandbox (#38216)`
|
||||
)
|
||||
|
||||
try {
|
||||
app.relaunch({ args: buildNoSandboxRelaunchArgs(process.argv.slice(1)) })
|
||||
app.exit(0)
|
||||
} catch (error) {
|
||||
console.error(`[hermes] --no-sandbox relaunch failed: ${error?.message || error}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ipcMain.handle('hermes:get-remote-display-reason', () => REMOTE_DISPLAY_REASON)
|
||||
|
||||
// Keep the renderer running at full speed while the window is in the background
|
||||
|
|
@ -7325,6 +7418,20 @@ function createWindow() {
|
|||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.show()
|
||||
}
|
||||
|
||||
// #38216: clear the mid-boot marker only after a window is actually usable.
|
||||
// Keep sticky `fallback` when we launched with --no-sandbox so the next
|
||||
// Start Menu click does not re-enter the GPU FATAL crash loop.
|
||||
if (IS_WINDOWS) {
|
||||
try {
|
||||
writeSandboxMarker(
|
||||
app.getPath('userData'),
|
||||
markerAfterSuccessfulBoot({ fallbackActive: windowsSandboxFallbackActive })
|
||||
)
|
||||
} catch (error) {
|
||||
rememberLog(`[sandbox] marker update after ready-to-show failed: ${error?.message || error}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.on('will-enter-full-screen', () => sendWindowStateChanged(true))
|
||||
|
|
@ -9257,6 +9364,16 @@ function configureSpellChecker() {
|
|||
}
|
||||
|
||||
app.on('before-quit', () => {
|
||||
// Clean quit mid-boot should not trip next-launch --no-sandbox (#38216).
|
||||
// FATAL GPU aborts skip before-quit, leaving the `booting` marker in place.
|
||||
if (IS_WINDOWS && !windowsSandboxFallbackActive) {
|
||||
try {
|
||||
writeSandboxMarker(app.getPath('userData'), markerAfterSuccessfulBoot({ fallbackActive: false }))
|
||||
} catch {
|
||||
void 0
|
||||
}
|
||||
}
|
||||
|
||||
// The always-on-top overlay isn't a "real" app window; close it so a stray
|
||||
// pet can't keep the process alive or float over a quit app.
|
||||
closePetOverlay()
|
||||
|
|
|
|||
261
apps/desktop/electron/windows-sandbox-fallback.ts
Normal file
261
apps/desktop/electron/windows-sandbox-fallback.ts
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
/**
|
||||
* Windows Chromium/Electron sandbox recovery for #38216.
|
||||
*
|
||||
* On some Windows hosts the GPU/renderer sandboxes die with STATUS_BREAKPOINT
|
||||
* (`0x80000003` / exit `-2147483645`). Chromium then FATAL-exits
|
||||
* ("GPU process isn't usable. Goodbye.") before the UI is usable.
|
||||
*
|
||||
* Two complementary recoveries, both scoped to win32:
|
||||
*
|
||||
* 1. Grant `S-1-15-2-2` (ALL APPLICATION PACKAGES) RX on the install /
|
||||
* userData trees. Missing that ACE plus orphan AppContainer SIDs is a
|
||||
* known Chromium CHECK failure (electron/electron#51761).
|
||||
* 2. Sticky boot marker: write `booting` before sandbox bring-up; clear it
|
||||
* only on a clean ready. An uncleared marker on the next launch means the
|
||||
* previous process aborted mid-boot → enable `--no-sandbox` (the only flag
|
||||
* reporters verified as fully stable for AMD RX 6000 / hybrid GPU hosts).
|
||||
*
|
||||
* Pure helpers stay injectable so tests never boot Electron or touch real ACLs.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
export const WINDOWS_SANDBOX_MARKER_FILENAME = 'windows-sandbox-fallback.json'
|
||||
|
||||
/** Well-known SID for "ALL APPLICATION PACKAGES". */
|
||||
export const ALL_APPLICATION_PACKAGES_SID = 'S-1-15-2-2'
|
||||
|
||||
/** STATUS_BREAKPOINT as a signed Win32 exit code (WER / Chromium). */
|
||||
export const WINDOWS_SANDBOX_BREAKPOINT_EXIT = -2147483645
|
||||
|
||||
export type SandboxMarkerState = 'booting' | 'fallback' | 'ok'
|
||||
|
||||
export interface SandboxMarker {
|
||||
state: SandboxMarkerState
|
||||
}
|
||||
|
||||
export function sandboxMarkerPath(userDataDir: string): string {
|
||||
return path.join(String(userDataDir || ''), WINDOWS_SANDBOX_MARKER_FILENAME)
|
||||
}
|
||||
|
||||
export function isWindowsSandboxBreakpointExit(exitCode: unknown): boolean {
|
||||
const n = Number(exitCode)
|
||||
|
||||
if (!Number.isFinite(n)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Signed STATUS_BREAKPOINT, or the same 32-bit pattern as unsigned.
|
||||
return n === WINDOWS_SANDBOX_BREAKPOINT_EXIT || (n >>> 0) === 0x80000003
|
||||
}
|
||||
|
||||
export function alreadyHasNoSandbox(
|
||||
argv: readonly string[] = [],
|
||||
env: NodeJS.ProcessEnv = process.env
|
||||
): boolean {
|
||||
if (Array.isArray(argv) && argv.some(arg => arg === '--no-sandbox')) {
|
||||
return true
|
||||
}
|
||||
|
||||
const disable = String(env.ELECTRON_DISABLE_SANDBOX || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
|
||||
return disable === '1' || disable === 'true' || disable === 'yes' || disable === 'on'
|
||||
}
|
||||
|
||||
export function parseSandboxMarker(raw: unknown): SandboxMarker | null {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const state = (raw as { state?: unknown }).state
|
||||
|
||||
if (state === 'booting' || state === 'fallback' || state === 'ok') {
|
||||
return { state }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function readSandboxMarker(
|
||||
userDataDir: string,
|
||||
{ readFileSync = fs.readFileSync } = {}
|
||||
): SandboxMarker | null {
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(sandboxMarkerPath(userDataDir), 'utf8'))
|
||||
|
||||
return parseSandboxMarker(raw)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function writeSandboxMarker(
|
||||
userDataDir: string,
|
||||
marker: SandboxMarker,
|
||||
{
|
||||
mkdirSync = fs.mkdirSync,
|
||||
writeFileSync = fs.writeFileSync
|
||||
}: {
|
||||
mkdirSync?: typeof fs.mkdirSync
|
||||
writeFileSync?: typeof fs.writeFileSync
|
||||
} = {}
|
||||
): void {
|
||||
const dir = String(userDataDir || '')
|
||||
|
||||
if (!dir) {
|
||||
return
|
||||
}
|
||||
|
||||
mkdirSync(dir, { recursive: true })
|
||||
writeFileSync(sandboxMarkerPath(dir), `${JSON.stringify(marker)}\n`, 'utf8')
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether this Windows launch should disable the Chromium sandbox.
|
||||
*
|
||||
* `booting` left from a prior launch → previous process aborted before ready.
|
||||
* `fallback` → we already recovered once; keep the workaround sticky so the
|
||||
* Start Menu shortcut does not crash every other launch.
|
||||
*/
|
||||
export function shouldEnableWindowsNoSandbox(options: {
|
||||
platform?: NodeJS.Platform | string
|
||||
argv?: readonly string[]
|
||||
env?: NodeJS.ProcessEnv
|
||||
marker?: SandboxMarker | null
|
||||
} = {}): { enable: boolean; reason: string | null } {
|
||||
if ((options.platform ?? process.platform) !== 'win32') {
|
||||
return { enable: false, reason: null }
|
||||
}
|
||||
|
||||
const argv = options.argv ?? process.argv
|
||||
const env = options.env ?? process.env
|
||||
|
||||
if (alreadyHasNoSandbox(argv, env)) {
|
||||
return { enable: true, reason: 'already-enabled' }
|
||||
}
|
||||
|
||||
const state = options.marker?.state
|
||||
|
||||
if (state === 'booting') {
|
||||
return { enable: true, reason: 'uncleared-boot-marker' }
|
||||
}
|
||||
|
||||
if (state === 'fallback') {
|
||||
return { enable: true, reason: 'sticky-fallback' }
|
||||
}
|
||||
|
||||
return { enable: false, reason: null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Marker to persist immediately after the launch decision, before GPU/sandbox
|
||||
* child processes start. Successful boots rewrite this later.
|
||||
*/
|
||||
export function nextSandboxMarkerAfterLaunchDecision(options: {
|
||||
enabledNoSandbox: boolean
|
||||
}): SandboxMarker {
|
||||
if (options.enabledNoSandbox) {
|
||||
return { state: 'fallback' }
|
||||
}
|
||||
|
||||
return { state: 'booting' }
|
||||
}
|
||||
|
||||
/**
|
||||
* After the main window reaches ready-to-show: keep sticky fallback when we
|
||||
* launched with `--no-sandbox`, otherwise mark a clean boot so the next
|
||||
* launch can try the sandbox again (e.g. after an ACL grant fixed the host).
|
||||
*/
|
||||
export function markerAfterSuccessfulBoot(options: { fallbackActive: boolean }): SandboxMarker {
|
||||
return options.fallbackActive ? { state: 'fallback' } : { state: 'ok' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Build `icacls` argv that grants ALL APPLICATION PACKAGES RX with inheritance.
|
||||
* `/T` applies to existing children (win-unpacked DLLs); `/C` continues on
|
||||
* errors; `/Q` stays quiet for installer logs.
|
||||
*/
|
||||
export function buildIcaclsGrantArgs(targetDir: string): string[] {
|
||||
return [
|
||||
String(targetDir),
|
||||
'/grant',
|
||||
`*${ALL_APPLICATION_PACKAGES_SID}:(OI)(CI)(RX)`,
|
||||
'/T',
|
||||
'/C',
|
||||
'/Q'
|
||||
]
|
||||
}
|
||||
|
||||
export function grantAllApplicationPackagesAcl(
|
||||
targetDir: string,
|
||||
{
|
||||
platform = process.platform,
|
||||
execFileSync
|
||||
}: {
|
||||
platform?: NodeJS.Platform | string
|
||||
execFileSync?: (file: string, args: readonly string[], options?: object) => Buffer | string
|
||||
} = {}
|
||||
): { ok: boolean; error?: string } {
|
||||
if (platform !== 'win32') {
|
||||
return { ok: false }
|
||||
}
|
||||
|
||||
const dir = String(targetDir || '').trim()
|
||||
|
||||
if (!dir || typeof execFileSync !== 'function') {
|
||||
return { ok: false, error: 'missing-target-or-exec' }
|
||||
}
|
||||
|
||||
try {
|
||||
execFileSync('icacls', buildIcaclsGrantArgs(dir), {
|
||||
windowsHide: true,
|
||||
timeout: 30_000,
|
||||
stdio: 'ignore'
|
||||
})
|
||||
|
||||
return { ok: true }
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a GPU child died with the #38216 breakpoint signature and we
|
||||
* should one-shot relaunch with `--no-sandbox` before Chromium FATAL-exits.
|
||||
*/
|
||||
export function shouldRelaunchForGpuSandboxCrash(options: {
|
||||
platform?: NodeJS.Platform | string
|
||||
details?: { type?: string; exitCode?: number | string } | null
|
||||
alreadyNoSandbox?: boolean
|
||||
relaunchAttempted?: boolean
|
||||
}): boolean {
|
||||
if ((options.platform ?? process.platform) !== 'win32') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (options.alreadyNoSandbox || options.relaunchAttempted) {
|
||||
return false
|
||||
}
|
||||
|
||||
const type = String(options.details?.type || '').toLowerCase()
|
||||
|
||||
if (type !== 'gpu') {
|
||||
return false
|
||||
}
|
||||
|
||||
return isWindowsSandboxBreakpointExit(options.details?.exitCode)
|
||||
}
|
||||
|
||||
export function buildNoSandboxRelaunchArgs(argv: readonly string[]): string[] {
|
||||
const args = (Array.isArray(argv) ? argv : []).filter(arg => arg !== '--no-sandbox')
|
||||
|
||||
args.push('--no-sandbox')
|
||||
|
||||
return args
|
||||
}
|
||||
|
|
@ -2966,6 +2966,23 @@ function Install-Desktop {
|
|||
# =false) because enabling it drags in signtool -> winCodeSign -> the
|
||||
# unfixable symlink crash; the afterPack hook runs rcedit directly.
|
||||
|
||||
# 3c. Grant ALL APPLICATION PACKAGES (S-1-15-2-2) RX on the unpacked app
|
||||
# directory. Chromium's GPU/renderer sandboxes CHECK-fail with
|
||||
# 0x80000003 when this ACE is missing alongside orphan AppContainer
|
||||
# SIDs under %LOCALAPPDATA% (electron/electron#51761, hermes-agent#38216).
|
||||
# Best-effort — never fail an otherwise-good install over ACL repair.
|
||||
try {
|
||||
$appDir = Split-Path -Parent $desktopExe
|
||||
& icacls $appDir /grant "*S-1-15-2-2:(OI)(CI)(RX)" /T /C /Q | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Success "Granted AppContainer read access on $appDir"
|
||||
} else {
|
||||
Write-Warn "icacls AppContainer grant returned exit $LASTEXITCODE for $appDir"
|
||||
}
|
||||
} catch {
|
||||
Write-Warn "Could not grant AppContainer ACL: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# 4. Create Start Menu + Desktop shortcuts pointing DIRECTLY at the packed
|
||||
# Hermes.exe. We deliberately do NOT point them at `hermes desktop`: that
|
||||
# command rebuilds (npm install + electron-builder) on every launch,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue