From abc7c9341be212ec9063d40552377ac4a10de098 Mon Sep 17 00:00:00 2001 From: |VOID| Date: Thu, 18 Dec 2025 08:09:54 -0600 Subject: [PATCH 1/3] Ensure chrome-sandbox permissions on Linux --- build/afterPack.js | 29 +++++++++++++++++ build/sandboxPermissions.js | 62 +++++++++++++++++++++++++++++++++++++ main.js | 18 +++++++++-- package.json | 1 + 4 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 build/afterPack.js create mode 100644 build/sandboxPermissions.js diff --git a/build/afterPack.js b/build/afterPack.js new file mode 100644 index 0000000..200aae1 --- /dev/null +++ b/build/afterPack.js @@ -0,0 +1,29 @@ +const fs = require('fs'); +const path = require('path'); +const { ensureChromeSandboxPermissions } = require('./sandboxPermissions'); + +/** + * Ensure the bundled chrome-sandbox binary has the correct permissions on Linux + * so Chromium's sandbox can start without disabling it. + */ +exports.default = async function afterPack(context) { + if (context.electronPlatformName !== 'linux') { + return; + } + + const sandboxPath = path.join(context.appOutDir, 'chrome-sandbox'); + + const sandboxAdjusted = await ensureChromeSandboxPermissions( + sandboxPath, + (message) => context.packager.info(message), + ); + + if (!sandboxAdjusted) { + try { + await fs.promises.access(sandboxPath, fs.constants.F_OK); + context.packager.info('chrome-sandbox permissions could not be fully adjusted; ensure root ownership after installation if sandbox errors occur.'); + } catch (error) { + context.packager.info(`chrome-sandbox missing at ${sandboxPath}: ${error.message}`); + } + } +}; diff --git a/build/sandboxPermissions.js b/build/sandboxPermissions.js new file mode 100644 index 0000000..a21b5e1 --- /dev/null +++ b/build/sandboxPermissions.js @@ -0,0 +1,62 @@ +const fs = require('fs'); + +const DESIRED_MODE = 0o4755; + +function formatUid(uid) { + return typeof uid === 'number' ? uid : 'unknown'; +} + +async function ensureChromeSandboxPermissions(sandboxPath, log) { + const logger = typeof log === 'function' ? log : () => {}; + + try { + await fs.promises.access(sandboxPath, fs.constants.X_OK); + } catch (error) { + logger(`chrome-sandbox missing or not executable at ${sandboxPath}: ${error.message}`); + return false; + } + + let stat; + try { + stat = await fs.promises.stat(sandboxPath); + } catch (error) { + logger(`Unable to read chrome-sandbox stats: ${error.message}`); + return false; + } + + const currentMode = stat.mode & 0o7777; + const needsModeUpdate = currentMode !== DESIRED_MODE; + const needsOwnerUpdate = typeof stat.uid === 'number' && stat.uid !== 0; + + if (!needsModeUpdate && !needsOwnerUpdate) { + logger(`chrome-sandbox already has correct permissions at ${sandboxPath}`); + return true; + } + + const canChown = typeof process.geteuid === 'function' && process.geteuid() === 0; + + if (needsOwnerUpdate && !canChown) { + logger(`chrome-sandbox is owned by uid ${formatUid(stat.uid)}; run with elevated privileges to set root ownership and setuid bit.`); + return false; + } + + try { + if (needsOwnerUpdate && canChown) { + await fs.promises.chown(sandboxPath, 0, typeof stat.gid === 'number' ? stat.gid : 0); + } + + if (needsModeUpdate || needsOwnerUpdate) { + await fs.promises.chmod(sandboxPath, DESIRED_MODE); + } + + logger(`Adjusted chrome-sandbox permissions to mode 4755${needsOwnerUpdate ? ' with root ownership' : ''}.`); + return true; + } catch (error) { + logger(`Unable to adjust chrome-sandbox permissions: ${error.message}`); + return false; + } +} + +module.exports = { + ensureChromeSandboxPermissions, +}; diff --git a/main.js b/main.js index 2dbe3f9..c17cf9d 100644 --- a/main.js +++ b/main.js @@ -1,6 +1,7 @@ const { app, BrowserWindow, Menu, Tray } = require('electron') const path = require('path') const log = require('electron-log'); +const { ensureChromeSandboxPermissions } = require('./build/sandboxPermissions'); log.initialize(); @@ -177,6 +178,16 @@ function getIconDir() { } // check if second instance was started +async function prepareSandbox() { + if (process.platform !== 'linux') { + return; + } + + const sandboxPath = path.join(path.dirname(process.execPath), 'chrome-sandbox'); + + await ensureChromeSandboxPermissions(sandboxPath, (message) => log.info(message)); +} + if (!gotTheLock) { log.info('WLED-GUI quitted'); app.quit() @@ -191,8 +202,11 @@ if (!gotTheLock) { // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. - app.whenReady().then(createWindow) - app.whenReady().then(loadSettings) + app.whenReady().then(async () => { + await prepareSandbox(); + createWindow(); + loadSettings(); + }) // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits diff --git a/package.json b/package.json index bf6b724..3ba4807 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "build mac": "electron-builder --mac --publish=never" }, "build": { + "afterPack": "./build/afterPack.js", "appId": "wled-gui.woody.pizza", "productName": "WLED", "artifactName": "${productName}-${version}-${os}.${ext}", From 4a494efab470e49e2db1edcfd11ba58c5713ca4d Mon Sep 17 00:00:00 2001 From: |VOID| Date: Thu, 18 Dec 2025 08:18:47 -0600 Subject: [PATCH 2/3] Exit when chrome-sandbox permissions are invalid --- main.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/main.js b/main.js index c17cf9d..f11fda7 100644 --- a/main.js +++ b/main.js @@ -185,7 +185,18 @@ async function prepareSandbox() { const sandboxPath = path.join(path.dirname(process.execPath), 'chrome-sandbox'); - await ensureChromeSandboxPermissions(sandboxPath, (message) => log.info(message)); + const adjusted = await ensureChromeSandboxPermissions( + sandboxPath, + (message) => log.info(message), + ); + + if (!adjusted) { + log.error('chrome-sandbox permissions are incorrect; Electron will abort to avoid running without sandboxing.'); + log.error('Run the following commands with sudo/root privileges to fix:'); + log.error(` sudo chown root:root ${sandboxPath}`); + log.error(` sudo chmod 4755 ${sandboxPath}`); + app.exit(1); + } } if (!gotTheLock) { From c88ca933cb3b8d48e7316f62bb62c0b8dc6638b0 Mon Sep 17 00:00:00 2001 From: |VOID| Date: Thu, 18 Dec 2025 08:36:45 -0600 Subject: [PATCH 3/3] Enable user namespace fallback for sandbox --- build/sandboxPermissions.js | 60 +++++++++++++++++++++++++++++++++++++ main.js | 11 +++---- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/build/sandboxPermissions.js b/build/sandboxPermissions.js index a21b5e1..983236b 100644 --- a/build/sandboxPermissions.js +++ b/build/sandboxPermissions.js @@ -1,6 +1,7 @@ const fs = require('fs'); const DESIRED_MODE = 0o4755; +const USERNS_FLAG_PATH = '/proc/sys/kernel/unprivileged_userns_clone'; function formatUid(uid) { return typeof uid === 'number' ? uid : 'unknown'; @@ -57,6 +58,65 @@ async function ensureChromeSandboxPermissions(sandboxPath, log) { } } +async function hasUserNamespaceSupport(log) { + const logger = typeof log === 'function' ? log : () => {}; + + try { + const contents = await fs.promises.readFile(USERNS_FLAG_PATH, 'utf8'); + const isEnabled = contents.trim() === '1'; + + if (!isEnabled) { + logger(`User namespaces are disabled (value at ${USERNS_FLAG_PATH} is ${contents.trim()})`); + } + + return isEnabled; + } catch (error) { + logger(`Unable to read ${USERNS_FLAG_PATH}: ${error.message}`); + return false; + } +} + +async function disableSetuidSandboxHelper(sandboxPath, log) { + const logger = typeof log === 'function' ? log : () => {}; + + try { + await fs.promises.access(sandboxPath, fs.constants.F_OK); + } catch (error) { + logger(`chrome-sandbox missing at ${sandboxPath}, nothing to disable.`); + return true; + } + + const renamedPath = `${sandboxPath}.disabled`; + + try { + await fs.promises.rename(sandboxPath, renamedPath); + logger(`Renamed chrome-sandbox to ${renamedPath} to allow user namespace sandbox.`); + return true; + } catch (error) { + logger(`Failed to disable chrome-sandbox helper: ${error.message}`); + return false; + } +} + +async function prepareChromeSandbox(sandboxPath, log) { + const logger = typeof log === 'function' ? log : () => {}; + + const adjusted = await ensureChromeSandboxPermissions(sandboxPath, logger); + + if (adjusted) { + return true; + } + + if (await hasUserNamespaceSupport(logger)) { + logger('Falling back to user-namespace sandbox because setuid chrome-sandbox cannot be prepared.'); + return disableSetuidSandboxHelper(sandboxPath, logger); + } + + logger('Cannot adjust chrome-sandbox permissions and user namespaces are unavailable.'); + return false; +} + module.exports = { ensureChromeSandboxPermissions, + prepareChromeSandbox, }; diff --git a/main.js b/main.js index f11fda7..f3e2592 100644 --- a/main.js +++ b/main.js @@ -1,7 +1,7 @@ const { app, BrowserWindow, Menu, Tray } = require('electron') const path = require('path') const log = require('electron-log'); -const { ensureChromeSandboxPermissions } = require('./build/sandboxPermissions'); +const { prepareChromeSandbox } = require('./build/sandboxPermissions'); log.initialize(); @@ -185,13 +185,10 @@ async function prepareSandbox() { const sandboxPath = path.join(path.dirname(process.execPath), 'chrome-sandbox'); - const adjusted = await ensureChromeSandboxPermissions( - sandboxPath, - (message) => log.info(message), - ); + const prepared = await prepareChromeSandbox(sandboxPath, (message) => log.info(message)); - if (!adjusted) { - log.error('chrome-sandbox permissions are incorrect; Electron will abort to avoid running without sandboxing.'); + if (!prepared) { + log.error('chrome-sandbox cannot be prepared and user namespaces are unavailable; Electron will abort to avoid running without sandboxing.'); log.error('Run the following commands with sudo/root privileges to fix:'); log.error(` sudo chown root:root ${sandboxPath}`); log.error(` sudo chmod 4755 ${sandboxPath}`);