fix(desktop): preserve Linux sandboxing wherever the host allows it (#4447)

* fix(desktop): preserve Linux sandboxing wherever the host allows it

Chromium checks its Linux sandbox before main.js, while package installation probes run as root and miss AppArmor restrictions on desktop users. Choose the launch flags before exec and always install the SUID alternative for native packages. Test published artifacts as an ordinary user with namespaces both denied and allowed; never repair permissions in the smoke harness.

* fix(desktop): retain the Linux release build baseline

* fix(ci): allow canceled desktop smoke uploads to stop

* test(desktop): verify the installed RPM sandbox under AppArmor

* fix(ci): resolve packaged desktop dependencies before installation

* chore(desktop): keep QA evidence out of the repository
This commit is contained in:
Mohamed Boudra
2026-09-11 09:05:26 +02:00
committed by GitHub
parent 0110302b6f
commit 772ff8d6f3
21 changed files with 597 additions and 69 deletions
+6
View File
@@ -3,6 +3,8 @@ const path = require("path");
const { smokePackagedDesktopApp } = require("../e2e/packaged-app-smoke.js");
const { installLinuxLauncher } = require("./linux-sandbox");
const EXECUTABLE_NAME = "Paseo";
// electron-builder arch enum → Node.js arch string
@@ -115,6 +117,10 @@ exports.default = async function afterPack(context) {
pruneNativeModules(context.appOutDir, platform, arch);
if (platform === "linux") {
installLinuxLauncher(context.appOutDir);
}
if (platform === "linux" || platform === "win32") {
if (arch !== process.arch) {
console.log(
@@ -0,0 +1,54 @@
#!/bin/bash
if type update-alternatives >/dev/null 2>&1; then
# Remove previous link if it doesn't use update-alternatives
if [ -L '/usr/bin/${executable}' -a -e '/usr/bin/${executable}' -a "`readlink '/usr/bin/${executable}'`" != '/etc/alternatives/${executable}' ]; then
rm -f '/usr/bin/${executable}'
fi
update-alternatives --install '/usr/bin/${executable}' '${executable}' '/opt/${sanitizedProductName}/${executable}' 100 || ln -sf '/opt/${sanitizedProductName}/${executable}' '/usr/bin/${executable}'
else
ln -sf '/opt/${sanitizedProductName}/${executable}' '/usr/bin/${executable}'
fi
# Package installation runs as root, whose namespace access says nothing about
# the desktop user under AppArmor. Always provide Chromium's SUID alternative.
chown root:root '/opt/${sanitizedProductName}/chrome-sandbox' || exit 1
chmod 4755 '/opt/${sanitizedProductName}/chrome-sandbox' || exit 1
if hash update-mime-database 2>/dev/null; then
update-mime-database /usr/share/mime || true
fi
if hash update-desktop-database 2>/dev/null; then
update-desktop-database /usr/share/applications || true
fi
# Install apparmor profile. (Ubuntu 24+)
# First check if the version of AppArmor running on the device supports our profile.
# This is in order to keep backwards compatibility with Ubuntu 22.04 which does not support abi/4.0.
# In that case, we just skip installing the profile since the app runs fine without it on 22.04.
#
# Those apparmor_parser flags are akin to performing a dry run of loading a profile.
# https://wiki.debian.org/AppArmor/HowToUse#Dumping_profiles
#
# Unfortunately, at the moment AppArmor doesn't have a good story for backwards compatibility.
# https://askubuntu.com/questions/1517272/writing-a-backwards-compatible-apparmor-profile
if apparmor_status --enabled > /dev/null 2>&1; then
APPARMOR_PROFILE_SOURCE='/opt/${sanitizedProductName}/resources/apparmor-profile'
APPARMOR_PROFILE_TARGET='/etc/apparmor.d/${executable}'
if apparmor_parser --skip-kernel-load --debug "$APPARMOR_PROFILE_SOURCE" > /dev/null 2>&1; then
cp -f "$APPARMOR_PROFILE_SOURCE" "$APPARMOR_PROFILE_TARGET"
# Updating the current AppArmor profile is not possible and probably not meaningful in a chroot'ed environment.
# Use cases are for example environments where images for clients are maintained.
# There, AppArmor might correctly be installed, but live updating makes no sense.
if ! { [ -x '/usr/bin/ischroot' ] && /usr/bin/ischroot; } && hash apparmor_parser 2>/dev/null; then
# Extra flags taken from dh_apparmor:
# > By using '-W -T' we ensure that any abstraction updates are also pulled in.
# https://wiki.debian.org/AppArmor/Contribute/FirstTimeProfileImport
apparmor_parser --replace --write-cache --skip-read-cache "$APPARMOR_PROFILE_TARGET"
fi
else
echo "Skipping the installation of the AppArmor profile as this version of AppArmor does not seem to support the bundled profile"
fi
fi
@@ -0,0 +1,12 @@
const fs = require("node:fs");
const path = require("node:path");
// Keep one pre-Chromium entrypoint for AppRun, desktop entries, updates, and tarballs.
exports.installLinuxLauncher = function installLinuxLauncher(appOutDir) {
const launcher = path.join(appOutDir, "Paseo");
if (!fs.existsSync(`${launcher}.bin`)) {
fs.renameSync(launcher, `${launcher}.bin`);
}
fs.copyFileSync(path.join(__dirname, "launcher.sh"), launcher);
fs.chmodSync(launcher, 0o755);
};
@@ -0,0 +1,55 @@
#!/bin/sh
set -eu
# Resolve /usr/bin alternatives and user-created symlinks before finding Electron.
launcher=$(readlink -f -- "$0")
executable="${launcher}.bin"
# Node entrypoints are not Chromium processes. Never inject browser flags here.
if [ "${ELECTRON_RUN_AS_NODE:-}" = 1 ]; then
exec "$executable" "$@"
fi
for arg in "$@"; do
if [ "$arg" = '--no-sandbox' ]; then
export PASEO_DESKTOP_SANDBOX_REASON='requested by --no-sandbox'
printf '[linux-sandbox] disabled: %s\n' "$PASEO_DESKTOP_SANDBOX_REASON" >&2 || true
exec "$executable" "$@"
fi
done
# Match main's whitespace-separated debugging flags without evaluating shell code.
set -f
for flag in ${PASEO_ELECTRON_FLAGS:-}; do
if [ "$flag" = '--no-sandbox' ]; then
export PASEO_DESKTOP_SANDBOX_REASON='requested by PASEO_ELECTRON_FLAGS'
printf '[linux-sandbox] disabled: %s\n' "$PASEO_DESKTOP_SANDBOX_REASON" >&2 || true
exec "$executable" --no-sandbox "$@"
fi
done
# Map the real UID and create a network namespace as Chromium does. A bare
# unshare --user can succeed under AppArmor while namespace capabilities fail.
if namespace_error=$(unshare --user --map-root-user --net true 2>&1); then
export PASEO_DESKTOP_SANDBOX_REASON='user namespaces available'
printf '[linux-sandbox] enabled: %s\n' "$PASEO_DESKTOP_SANDBOX_REASON" >&2 || true
exec "$executable" "$@"
fi
helper="$(dirname -- "$launcher")/chrome-sandbox"
if [ -x "$helper" ] && [ "$(stat -Lc '%u:%a' -- "$helper")" = '0:4755' ] &&
grep -q '^NoNewPrivs:[[:space:]]*0$' /proc/self/status; then
mount_options=$(findmnt -n -o VFS-OPTIONS --target "$helper" 2>/dev/null || true)
case ",$mount_options," in
*,nosuid,*|,,) ;;
*)
export PASEO_DESKTOP_SANDBOX_REASON='root-owned SUID helper available'
printf '[linux-sandbox] enabled: %s\n' "$PASEO_DESKTOP_SANDBOX_REASON" >&2 || true
exec "$executable" "$@"
;;
esac
fi
export PASEO_DESKTOP_SANDBOX_REASON="user namespaces unavailable${namespace_error:+ ($namespace_error)}; no usable SUID helper"
printf '[linux-sandbox] disabled: %s\n' "$PASEO_DESKTOP_SANDBOX_REASON" >&2 || true
exec "$executable" --no-sandbox "$@"