From 76c79f473ad2eb71caa8f91e84119c0e373f751d Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Fri, 2 Oct 2026 01:13:27 -0700 Subject: [PATCH] fix(linux): move Chromium shared memory off a tiny /dev/shm (#23751) * fix(linux): move Chromium shared memory off a tiny /dev/shm Containers such as GitHub Codespaces mount a 64 MB /dev/shm by default. When it fills, Chromium aborts the renderer (IMMEDIATE_CRASH, SIGILL/SIGTRAP) on every reload, trapping Orca in a renderer crash loop. On Linux, stat /dev/shm before app ready and append --disable-dev-shm-usage when it is under 512 MB or unreadable (ORCA_DEV_SHM=off|force overrides). Records a dev_shm_policy crash breadcrumb so future container crashes are diagnosable. * docs(linux): correct dev-shm hasSwitch rationale --------- Co-authored-by: m4air Co-authored-by: m4air --- src/main/startup/linux-dev-shm-policy.test.ts | 159 ++++++++++++++++++ src/main/startup/linux-dev-shm-policy.ts | 54 ++++++ src/main/startup/main-process-preflight.ts | 2 + 3 files changed, 215 insertions(+) create mode 100644 src/main/startup/linux-dev-shm-policy.test.ts create mode 100644 src/main/startup/linux-dev-shm-policy.ts diff --git a/src/main/startup/linux-dev-shm-policy.test.ts b/src/main/startup/linux-dev-shm-policy.test.ts new file mode 100644 index 00000000000..e3c6a6de6a7 --- /dev/null +++ b/src/main/startup/linux-dev-shm-policy.test.ts @@ -0,0 +1,159 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { appMock, statfsSyncMock, breadcrumbMock } = vi.hoisted(() => ({ + appMock: { + commandLine: { + appendSwitch: vi.fn(), + hasSwitch: vi.fn(() => false) + } + }, + statfsSyncMock: vi.fn(), + breadcrumbMock: vi.fn() +})) + +vi.mock('electron', () => ({ app: appMock })) +vi.mock('node:fs', async (importOriginal) => ({ + ...(await importOriginal>()), + statfsSync: statfsSyncMock +})) +vi.mock('../crash-reporting/crash-breadcrumb-store', () => ({ + recordCrashBreadcrumb: breadcrumbMock +})) + +const MIB = 1024 * 1024 +const originalPlatform = process.platform +const originalOverride = process.env.ORCA_DEV_SHM + +function setPlatform(platform: NodeJS.Platform): void { + Object.defineProperty(process, 'platform', { value: platform, configurable: true }) +} + +function mockDevShm(totalMib: number, freeMib: number): void { + // statfs reports sizes in blocks; use 4 KiB blocks like tmpfs. + const bsize = 4096 + statfsSyncMock.mockReturnValue({ + bsize, + blocks: (totalMib * MIB) / bsize, + bavail: (freeMib * MIB) / bsize + }) +} + +beforeEach(() => { + vi.resetModules() + appMock.commandLine.appendSwitch.mockReset() + appMock.commandLine.hasSwitch.mockReset() + appMock.commandLine.hasSwitch.mockReturnValue(false) + statfsSyncMock.mockReset() + breadcrumbMock.mockReset() + delete process.env.ORCA_DEV_SHM +}) + +afterEach(() => { + setPlatform(originalPlatform) + if (originalOverride === undefined) { + delete process.env.ORCA_DEV_SHM + } else { + process.env.ORCA_DEV_SHM = originalOverride + } +}) + +describe('configureLinuxDevShmUsage', () => { + it('moves Chromium shared memory off a Docker-default 64 MB /dev/shm', async () => { + setPlatform('linux') + mockDevShm(64, 60) + const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy') + + configureLinuxDevShmUsage() + + expect(statfsSyncMock).toHaveBeenCalledWith('/dev/shm') + expect(appMock.commandLine.appendSwitch).toHaveBeenCalledWith('disable-dev-shm-usage') + expect(breadcrumbMock).toHaveBeenCalledWith('dev_shm_policy', { + devShmTotalMB: 64, + devShmFreeMB: 60, + devShmUsageDisabled: true, + reason: 'small' + }) + }) + + it('leaves a normally sized /dev/shm on the fast shared-memory path', async () => { + setPlatform('linux') + mockDevShm(8192, 8000) + const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy') + + configureLinuxDevShmUsage() + + expect(appMock.commandLine.appendSwitch).not.toHaveBeenCalled() + expect(breadcrumbMock).toHaveBeenCalledWith('dev_shm_policy', { + devShmTotalMB: 8192, + devShmFreeMB: 8000, + devShmUsageDisabled: false, + reason: 'ok' + }) + }) + + it('disables /dev/shm usage when the mount is missing', async () => { + setPlatform('linux') + statfsSyncMock.mockImplementation(() => { + throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) + }) + const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy') + + configureLinuxDevShmUsage() + + expect(appMock.commandLine.appendSwitch).toHaveBeenCalledWith('disable-dev-shm-usage') + expect(breadcrumbMock).toHaveBeenCalledWith('dev_shm_policy', { + devShmUsageDisabled: true, + reason: 'unreadable' + }) + }) + + it('honors ORCA_DEV_SHM=off on a small mount and =force on a large one', async () => { + setPlatform('linux') + mockDevShm(64, 60) + process.env.ORCA_DEV_SHM = 'off' + const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy') + configureLinuxDevShmUsage() + expect(appMock.commandLine.appendSwitch).not.toHaveBeenCalled() + + mockDevShm(8192, 8000) + process.env.ORCA_DEV_SHM = 'force' + configureLinuxDevShmUsage() + expect(appMock.commandLine.appendSwitch).toHaveBeenCalledWith('disable-dev-shm-usage') + }) + + it('does not append the switch twice when it is already on the command line', async () => { + setPlatform('linux') + mockDevShm(64, 60) + appMock.commandLine.hasSwitch.mockReturnValue(true) + const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy') + + configureLinuxDevShmUsage() + + expect(appMock.commandLine.appendSwitch).not.toHaveBeenCalled() + }) + + it('is a no-op off Linux', async () => { + setPlatform('darwin') + const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy') + + configureLinuxDevShmUsage() + + expect(statfsSyncMock).not.toHaveBeenCalled() + expect(appMock.commandLine.appendSwitch).not.toHaveBeenCalled() + expect(breadcrumbMock).not.toHaveBeenCalled() + }) +}) + +describe('desktop startup wiring', () => { + it('runs the /dev/shm policy for every launch before app ready, GPU fallback included', () => { + const source = readFileSync( + join(process.cwd(), 'src/main/startup/main-process-preflight.ts'), + 'utf8' + ).replace(/\r\n/g, '\n') + const call = source.indexOf('\n configureLinuxDevShmUsage()\n') + expect(call).toBeGreaterThan(-1) + expect(call).toBeLessThan(source.indexOf('\n maybeApplyGpuFallbackForThisLaunch()\n')) + }) +}) diff --git a/src/main/startup/linux-dev-shm-policy.ts b/src/main/startup/linux-dev-shm-policy.ts new file mode 100644 index 00000000000..8f707e3e64c --- /dev/null +++ b/src/main/startup/linux-dev-shm-policy.ts @@ -0,0 +1,54 @@ +import { statfsSync } from 'node:fs' +import { app } from 'electron' +import { recordCrashBreadcrumb } from '../crash-reporting/crash-breadcrumb-store' + +const DEV_SHM_PATH = '/dev/shm' +const DEV_SHM_OVERRIDE_ENV_VAR = 'ORCA_DEV_SHM' +// Why 512: Docker/Codespaces default to 64 MB, where Chromium's ring-buffer and +// texture allocations fail and IMMEDIATE_CRASH the renderer on every reload. +const MIN_DEV_SHM_TOTAL_MIB = 512 +const MIB = 1024 * 1024 + +type DevShmReading = { totalMib: number; freeMib: number } | null + +function readDevShm(): DevShmReading { + try { + const stats = statfsSync(DEV_SHM_PATH) + return { + totalMib: Math.floor((stats.blocks * stats.bsize) / MIB), + freeMib: Math.floor((stats.bavail * stats.bsize) / MIB) + } + } catch { + return null + } +} + +/** + * On Linux hosts with a tiny or missing /dev/shm (containers, Codespaces), + * back Chromium shared memory with /tmp files instead of letting renderers abort. + */ +export function configureLinuxDevShmUsage(): void { + if (process.platform !== 'linux') { + return + } + const override = (process.env[DEV_SHM_OVERRIDE_ENV_VAR] ?? '').trim().toLowerCase() + const reading = readDevShm() + const reason = + override === 'off' || override === 'force' + ? override + : reading === null + ? 'unreadable' + : reading.totalMib < MIN_DEV_SHM_TOTAL_MIB + ? 'small' + : 'ok' + const disable = reason === 'force' || reason === 'small' || reason === 'unreadable' + // Why hasSwitch: user argv may already carry it (headless serve appends its own later). + if (disable && !app.commandLine.hasSwitch('disable-dev-shm-usage')) { + app.commandLine.appendSwitch('disable-dev-shm-usage') + } + recordCrashBreadcrumb('dev_shm_policy', { + ...(reading ? { devShmTotalMB: reading.totalMib, devShmFreeMB: reading.freeMib } : {}), + devShmUsageDisabled: disable, + reason + }) +} diff --git a/src/main/startup/main-process-preflight.ts b/src/main/startup/main-process-preflight.ts index be0254a9d43..67be830901f 100644 --- a/src/main/startup/main-process-preflight.ts +++ b/src/main/startup/main-process-preflight.ts @@ -33,6 +33,7 @@ import { } from '../updater' import { getDevInstanceIdentity, shouldApplyPreReadyAppName } from './dev-instance-identity' import { enableRendererHeapHeadroom } from './renderer-heap-headroom' +import { configureLinuxDevShmUsage } from './linux-dev-shm-policy' import { isStartupDiagnosticsEnabled, logStartupDiagnostic } from './startup-diagnostics' import { startEventLoopStallProbe } from './event-loop-stall-probe' import { @@ -364,6 +365,7 @@ function initializeMainProcessPreflight(options: MainProcessPreflightOptions): b optOutOfHiddenPageWakeUpThrottling() configureElectronNetworkCompatibility() enableRendererHeapHeadroom() + configureLinuxDevShmUsage() maybeApplyGpuFallbackForThisLaunch() if (!state.gpuFallbackActiveThisLaunch) { enableMainProcessGpuFeatures()