Skip to content
Merged
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
68 changes: 67 additions & 1 deletion scripts/release/test-strip-appimage-rpaths.sh
Original file line number Diff line number Diff line change
Expand Up @@ -571,8 +571,23 @@ printf '%s\n' \
' cp "$4" "$APPARMOR_PROFILE_SNAPSHOT"' \
'fi' \
>"$APPARMOR_BIN/sudo"
chmod +x "$APPARMOR_BIN/apparmor_parser" "$APPARMOR_BIN/sudo"
# Stand in for a restricted Ubuntu 24.04 host so the sysctl branch is exercised
# deterministically on any development machine, including ones with no sysctl
# key of that name at all.
printf '%s\n' \
'#!/usr/bin/env bash' \
'if [ "${1:-}" = "-n" ]; then' \
' printf "%s\n" "1"' \
' exit 0' \
'fi' \
'exit 0' \
>"$APPARMOR_BIN/sysctl"
chmod +x \
"$APPARMOR_BIN/apparmor_parser" \
"$APPARMOR_BIN/sudo" \
"$APPARMOR_BIN/sysctl"
export APPARMOR_RECORD APPARMOR_PROFILE_SNAPSHOT
USERNS_SYSCTL="kernel.apparmor_restrict_unprivileged_userns"
PATH="$APPARMOR_BIN:$PATH" install_smoke_userns_profile \
"$RUNTIME_COMPLETE" "$APPARMOR_PROFILE" \
|| fail "install_smoke_userns_profile rejected the fixture AppDir"
Expand Down Expand Up @@ -610,12 +625,16 @@ set -e
[ "$apparmor_smoke_status" -eq 37 ] \
|| fail "AppArmor wrapper did not preserve smoke failure status 37"
printf '%s\n' \
"--non-interactive sysctl -q -w $USERNS_SYSCTL=0" \
"--non-interactive apparmor_parser --replace $APPARMOR_PROFILE" \
"smoke" \
"--non-interactive apparmor_parser --remove $APPARMOR_PROFILE" \
"--non-interactive sysctl -q -w $USERNS_SYSCTL=1" \
>"$WORK/apparmor-expected-order"
cmp -s "$WORK/apparmor-expected-order" "$APPARMOR_RECORD" \
|| fail "AppArmor profile was not loaded before and removed after a failing smoke"
[ ! -e "$APPARMOR_PROFILE.sysctl" ] \
|| fail "userns sysctl state file survived a failing smoke"

APPARMOR_TERM_RECORD="$WORK/apparmor-term-command-record"
APPARMOR_TERM_MARKER="$WORK/apparmor-term-smoke-started"
Expand Down Expand Up @@ -665,14 +684,61 @@ set -e
[ "$apparmor_term_status" -eq 143 ] \
|| fail "AppArmor TERM fixture exited $apparmor_term_status instead of 143"
printf '%s\n' \
"--non-interactive sysctl -q -w $USERNS_SYSCTL=0" \
"--non-interactive apparmor_parser --replace $APPARMOR_PROFILE" \
"smoke-start" \
"--non-interactive apparmor_parser --remove $APPARMOR_PROFILE" \
"--non-interactive sysctl -q -w $USERNS_SYSCTL=1" \
>"$WORK/apparmor-term-expected-order"
cmp -s "$WORK/apparmor-term-expected-order" "$APPARMOR_TERM_RECORD" \
|| fail "AppArmor profile was not removed exactly once after TERM"
[ ! -e "$APPARMOR_PROFILE.sysctl" ] \
|| fail "userns sysctl state file survived a TERM-interrupted smoke"
echo "[test-rpaths] ok: CI smoke grants userns only to the extracted executable"

# The sysctl toggle must be a no-op on hosts that are already permissive or
# that have no AppArmor userns restriction at all, so the release smoke never
# needs sudo outside the restricted-Ubuntu case it exists for.
USERNS_NOOP_BIN="$WORK/userns-noop-bin"
USERNS_NOOP_RECORD="$WORK/userns-noop-command-record"
USERNS_NOOP_STATE="$WORK/userns-noop.sysctl"
mkdir -p "$USERNS_NOOP_BIN"
printf '%s\n' \
'#!/usr/bin/env bash' \
'printf "%s\n" "$*" >>"$USERNS_NOOP_RECORD"' \
>"$USERNS_NOOP_BIN/sudo"
chmod +x "$USERNS_NOOP_BIN/sudo"
export USERNS_NOOP_RECORD
for permissive_value in 0 ""; do
: >"$USERNS_NOOP_RECORD"
rm -f "$USERNS_NOOP_STATE"
if [ -n "$permissive_value" ]; then
printf '%s\n' \
'#!/usr/bin/env bash' \
'[ "${1:-}" = "-n" ] && printf "%s\n" "0"' \
'exit 0' \
>"$USERNS_NOOP_BIN/sysctl"
else
# No such key on this host: sysctl exits non-zero and prints nothing.
printf '%s\n' '#!/usr/bin/env bash' 'exit 1' \
>"$USERNS_NOOP_BIN/sysctl"
fi
chmod +x "$USERNS_NOOP_BIN/sysctl"
PATH="$USERNS_NOOP_BIN:$PATH" \
relax_smoke_userns_restriction "$USERNS_NOOP_STATE" >/dev/null \
|| fail "relax_smoke_userns_restriction failed on a permissive host"
[ ! -s "$USERNS_NOOP_RECORD" ] \
|| fail "relax_smoke_userns_restriction used sudo on a permissive host"
[ ! -e "$USERNS_NOOP_STATE" ] \
|| fail "relax_smoke_userns_restriction recorded state it never changed"
PATH="$USERNS_NOOP_BIN:$PATH" \
restore_smoke_userns_restriction "$USERNS_NOOP_STATE" >/dev/null \
|| fail "restore_smoke_userns_restriction failed without recorded state"
[ ! -s "$USERNS_NOOP_RECORD" ] \
|| fail "restore_smoke_userns_restriction used sudo without recorded state"
done
echo "[test-rpaths] ok: userns sysctl toggle is a no-op on permissive hosts"

assert_runtime_layout_rejected missing-anylinux \
"missing anylinux.so" remove_anylinux
assert_runtime_layout_rejected missing-libxdo \
Expand Down
94 changes: 89 additions & 5 deletions scripts/release/validate-appimage-runtime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
# Env:
# APPIMAGE_RUNTIME_SMOKE — set to 1 to require the bounded Xvfb startup smoke;
# otherwise only static final-artifact checks run
# APPIMAGE_RUNTIME_APPARMOR_USERNS — set to 1 on Ubuntu 24.04 CI to load a
# temporary, per-executable AppArmor profile granting
# Chromium's sandbox access to user namespaces
# APPIMAGE_RUNTIME_APPARMOR_USERNS — set to 1 on Ubuntu 24.04 CI to give
# Chromium's sandbox access to user namespaces for
# the smoke window: relaxes
# kernel.apparmor_restrict_unprivileged_userns (and
# restores it afterwards) and loads a temporary,
# per-executable AppArmor profile

set -euo pipefail

Expand Down Expand Up @@ -208,6 +211,67 @@ smoke_extracted_apprun() {
echo "[appimage-runtime] Application remained alive for the 15-second startup window"
}

# Ubuntu 23.10+ (the hosted ubuntu-24.04 runner included) sets
# `kernel.apparmor_restrict_unprivileged_userns=1`, which denies unprivileged
# user-namespace creation to every *unconfined* process on the box. Chromium's
# zygote needs one, so CEF aborts during startup with
# `FATAL:zygote_host_impl_linux.cc] No usable sandbox!` and the smoke sees exit
# 133 (SIGTRAP) instead of the expected 124 (still alive at the timeout).
#
# `install_smoke_userns_profile` below is Chromium's second documented remedy —
# a per-executable AppArmor profile carrying `userns,`. It loads successfully
# but never takes effect here, because the profile attaches by execve path and
# the sharun launcher runs the app through the AppDir's bundled dynamic loader
# rather than exec'ing `shared/bin/OpenHuman` directly (Chromium then re-execs
# `/proc/self/exe` for the zygote). Toggling the sysctl is Chromium's first
# documented remedy and is path-independent, so it cannot miss the way the
# profile attachment does. The AppArmor profile is retained alongside it: it is
# harmless, and it keeps the narrower per-executable grant in place for hosts
# where the sysctl is unavailable.
#
# The original value is restored after the smoke window so the runner is left
# exactly as it was found.
SMOKE_USERNS_SYSCTL="kernel.apparmor_restrict_unprivileged_userns"

smoke_userns_sysctl_value() {
sysctl -n "$SMOKE_USERNS_SYSCTL" 2>/dev/null || true
}

relax_smoke_userns_restriction() {
local previous_file="$1"
local current
current="$(smoke_userns_sysctl_value)"

if [ -z "$current" ]; then
echo "[appimage-runtime] $SMOKE_USERNS_SYSCTL is absent; unprivileged user namespaces are already unrestricted"
return 0
fi
if [ "$current" = "0" ]; then
echo "[appimage-runtime] $SMOKE_USERNS_SYSCTL is already 0; leaving it unchanged"
return 0
fi

command -v sudo >/dev/null 2>&1 \
|| { runtime_validation_error "sudo is required to relax $SMOKE_USERNS_SYSCTL for the AppImage smoke"; return 1; }
sudo --non-interactive sysctl -q -w "$SMOKE_USERNS_SYSCTL=0" \
|| { runtime_validation_error "could not relax $SMOKE_USERNS_SYSCTL for the AppImage smoke"; return 1; }

printf '%s\n' "$current" >"$previous_file"
Comment on lines +256 to +259

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record sysctl state before relaxing userns

When the AppImage smoke runs on a restricted Ubuntu host and the job is cancelled or TERM'd just after sudo sysctl ...=0 takes effect but before $previous_file is written, the installed signal trap calls restore_smoke_userns_restriction with no state file and therefore skips the restore. That leaves unprivileged user namespaces enabled for the rest of the runner/session; write the previous value before flipping the sysctl (or otherwise mark that a restore is required) so the interrupt path can always put the host back.

Useful? React with 👍 / 👎.

echo "[appimage-runtime] Relaxed $SMOKE_USERNS_SYSCTL: $current -> 0 for the smoke window"
}

restore_smoke_userns_restriction() {
local previous_file="$1"
[ -s "$previous_file" ] || return 0

local previous
previous="$(cat "$previous_file")"
rm -f "$previous_file"
sudo --non-interactive sysctl -q -w "$SMOKE_USERNS_SYSCTL=$previous" \
|| { runtime_validation_error "could not restore $SMOKE_USERNS_SYSCTL to $previous"; return 1; }
echo "[appimage-runtime] Restored $SMOKE_USERNS_SYSCTL to $previous"
}

install_smoke_userns_profile() {
local appdir="$1"
local profile_file="$2"
Expand Down Expand Up @@ -248,6 +312,8 @@ smoke_extracted_apprun_with_userns() {
local log_file="$3"
local profile_file="$4"
local profile_loaded=0
# Derived rather than passed so the four-argument contract is unchanged.
local userns_sysctl_file="$profile_file.sysctl"
local previous_hup_trap previous_int_trap previous_term_trap
previous_hup_trap="$(trap -p HUP)"
previous_int_trap="$(trap -p INT)"
Expand All @@ -266,6 +332,8 @@ smoke_extracted_apprun_with_userns() {
remove_smoke_userns_profile "$profile_file" \
|| echo "[appimage-runtime] ERROR: AppArmor cleanup failed after smoke interruption" >&2
fi
restore_smoke_userns_restriction "$userns_sysctl_file" \
|| echo "[appimage-runtime] ERROR: $SMOKE_USERNS_SYSCTL restore failed after smoke interruption" >&2
}

interrupt_smoke_with_userns() {
Expand All @@ -278,7 +346,15 @@ smoke_extracted_apprun_with_userns() {
trap 'interrupt_smoke_with_userns 130' INT
trap 'interrupt_smoke_with_userns 143' TERM

rm -f "$userns_sysctl_file"
if ! relax_smoke_userns_restriction "$userns_sysctl_file"; then
restore_smoke_signal_traps
return 1
fi

if ! install_smoke_userns_profile "$appdir" "$profile_file"; then
restore_smoke_userns_restriction "$userns_sysctl_file" \
|| echo "[appimage-runtime] ERROR: $SMOKE_USERNS_SYSCTL restore failed after AppArmor setup failure" >&2
restore_smoke_signal_traps
return 1
fi
Expand All @@ -299,10 +375,18 @@ smoke_extracted_apprun_with_userns() {
remove_status=$?
fi

local restore_status
if restore_smoke_userns_restriction "$userns_sysctl_file"; then
restore_status=0
else
restore_status=$?
fi

restore_smoke_signal_traps
# Preserve the original smoke failure even if profile cleanup also fails.
# Preserve the original smoke failure even if cleanup also fails.
[ "$smoke_status" -eq 0 ] || return "$smoke_status"
return "$remove_status"
[ "$remove_status" -eq 0 ] || return "$remove_status"
return "$restore_status"
}

validate_final_appimage() (
Expand Down
Loading