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 <m4air@m4airs-Air.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-02 01:13:27 -07:00
committed by GitHub
co-authored by m4air m4air
parent e3621295e6
commit 76c79f473a
3 changed files with 215 additions and 0 deletions
@@ -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<Record<string, unknown>>()),
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'))
})
})
+54
View File
@@ -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
})
}
@@ -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()