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
29 changes: 29 additions & 0 deletions build/afterPack.js
Original file line number Diff line number Diff line change
@@ -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}`);
}
}
};
122 changes: 122 additions & 0 deletions build/sandboxPermissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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';
}

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;
}
}

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,
};
26 changes: 24 additions & 2 deletions main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { app, BrowserWindow, Menu, Tray } = require('electron')
const path = require('path')
const log = require('electron-log');
const { prepareChromeSandbox } = require('./build/sandboxPermissions');

log.initialize();

Expand Down Expand Up @@ -177,6 +178,24 @@ 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');

const prepared = await prepareChromeSandbox(sandboxPath, (message) => log.info(message));

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}`);
app.exit(1);
}
}

if (!gotTheLock) {
log.info('WLED-GUI quitted');
app.quit()
Expand All @@ -191,8 +210,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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down