Skip to content
Open
Show file tree
Hide file tree
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
16 changes: 15 additions & 1 deletion dsh-desktop-next/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import { manualUpdateHint, releaseNotesUrl, updateLabel } from './update-state.t
import { DESKTOP_SHUTDOWN_TIMEOUT_MS } from '../../dsh-plugin-desktop/src/shutdown.ts'
import { isDesktopRendererStatus, NextRendererRecovery, type DesktopNavigationCause } from './renderer-recovery.ts'
import { deElevateAtStartup, lowIntegrityAtStartup, windowsElevation } from './windows-elevation.ts'
import { createSandboxLaunchReporter, gpuLaunchFailure, lowIntegrityLaunchDialog, rendererLaunchFailure } from './sandbox-launch.ts'
import { createSandboxLaunchReporter, gpuLaunchFailure, lowIntegrityLaunchDialog, rendererLaunchFailure, DISABLE_GPU_SANDBOX_ARGUMENT, gpuSandboxBreakpoint, gpuSandboxFallbackArguments } from './sandbox-launch.ts'
import { pickOutsideProgramDirectory, programDirectory, programDirectoryRefusal, refusedProgramDirectoryOverlap, type ProgramDirectoryOverlap, type ProgramDirectoryPurpose } from './program-directory.ts'
import { startCrashReporting } from './crash-reporting.ts'

Expand Down Expand Up @@ -1577,6 +1577,20 @@ if (ownsInstance) {
if (details.reason !== 'clean-exit' && !quitting) runtime.memory.incident({ process: 'child', type: details.type, reason: details.reason, exitCode: details.exitCode })
const failure = gpuLaunchFailure(process.platform, details)
if (failure && !quitting) reportSandboxLaunchFailure(failure)
// A GPU child that dies with STATUS_BREAKPOINT (0x80000003) leaves Chromium no working GPU mode, and the
// browser process ends before any window exists. Relaunch once with only the GPU sandbox disabled; the flag
// stays on the command line, so a second failure is not retried into a loop.
if (!quitting && gpuSandboxBreakpoint(process.platform, details)) {
const fallback = gpuSandboxFallbackArguments(process.argv.slice(1))
if (fallback !== undefined) {
quitting = true
runtime.diagnostics.record({ source: 'electron', event: 'sandbox.gpu-breakpoint-fallback', level: 'warn',
fields: { exitCode: details.exitCode, argument: DISABLE_GPU_SANDBOX_ARGUMENT } })
runtime.diagnostics.flush()
relaunchApp(app, fallback)
app.exit(0)
}
}
})
app.on('render-process-gone', (_event, contents, details) => {
const failure = rendererLaunchFailure(process.platform, details)
Expand Down
34 changes: 34 additions & 0 deletions dsh-desktop-next/src/sandbox-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export const SBOX_ERROR_CREATE_PROCESS = 18
/** Startup outcomes in which a restart without administrator rights was attempted and the launch stayed elevated. */
const RESTART_FAILURES = new Set<string>(['no-shell', 'shell-elevated', 'shell-other-user', 'restarted'])

/** Windows `STATUS_BREAKPOINT`: a sandboxed child died before it could report an exit. */
export const STATUS_BREAKPOINT_EXIT = -2147483645

/** Chromium switch that leaves the GPU process unsandboxed while every other child stays sandboxed. */
export const DISABLE_GPU_SANDBOX_ARGUMENT = '--disable-gpu-sandbox'

/**
* Recognize a failed GPU process launch in Electron's `child-process-gone` details.
* @param platform - Node platform.
Expand All @@ -32,6 +38,34 @@ export function gpuLaunchFailure(platform: NodeJS.Platform, details: { type: str
return platform === 'win32' && details.type === 'GPU' && details.reason === 'launch-failed' ? { process: 'gpu', code: details.exitCode } : undefined
}

/**
* Recognize a sandboxed GPU child that died with `STATUS_BREAKPOINT` on Windows.
*
* Electron reports this as `reason: 'crashed'`, `exitCode` 0x80000003 rather than a sandbox launch result, so
* {@link gpuLaunchFailure} deliberately ignores it. Chromium then retries every GPU mode and ends the browser
* process ("GPU process isn't usable. Goodbye."), which is why the user only sees that nothing opens (#1487,
* #1323). It is known to happen on Windows 11 26H2 builds 26200+ (electron/electron#54614) and when an ancestor
* of the install directory carries an AppContainer package-SID ACE (#1372). `--disable-gpu-sandbox` is the
* documented workaround for the first cause and keeps every other child sandboxed.
*
* @param platform - Node platform.
* @param details - Electron child process details.
* @returns whether a GPU child died with the breakpoint status.
*/
export function gpuSandboxBreakpoint(platform: NodeJS.Platform, details: { type: string; reason: string; exitCode: number }): boolean {
return platform === 'win32' && details.type === 'GPU' && details.reason === 'crashed' && details.exitCode === STATUS_BREAKPOINT_EXIT
}

/**
* Command line for the fallback relaunch: keep this run's arguments and append the switch.
* @param argv - argument list without the executable.
* @returns the relaunch arguments, or undefined when the switch was already requested, where a second relaunch
* would only reproduce the crash.
*/
export function gpuSandboxFallbackArguments(argv: readonly string[]): string[] | undefined {
return argv.includes(DISABLE_GPU_SANDBOX_ARGUMENT) ? undefined : [...argv, DISABLE_GPU_SANDBOX_ARGUMENT]
}

/**
* Recognize a failed renderer launch in Electron's `render-process-gone` details.
* @param platform - Node platform.
Expand Down
28 changes: 27 additions & 1 deletion dsh-desktop-next/tests/main-lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,6 @@ it('explains the first Windows GPU launch failure in a native dialog, records it
app.emit('child-process-gone', {}, gpu)
Object.defineProperty(process, 'platform', { ...platform, value: 'win32' })
app.emit('child-process-gone', {}, { type: 'Utility', reason: 'launch-failed', exitCode: 18, serviceName: 'audio.mojom.AudioService' })
app.emit('child-process-gone', {}, { ...gpu, reason: 'crashed', exitCode: -2147483645 })
expect(dialog.showMessageBoxSync).not.toHaveBeenCalled()
app.emit('child-process-gone', {}, gpu)
app.emit('child-process-gone', {}, gpu)
Expand Down Expand Up @@ -711,6 +710,33 @@ it('starts local-only Crashpad before any window and turns renderer and child fa
} finally { vi.unstubAllEnvs(); rmSync(home, { recursive: true, force: true }) }
})

it('relaunches once with only the GPU sandbox disabled after a Windows GPU breakpoint', async () => {
const home = mkdtempSync(join(tmpdir(), 'next-sandbox-breakpoint-'))
vi.stubEnv('DSH_DESKTOP_NEXT_HOME', home)
const platform = Object.getOwnPropertyDescriptor(process, 'platform')!
try {
await import('../src/main.ts')
await vi.waitFor(() => expect(fixture.windows).toHaveLength(1))
const { app, dialog } = await import('electron')
Object.defineProperty(process, 'platform', { ...platform, value: 'win32' })
const breakpoint = { type: 'GPU', reason: 'crashed', exitCode: -2147483645 }
app.emit('child-process-gone', {}, breakpoint)
await vi.waitFor(() => expect(app.relaunch).toHaveBeenCalledOnce())
expect(vi.mocked(app.relaunch).mock.calls[0]![0]!.args).toContain('--disable-gpu-sandbox')
expect(fixture.diagnosticAppend).toHaveBeenCalledWith(expect.objectContaining({
source: 'electron', event: 'sandbox.gpu-breakpoint-fallback', level: 'warn',
fields: { exitCode: -2147483645, argument: '--disable-gpu-sandbox' },
}))
expect(app.exit).toHaveBeenCalledWith(0)
expect(dialog.showMessageBoxSync).not.toHaveBeenCalled()
// The relaunched process carries the switch, so a second failure cannot restart in a loop.
app.emit('child-process-gone', {}, breakpoint)
expect(app.relaunch).toHaveBeenCalledOnce()
} finally {
Object.defineProperty(process, 'platform', platform)
vi.unstubAllEnvs(); rmSync(home, { recursive: true, force: true })
}})

it('explains a Windows renderer launch failure only for the Desktop windows', async () => {
const home = mkdtempSync(join(tmpdir(), 'next-sandbox-renderer-'))
vi.stubEnv('DSH_DESKTOP_NEXT_HOME', home)
Expand Down
19 changes: 18 additions & 1 deletion dsh-desktop-next/tests/sandbox-launch.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** Native explanation of Windows sandbox launch failures. */
import { describe, expect, it, vi } from 'vitest'
import { createSandboxLaunchReporter, gpuLaunchFailure, lowIntegrityLaunchDialog, rendererLaunchFailure, sandboxLaunchDialog, type SandboxLaunchContext } from '../src/sandbox-launch.ts'
import { createSandboxLaunchReporter, gpuLaunchFailure, lowIntegrityLaunchDialog, rendererLaunchFailure, sandboxLaunchDialog, type SandboxLaunchContext, DISABLE_GPU_SANDBOX_ARGUMENT, gpuSandboxBreakpoint, gpuSandboxFallbackArguments, STATUS_BREAKPOINT_EXIT } from '../src/sandbox-launch.ts'

const EXECUTABLE = 'E:\\Program Files\\DSH NEXT\\DSH NEXT.exe'
const GPU = { process: 'gpu', code: 18 } as const
Expand Down Expand Up @@ -121,3 +121,20 @@ describe('lowIntegrityLaunchDialog', () => {
expect(text.detail).not.toContain('icacls')
})
})

describe('gpu sandbox breakpoint fallback', () => {
it('recognizes only a Windows GPU child that died with STATUS_BREAKPOINT', () => {
const gpu = { type: 'GPU', reason: 'crashed', exitCode: STATUS_BREAKPOINT_EXIT }
expect(gpuSandboxBreakpoint('win32', gpu)).toBe(true)
expect(gpuSandboxBreakpoint('darwin', gpu)).toBe(false)
expect(gpuSandboxBreakpoint('win32', { ...gpu, reason: 'launch-failed' })).toBe(false)
expect(gpuSandboxBreakpoint('win32', { ...gpu, exitCode: 18 })).toBe(false)
expect(gpuSandboxBreakpoint('win32', { ...gpu, type: 'Utility' })).toBe(false)
})

it('appends the GPU sandbox switch once and keeps the rest of the command line', () => {
expect(gpuSandboxFallbackArguments(['--next-channel'])).toEqual(['--next-channel', DISABLE_GPU_SANDBOX_ARGUMENT])
expect(gpuSandboxFallbackArguments([])).toEqual([DISABLE_GPU_SANDBOX_ARGUMENT])
expect(gpuSandboxFallbackArguments([DISABLE_GPU_SANDBOX_ARGUMENT])).toBeUndefined()
expect(gpuSandboxFallbackArguments(['--next-channel', DISABLE_GPU_SANDBOX_ARGUMENT])).toBeUndefined() })
})
16 changes: 15 additions & 1 deletion dsh-plugin-desktop-beta/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import { manualUpdateHint, releaseNotesUrl, updateLabel } from './update-state.t
import { DESKTOP_SHUTDOWN_TIMEOUT_MS } from '../../dsh-plugin-desktop/src/shutdown.ts'
import { isDesktopRendererStatus, NextRendererRecovery, type DesktopNavigationCause } from './renderer-recovery.ts'
import { deElevateAtStartup, lowIntegrityAtStartup, windowsElevation } from './windows-elevation.ts'
import { createSandboxLaunchReporter, gpuLaunchFailure, lowIntegrityLaunchDialog, rendererLaunchFailure } from './sandbox-launch.ts'
import { createSandboxLaunchReporter, gpuLaunchFailure, lowIntegrityLaunchDialog, rendererLaunchFailure, DISABLE_GPU_SANDBOX_ARGUMENT, gpuSandboxBreakpoint, gpuSandboxFallbackArguments } from './sandbox-launch.ts'
import { pickOutsideProgramDirectory, programDirectory, programDirectoryRefusal, refusedProgramDirectoryOverlap, type ProgramDirectoryOverlap, type ProgramDirectoryPurpose } from './program-directory.ts'
import { startCrashReporting } from './crash-reporting.ts'

Expand Down Expand Up @@ -1577,6 +1577,20 @@ if (ownsInstance) {
if (details.reason !== 'clean-exit' && !quitting) runtime.memory.incident({ process: 'child', type: details.type, reason: details.reason, exitCode: details.exitCode })
const failure = gpuLaunchFailure(process.platform, details)
if (failure && !quitting) reportSandboxLaunchFailure(failure)
// A GPU child that dies with STATUS_BREAKPOINT (0x80000003) leaves Chromium no working GPU mode, and the
// browser process ends before any window exists. Relaunch once with only the GPU sandbox disabled; the flag
// stays on the command line, so a second failure is not retried into a loop.
if (!quitting && gpuSandboxBreakpoint(process.platform, details)) {
const fallback = gpuSandboxFallbackArguments(process.argv.slice(1))
if (fallback !== undefined) {
quitting = true
runtime.diagnostics.record({ source: 'electron', event: 'sandbox.gpu-breakpoint-fallback', level: 'warn',
fields: { exitCode: details.exitCode, argument: DISABLE_GPU_SANDBOX_ARGUMENT } })
runtime.diagnostics.flush()
relaunchApp(app, fallback)
app.exit(0)
}
}
})
app.on('render-process-gone', (_event, contents, details) => {
const failure = rendererLaunchFailure(process.platform, details)
Expand Down
34 changes: 34 additions & 0 deletions dsh-plugin-desktop-beta/src/sandbox-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export const SBOX_ERROR_CREATE_PROCESS = 18
/** Startup outcomes in which a restart without administrator rights was attempted and the launch stayed elevated. */
const RESTART_FAILURES = new Set<string>(['no-shell', 'shell-elevated', 'shell-other-user', 'restarted'])

/** Windows `STATUS_BREAKPOINT`: a sandboxed child died before it could report an exit. */
export const STATUS_BREAKPOINT_EXIT = -2147483645

/** Chromium switch that leaves the GPU process unsandboxed while every other child stays sandboxed. */
export const DISABLE_GPU_SANDBOX_ARGUMENT = '--disable-gpu-sandbox'

/**
* Recognize a failed GPU process launch in Electron's `child-process-gone` details.
* @param platform - Node platform.
Expand All @@ -32,6 +38,34 @@ export function gpuLaunchFailure(platform: NodeJS.Platform, details: { type: str
return platform === 'win32' && details.type === 'GPU' && details.reason === 'launch-failed' ? { process: 'gpu', code: details.exitCode } : undefined
}

/**
* Recognize a sandboxed GPU child that died with `STATUS_BREAKPOINT` on Windows.
*
* Electron reports this as `reason: 'crashed'`, `exitCode` 0x80000003 rather than a sandbox launch result, so
* {@link gpuLaunchFailure} deliberately ignores it. Chromium then retries every GPU mode and ends the browser
* process ("GPU process isn't usable. Goodbye."), which is why the user only sees that nothing opens (#1487,
* #1323). It is known to happen on Windows 11 26H2 builds 26200+ (electron/electron#54614) and when an ancestor
* of the install directory carries an AppContainer package-SID ACE (#1372). `--disable-gpu-sandbox` is the
* documented workaround for the first cause and keeps every other child sandboxed.
*
* @param platform - Node platform.
* @param details - Electron child process details.
* @returns whether a GPU child died with the breakpoint status.
*/
export function gpuSandboxBreakpoint(platform: NodeJS.Platform, details: { type: string; reason: string; exitCode: number }): boolean {
return platform === 'win32' && details.type === 'GPU' && details.reason === 'crashed' && details.exitCode === STATUS_BREAKPOINT_EXIT
}

/**
* Command line for the fallback relaunch: keep this run's arguments and append the switch.
* @param argv - argument list without the executable.
* @returns the relaunch arguments, or undefined when the switch was already requested, where a second relaunch
* would only reproduce the crash.
*/
export function gpuSandboxFallbackArguments(argv: readonly string[]): string[] | undefined {
return argv.includes(DISABLE_GPU_SANDBOX_ARGUMENT) ? undefined : [...argv, DISABLE_GPU_SANDBOX_ARGUMENT]
}

/**
* Recognize a failed renderer launch in Electron's `render-process-gone` details.
* @param platform - Node platform.
Expand Down
28 changes: 27 additions & 1 deletion dsh-plugin-desktop-beta/tests/main-lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,6 @@ it('explains the first Windows GPU launch failure in a native dialog, records it
app.emit('child-process-gone', {}, gpu)
Object.defineProperty(process, 'platform', { ...platform, value: 'win32' })
app.emit('child-process-gone', {}, { type: 'Utility', reason: 'launch-failed', exitCode: 18, serviceName: 'audio.mojom.AudioService' })
app.emit('child-process-gone', {}, { ...gpu, reason: 'crashed', exitCode: -2147483645 })
expect(dialog.showMessageBoxSync).not.toHaveBeenCalled()
app.emit('child-process-gone', {}, gpu)
app.emit('child-process-gone', {}, gpu)
Expand Down Expand Up @@ -711,6 +710,33 @@ it('starts local-only Crashpad before any window and turns renderer and child fa
} finally { vi.unstubAllEnvs(); rmSync(home, { recursive: true, force: true }) }
})

it('relaunches once with only the GPU sandbox disabled after a Windows GPU breakpoint', async () => {
const home = mkdtempSync(join(tmpdir(), 'next-sandbox-breakpoint-'))
vi.stubEnv('DSH_DESKTOP_NEXT_HOME', home)
const platform = Object.getOwnPropertyDescriptor(process, 'platform')!
try {
await import('../src/main.ts')
await vi.waitFor(() => expect(fixture.windows).toHaveLength(1))
const { app, dialog } = await import('electron')
Object.defineProperty(process, 'platform', { ...platform, value: 'win32' })
const breakpoint = { type: 'GPU', reason: 'crashed', exitCode: -2147483645 }
app.emit('child-process-gone', {}, breakpoint)
await vi.waitFor(() => expect(app.relaunch).toHaveBeenCalledOnce())
expect(vi.mocked(app.relaunch).mock.calls[0]![0]!.args).toContain('--disable-gpu-sandbox')
expect(fixture.diagnosticAppend).toHaveBeenCalledWith(expect.objectContaining({
source: 'electron', event: 'sandbox.gpu-breakpoint-fallback', level: 'warn',
fields: { exitCode: -2147483645, argument: '--disable-gpu-sandbox' },
}))
expect(app.exit).toHaveBeenCalledWith(0)
expect(dialog.showMessageBoxSync).not.toHaveBeenCalled()
// The relaunched process carries the switch, so a second failure cannot restart in a loop.
app.emit('child-process-gone', {}, breakpoint)
expect(app.relaunch).toHaveBeenCalledOnce()
} finally {
Object.defineProperty(process, 'platform', platform)
vi.unstubAllEnvs(); rmSync(home, { recursive: true, force: true })
}})

it('explains a Windows renderer launch failure only for the Desktop windows', async () => {
const home = mkdtempSync(join(tmpdir(), 'next-sandbox-renderer-'))
vi.stubEnv('DSH_DESKTOP_NEXT_HOME', home)
Expand Down
19 changes: 18 additions & 1 deletion dsh-plugin-desktop-beta/tests/sandbox-launch.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** Native explanation of Windows sandbox launch failures. */
import { describe, expect, it, vi } from 'vitest'
import { createSandboxLaunchReporter, gpuLaunchFailure, lowIntegrityLaunchDialog, rendererLaunchFailure, sandboxLaunchDialog, type SandboxLaunchContext } from '../src/sandbox-launch.ts'
import { createSandboxLaunchReporter, gpuLaunchFailure, lowIntegrityLaunchDialog, rendererLaunchFailure, sandboxLaunchDialog, type SandboxLaunchContext, DISABLE_GPU_SANDBOX_ARGUMENT, gpuSandboxBreakpoint, gpuSandboxFallbackArguments, STATUS_BREAKPOINT_EXIT } from '../src/sandbox-launch.ts'

const EXECUTABLE = 'E:\\Program Files\\DSH Desktop Beta\\DSH Desktop Beta.exe'
const GPU = { process: 'gpu', code: 18 } as const
Expand Down Expand Up @@ -121,3 +121,20 @@ describe('lowIntegrityLaunchDialog', () => {
expect(text.detail).not.toContain('icacls')
})
})

describe('gpu sandbox breakpoint fallback', () => {
it('recognizes only a Windows GPU child that died with STATUS_BREAKPOINT', () => {
const gpu = { type: 'GPU', reason: 'crashed', exitCode: STATUS_BREAKPOINT_EXIT }
expect(gpuSandboxBreakpoint('win32', gpu)).toBe(true)
expect(gpuSandboxBreakpoint('darwin', gpu)).toBe(false)
expect(gpuSandboxBreakpoint('win32', { ...gpu, reason: 'launch-failed' })).toBe(false)
expect(gpuSandboxBreakpoint('win32', { ...gpu, exitCode: 18 })).toBe(false)
expect(gpuSandboxBreakpoint('win32', { ...gpu, type: 'Utility' })).toBe(false)
})

it('appends the GPU sandbox switch once and keeps the rest of the command line', () => {
expect(gpuSandboxFallbackArguments(['--next-channel'])).toEqual(['--next-channel', DISABLE_GPU_SANDBOX_ARGUMENT])
expect(gpuSandboxFallbackArguments([])).toEqual([DISABLE_GPU_SANDBOX_ARGUMENT])
expect(gpuSandboxFallbackArguments([DISABLE_GPU_SANDBOX_ARGUMENT])).toBeUndefined()
expect(gpuSandboxFallbackArguments(['--next-channel', DISABLE_GPU_SANDBOX_ARGUMENT])).toBeUndefined() })
})