diff --git a/gui-js/apps/minsky-electron/src/app/app.sandbox.spec.ts b/gui-js/apps/minsky-electron/src/app/app.sandbox.spec.ts new file mode 100644 index 000000000..698b4aa8a --- /dev/null +++ b/gui-js/apps/minsky-electron/src/app/app.sandbox.spec.ts @@ -0,0 +1,35 @@ +import { requiresNoSandboxOnLinux } from './linux-sandbox'; + +describe('requiresNoSandboxOnLinux', () => { + it('returns true for Ubuntu 24.04', () => { + expect(requiresNoSandboxOnLinux('linux', 'ID=ubuntu\nVERSION_ID="24.04"\n', false)).toBe(true); + }); + + it('returns true for Ubuntu 26.04', () => { + expect(requiresNoSandboxOnLinux('linux', 'ID=ubuntu\nVERSION_ID="26.04"\n', false)).toBe(true); + }); + + it('normalizes quoted/mixed-case Ubuntu distro IDs', () => { + expect(requiresNoSandboxOnLinux('linux', 'ID="Ubuntu"\nVERSION_ID="24.04"\n', false)).toBe(true); + }); + + it('returns false for Ubuntu 24.04 when setuid sandbox is available', () => { + expect(requiresNoSandboxOnLinux('linux', 'ID=ubuntu\nVERSION_ID="24.04"\n', true)).toBe(false); + }); + + it('returns false for Ubuntu 22.04', () => { + expect(requiresNoSandboxOnLinux('linux', 'ID=ubuntu\nVERSION_ID="22.04"\n', false)).toBe(false); + }); + + it('returns false for non-Ubuntu Linux', () => { + expect(requiresNoSandboxOnLinux('linux', 'ID=debian\nVERSION_ID="12"\n', false)).toBe(false); + }); + + it('returns true for Ubuntu-like distro IDs on Linux 24+', () => { + expect(requiresNoSandboxOnLinux('linux', 'ID=linuxmint\nID_LIKE="ubuntu debian"\nVERSION_ID="24.04"\n', false)).toBe(true); + }); + + it('returns false for non-Linux platforms', () => { + expect(requiresNoSandboxOnLinux('darwin', 'ID=ubuntu\nVERSION_ID="24.04"\n', false)).toBe(false); + }); +}); diff --git a/gui-js/apps/minsky-electron/src/app/app.ts b/gui-js/apps/minsky-electron/src/app/app.ts index 42ad6a955..7b21c2943 100644 --- a/gui-js/apps/minsky-electron/src/app/app.ts +++ b/gui-js/apps/minsky-electron/src/app/app.ts @@ -16,6 +16,7 @@ import { ApplicationMenuManager } from './managers/ApplicationMenuManager'; import { BookmarkManager } from './managers/BookmarkManager'; import { CommandsManager } from './managers/CommandsManager'; import { HelpFilesManager } from './managers/HelpFilesManager'; +import { requiresNoSandboxOnLinux } from './linux-sandbox'; import { RecentFilesManager } from './managers/RecentFilesManager'; import { StoreManager } from './managers/StoreManager'; import { WindowManager } from './managers/WindowManager'; @@ -238,6 +239,11 @@ export default class App { // makes the code easier to write tests for App.BrowserWindow = browserWindow; App.application = app; + + if (requiresNoSandboxOnLinux()) { + app.commandLine.appendSwitch('no-sandbox'); + log.info('Enabled --no-sandbox for Ubuntu 24.04+ compatibility'); + } // Rendering was not working on some window's machines without disabling gpu if (process.platform === 'win32') { diff --git a/gui-js/apps/minsky-electron/src/app/linux-sandbox.ts b/gui-js/apps/minsky-electron/src/app/linux-sandbox.ts new file mode 100644 index 000000000..819e27ba3 --- /dev/null +++ b/gui-js/apps/minsky-electron/src/app/linux-sandbox.ts @@ -0,0 +1,54 @@ +import { existsSync, readFileSync, statSync } from 'fs'; +import { dirname, join } from 'path'; + +function parseOsReleaseValue(osReleaseContent: string, key: string): string { + const match=osReleaseContent.match(new RegExp(`^${key}=(.*)$`, 'm')); + return match? match[1].trim().replace(/^['"]|['"]$/g, '').toLowerCase(): ''; +} + +export function requiresNoSandboxOnLinux( + platform=process.platform, + osReleaseContent?: string, + hasUsableSandbox?: boolean +): boolean { + if (platform!=='linux') return false; + try { + if (!osReleaseContent) { + let osRelease='/etc/os-release'; + if (existsSync(process.resourcesPath+'/os-release')) + osRelease=process.resourcesPath+'/os-release'; + osReleaseContent=readFileSync(osRelease, 'utf8'); + } + const distro=parseOsReleaseValue(osReleaseContent, 'ID'); + const distroLike=parseOsReleaseValue(osReleaseContent, 'ID_LIKE'); + const versionId=parseOsReleaseValue(osReleaseContent, 'VERSION_ID'); + const versionMajor=Number(versionId.split('.')[0]); + const isUbuntuLike=distro==='ubuntu' || distroLike.split(/\s+/).includes('ubuntu'); + if (hasUsableSandbox===undefined) + hasUsableSandbox=hasUsableChromeSandbox(); + return isUbuntuLike && Number.isFinite(versionMajor) && versionMajor>=24 && !hasUsableSandbox; + } catch { + return false; + } +} + +function isSetuidRoot(path: string): boolean { + try { + const stat=statSync(path); + return stat.uid===0 && (stat.mode & 0o4000)!==0; + } catch { + return false; + } +} + +export function hasUsableChromeSandbox( + executablePath=process.execPath, + resourcesPath=process.resourcesPath +): boolean { + const candidates=[ + join(dirname(executablePath), 'chrome-sandbox'), + join(resourcesPath, 'chrome-sandbox'), + ]; + + return candidates.some(path => existsSync(path) && isSetuidRoot(path)); +}