fix(appimage): anchor sharun library paths to AppDir (#5189)

This commit is contained in:
Steven Enamakel
2026-07-25 07:20:12 +03:00
committed by GitHub
parent ca0e65f196
commit 79e2fc2566
6 changed files with 2554 additions and 85 deletions
+222 -71
View File
@@ -23,9 +23,16 @@
# APPIMAGETOOL_SHA256 — expected SHA256 of the download
# (verified before use when set; rotate
# alongside APPIMAGETOOL_URL)
# APPIMAGE_RUNTIME_SMOKE — set to 1 to run the final AppRun
# startup smoke after static validation
# APPIMAGE_RUNTIME_VALIDATOR — override the final-artifact validator
# command (intended for regression tests)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APPIMAGE_RUNTIME_VALIDATOR="${APPIMAGE_RUNTIME_VALIDATOR:-$SCRIPT_DIR/validate-appimage-runtime.sh}"
EXCLUDE_PATTERNS=(
'libGL.so.*'
'libGLX.so.*'
@@ -293,58 +300,111 @@ rewrite_sharun_lib_path() {
local lib_path="$appdir/shared/lib/lib.path"
[ -s "$lib_path" ] || return 1
if ! grep -E '(^|[+:])/home/runner/|(^|[+:])/__w/' "$lib_path" >/dev/null; then
local -a normalized_entries=()
local entry normalized suffix existing duplicate index
local normalized_count=0
while IFS= read -r entry || [ -n "$entry" ]; do
if [ -z "$entry" ]; then
echo "[strip-libs] ERROR: shared/lib/lib.path contains an empty entry" >&2
return 1
fi
case "$entry" in
*$'\r'*|*:*)
echo "[strip-libs] ERROR: shared/lib/lib.path contains a malformed entry: '$entry'" >&2
return 1
;;
[[:space:]]*|*[[:space:]])
echo "[strip-libs] ERROR: shared/lib/lib.path contains a malformed entry: '$entry'" >&2
return 1
;;
esac
case "$entry" in
+)
normalized="+"
;;
+/*)
normalized="$entry"
;;
/*)
case "$entry" in
*/squashfs-root/shared/lib)
normalized="+"
;;
*/squashfs-root/shared/lib/*)
suffix="${entry#*/squashfs-root/shared/lib/}"
normalized="+/$suffix"
;;
*/appimage_deb/data/usr/lib)
normalized="+"
;;
*/appimage_deb/data/usr/lib/*)
suffix="${entry#*/appimage_deb/data/usr/lib/}"
normalized="+/$suffix"
;;
# lib4bin/quick-sharun staging roots vary by runner and tool
# version; older artifacts use an arbitrary absolute prefix ending
# in data/usr/lib rather than the appimage_deb directory above.
# Match that stable staged-layout tail here. The derived marker is
# validated below against the extracted AppDir's shared/lib tree.
*/data/usr/lib)
normalized="+"
;;
*/data/usr/lib/*)
suffix="${entry#*/data/usr/lib/}"
normalized="+/$suffix"
;;
*)
continue
;;
esac
;;
*)
echo "[strip-libs] ERROR: shared/lib/lib.path contains a malformed entry: '$entry'" >&2
return 1
;;
esac
case "${normalized#+}" in
*+*)
echo "[strip-libs] ERROR: shared/lib/lib.path contains a malformed entry: '$entry'" >&2
return 1
;;
esac
duplicate=0
index=0
while [ "$index" -lt "$normalized_count" ]; do
existing="${normalized_entries[$index]}"
if [ "$existing" = "$normalized" ]; then
duplicate=1
break
fi
index=$((index + 1))
done
if [ "$duplicate" -eq 0 ]; then
normalized_entries[$normalized_count]="$normalized"
normalized_count=$((normalized_count + 1))
fi
done <"$lib_path"
if [ "$normalized_count" -eq 0 ]; then
echo "[strip-libs] ERROR: shared/lib/lib.path contains no valid AppDir library entries" >&2
return 1
fi
echo "[strip-libs] rewriting CI runner paths in shared/lib/lib.path"
local raw
raw="$(cat "$lib_path")"
local -a entries=()
local entry
while IFS= read -r entry; do
[ -n "$entry" ] || continue
entries+=("$entry")
done < <(printf '%s' "$raw" | tr '+:' '\n\n')
local -a cleaned=()
local rel seen_set=""
for entry in "${entries[@]}"; do
case "$entry" in
/home/runner/*|/__w/*)
rel="${entry##*/squashfs-root/}"
if [ "$rel" = "$entry" ]; then
rel="${entry##*/data/}"
[ "$rel" != "$entry" ] || continue
fi
;;
/*)
continue
;;
*)
rel="$entry"
;;
esac
[ -d "$appdir/$rel" ] || continue
case "+${seen_set}+" in
*"+${rel}+"*) continue ;;
esac
seen_set="${seen_set}+${rel}"
cleaned+=("$rel")
done
if [ "${#cleaned[@]}" -eq 0 ]; then
cleaned=("shared/lib")
local normalized_file
normalized_file="$(mktemp "$lib_path.tmp.XXXXXX")"
printf '%s\n' "${normalized_entries[@]}" >"$normalized_file"
if cmp -s "$normalized_file" "$lib_path"; then
rm -f "$normalized_file"
return 1
fi
local joined
joined="$(IFS='+'; echo "${cleaned[*]}")"
printf '%s' "$joined" > "$lib_path"
echo "[strip-libs] lib.path rewritten to: $joined"
mv "$normalized_file" "$lib_path"
echo "[strip-libs] normalized shared/lib/lib.path entries:"
printf '[strip-libs] %s\n' "${normalized_entries[@]}"
return 0
}
validate_sharun_lib_path() {
@@ -356,13 +416,89 @@ validate_sharun_lib_path() {
local lib_path="$appdir/shared/lib/lib.path"
if [ ! -s "$lib_path" ]; then
echo "[strip-libs] ERROR: sharun AppImage is missing shared/lib/lib.path; refusing to ship an AppImage that exits with 'Interpreter not found!'" >&2
exit 1
return 1
fi
if grep -E '(^|[+:])/home/runner/|(^|[+:])/__w/' "$lib_path" >/dev/null; then
echo "[strip-libs] ERROR: shared/lib/lib.path contains CI runner paths; regenerate it with bundle-relative entries before release." >&2
exit 1
local library_root="$appdir/shared/lib"
local canonical_root
if [ ! -d "$library_root" ] || ! canonical_root="$(realpath "$library_root")"; then
echo "[strip-libs] ERROR: sharun AppImage is missing shared/lib; refusing to validate library paths" >&2
return 1
fi
local entry suffix resolved canonical_resolved
local entry_count=0
while IFS= read -r entry || [ -n "$entry" ]; do
entry_count=$((entry_count + 1))
if [ -z "$entry" ]; then
echo "[strip-libs] ERROR: invalid sharun lib.path entry '': empty entries are not allowed" >&2
return 1
fi
case "$entry" in
*$'\r'*)
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': carriage returns are not allowed" >&2
return 1
;;
*:*)
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': loader separators are not allowed" >&2
return 1
;;
[[:space:]]*|*[[:space:]])
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': surrounding whitespace is not allowed" >&2
return 1
;;
esac
case "$entry" in
+)
suffix=""
resolved="$library_root"
;;
+/*)
suffix="${entry#+/}"
case "$suffix" in
*+*)
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': extra '+' markers are not allowed" >&2
return 1
;;
""|/*|*/|*//*|.|..|./*|../*|*/./*|*/../*|*/.|*/..)
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': path components must be non-empty descendants" >&2
return 1
;;
esac
resolved="$library_root/$suffix"
;;
*)
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': expected '+' or '+/suffix'" >&2
return 1
;;
esac
if [ ! -d "$resolved" ]; then
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': resolved directory does not exist" >&2
return 1
fi
if ! canonical_resolved="$(realpath "$resolved")"; then
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': could not canonicalize resolved directory" >&2
return 1
fi
case "$canonical_resolved" in
"$canonical_root"|"$canonical_root"/*)
;;
*)
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': resolved directory escapes shared/lib" >&2
return 1
;;
esac
done <"$lib_path"
if [ "$entry_count" -eq 0 ]; then
echo "[strip-libs] ERROR: invalid sharun lib.path entry '': at least one entry is required" >&2
return 1
fi
return 0
}
# is_elf — true if the file begins with the ELF magic, regardless of the +x
@@ -476,9 +612,9 @@ sanitize_elf_rpaths() {
return $rewrote
}
# validate_appimage_required_libs — fail the build loudly if a library the app
# binary hard-links (NEEDED) but which MUST travel inside the AppImage is absent
# from the bundle.
# validate_appimage_required_libs — fail the build loudly if the sharun preload
# library or a runtime library the app hard-links (NEEDED) is absent from the
# bundle.
#
# Problem (issues #3224 and #4020): the app links libxdo.so.3 via enigo
# (`#[link(name = "xdo")]`, used by src/openhuman/tools/impl/computer for Linux
@@ -490,6 +626,8 @@ sanitize_elf_rpaths() {
# linux_cef_deb_runtime_e2e; the AppImage path had no equivalent. This turns a
# silent runtime segfault into a loud build failure. CEF is staged separately
# from the ldd walk, so verify its runtime library survived bundling as well.
# anylinux.so establishes sharun's portable runtime before either dependency
# can load, so it is part of the same release contract.
validate_appimage_required_libs() {
local appdir="$1"
if ! uses_sharun_launcher "$appdir"; then
@@ -497,7 +635,7 @@ validate_appimage_required_libs() {
fi
local root pattern found
for pattern in 'libxdo.so*' 'libcef.so*'; do
for pattern in 'anylinux.so' 'libxdo.so*' 'libcef.so*'; do
found=0
for root in "$appdir/shared/lib" "$appdir/usr/lib" "$appdir/lib"; do
[ -d "$root" ] || continue
@@ -509,6 +647,9 @@ validate_appimage_required_libs() {
[ "$found" -eq 1 ] && continue
case "$pattern" in
anylinux.so)
echo "[strip-libs] ERROR: AppImage is missing anylinux.so — the sharun preload library was not copied into the final bundle. The AppImage launcher cannot establish its portable runtime without it." >&2
;;
libxdo.so\*)
echo "[strip-libs] ERROR: AppImage is missing libxdo.so.* — the enigo NEEDED dependency was not bundled (issue #3224). The app would segfault on launch on hosts without the legacy libxdo soname (e.g. Arch). Ensure libxdo-dev is installed on the build runner so lib4bin's ldd-walk bundles it." >&2
;;
@@ -520,21 +661,15 @@ validate_appimage_required_libs() {
done
}
# patch_apprun_sharun_cwd — inject `cd "$APPDIR"` into AppRun before the final
# exec call so sharun resolves preload/library paths relative to the AppDir
# rather than the caller's CWD.
# patch_apprun_sharun_cwd — retain historical shell-AppRun CWD hardening.
#
# Problem (issue #2822): sharun reads its `.preload` entry and library search
# paths relative to the process CWD. When a user launches the AppImage from
# any directory other than the AppDir itself (e.g. double-click from ~/Downloads)
# CWD != AppDir, so ld.so can't find `anylinux.so` (preload) or `libcef.so`
# (LD_LIBRARY_PATH entry). SHARUN_DIR is set correctly — the AppDir IS known —
# but sharun doesn't use it to anchor the preload/library arguments it hands to
# ld.so.
# Canonical `+` entries in shared/lib/lib.path are the primary fix for released
# layouts, where AppRun is the sharun ELF launcher itself. sharun expands `+`
# relative to the AppDir regardless of the caller's CWD.
#
# Fix: prepend `cd "$APPDIR"` to the exec line in AppRun so the working
# directory is always the AppDir by the time sharun/the binary runs. This
# mirrors the verified manual workaround from the bug report.
# Older layouts may instead have a shell AppRun that ultimately execs sharun.
# Keep prepending `cd "$APPDIR"` there as compatibility hardening, without
# treating it as the released ELF launcher's library-path repair.
#
# Returns 0 (true) if the AppRun was modified, 1 if no change was needed.
patch_apprun_sharun_cwd() {
@@ -585,15 +720,20 @@ patch_apprun_sharun_cwd() {
&& grep -Eq "$patched_line_re" "$tmp_apprun"; then
chmod --reference="$apprun" "$tmp_apprun"
mv "$tmp_apprun" "$apprun"
echo "[strip-libs] patched AppRun: added 'cd \"\$APPDIR\"' before exec to fix sharun CWD preload resolution (issue #2822)"
echo "[strip-libs] patched historical shell AppRun: added 'cd \"\$APPDIR\"' before exec"
return 0
else
rm -f "$tmp_apprun"
echo "[strip-libs] WARNING: could not locate 'exec \"\$@\"' in AppRun — sharun CWD fix not applied; AppImage may fail to launch from non-AppDir CWDs" >&2
echo "[strip-libs] WARNING: could not locate 'exec \"\$@\"' in historical shell AppRun; canonical '+' lib.path entries remain the released-layout fix" >&2
return 1
fi
}
validate_rebuilt_appimage() {
local rebuilt_path="$1"
"$APPIMAGE_RUNTIME_VALIDATOR" "$rebuilt_path" || return 1
}
strip_one_appimage() {
local img="$1"
local original
@@ -659,7 +799,10 @@ strip_one_appimage() {
if sanitize_elf_rpaths "$appdir"; then
rewrote_rpaths=1
fi
validate_sharun_lib_path "$appdir"
if ! validate_sharun_lib_path "$appdir"; then
rm -rf "$workdir"
return 1
fi
if ! validate_appimage_required_libs "$appdir"; then
rm -rf "$workdir"
return 1
@@ -667,6 +810,10 @@ strip_one_appimage() {
if [ "$removed" -eq 0 ] && [ "$added_loader" -eq 0 ] && [ "$rewrote_libpath" -eq 0 ] && [ "$patched_apprun" -eq 0 ] && [ "$rewrote_rpaths" -eq 0 ]; then
echo "[strip-libs] No graphics libs, missing sharun interpreter, or build-machine RPATHs found in $original; leaving unchanged."
if ! validate_rebuilt_appimage "$original"; then
rm -rf "$workdir"
return 1
fi
rm -rf "$workdir"
return
fi
@@ -681,6 +828,10 @@ strip_one_appimage() {
--no-appstream squashfs-root "$rebuilt" >/dev/null
)
mv "$rebuilt" "$original"
if ! validate_rebuilt_appimage "$original"; then
rm -rf "$workdir"
return 1
fi
rm -rf "$workdir"
MODIFIED_PATHS+=("$original")
}
+790 -5
View File
@@ -4,8 +4,11 @@
# 1. sanitize_elf_rpaths — strips /home/runner|/__w build-machine
# RPATHs from bundled ELFs, rewriting to
# $ORIGIN-relative.
# 2. validate_appimage_required_libs — hard-fails when libxdo.so.* or
# libcef.so is missing from a sharun AppDir.
# 2. validate_appimage_required_libs — hard-fails when anylinux.so,
# libxdo.so.*, or libcef.so is missing from
# a sharun AppDir.
# 3. validate-appimage-runtime.sh — checks the final extracted sharun layout
# and the pre-signing validation seam.
#
# Linux-only: needs `patchelf` and a host ELF to mutate. Skips cleanly (exit 0)
# on macOS / any host without patchelf so it is a no-op on dev boxes and a real
@@ -42,11 +45,785 @@ done
# shellcheck source=/dev/null
source "$TARGET"
RUNTIME_VALIDATOR="$SCRIPT_DIR/validate-appimage-runtime.sh"
[ -f "$RUNTIME_VALIDATOR" ] \
|| { echo "[test-rpaths] FAIL: $RUNTIME_VALIDATOR not found" >&2; exit 1; }
# shellcheck source=/dev/null
source "$RUNTIME_VALIDATOR"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
fail() { echo "[test-rpaths] FAIL: $1" >&2; exit 1; }
make_sharun_appdir() {
local appdir="$1"
mkdir -p "$appdir/shared/bin" "$appdir/shared/lib/plugins" "$appdir/bin"
cp "$HOST_ELF" "$appdir/sharun"
printf 'Interpreter not found!' >> "$appdir/sharun"
chmod +x "$appdir/sharun"
ln "$appdir/sharun" "$appdir/AppRun"
ln "$appdir/sharun" "$appdir/bin/OpenHuman"
cp "$HOST_ELF" "$appdir/shared/bin/OpenHuman"
chmod +x "$appdir/shared/bin/OpenHuman"
}
assert_lib_path_contents() {
local fixture="$1"
local expected="$2"
local expected_file="$fixture/expected-lib.path"
printf '%s\n' "$expected" >"$expected_file"
cmp -s "$expected_file" "$fixture/shared/lib/lib.path" \
|| fail "unexpected normalized lib.path for ${fixture##*/}"
}
# --- Case 0: rewrite_sharun_lib_path emits sharun-native marker entries -----
REWRITE_SQUASHFS="$WORK/rewrite-squashfs"
make_sharun_appdir "$REWRITE_SQUASHFS"
printf '%s\n' "$WORK/squashfs-root/shared/lib" >"$REWRITE_SQUASHFS/shared/lib/lib.path"
rewrite_sharun_lib_path "$REWRITE_SQUASHFS" \
|| fail "squashfs-root shared/lib was not normalized"
assert_lib_path_contents "$REWRITE_SQUASHFS" '+'
REWRITE_STAGING="$WORK/rewrite-staging"
make_sharun_appdir "$REWRITE_STAGING"
printf '%s\n' "$WORK/appimage_deb/data/usr/lib" >"$REWRITE_STAGING/shared/lib/lib.path"
rewrite_sharun_lib_path "$REWRITE_STAGING" \
|| fail "appimage_deb data/usr/lib was not normalized"
assert_lib_path_contents "$REWRITE_STAGING" '+'
REWRITE_DESCENDANTS="$WORK/rewrite-descendants"
make_sharun_appdir "$REWRITE_DESCENDANTS"
printf '%s\n%s\n' \
"$WORK/squashfs-root/shared/lib/plugins" \
"$WORK/appimage_deb/data/usr/lib/plugins" \
>"$REWRITE_DESCENDANTS/shared/lib/lib.path"
rewrite_sharun_lib_path "$REWRITE_DESCENDANTS" \
|| fail "AppDir library descendants were not normalized"
assert_lib_path_contents "$REWRITE_DESCENDANTS" '+/plugins'
REWRITE_MIXED="$WORK/rewrite-mixed"
make_sharun_appdir "$REWRITE_MIXED"
printf '%s\n' '+' '+/plugins' '+' '/opt/unrelated/lib' \
>"$REWRITE_MIXED/shared/lib/lib.path"
rewrite_sharun_lib_path "$REWRITE_MIXED" \
|| fail "mixed marker entries were not normalized"
assert_lib_path_contents "$REWRITE_MIXED" "$(printf '+\n+/plugins')"
cp "$REWRITE_MIXED/shared/lib/lib.path" "$REWRITE_MIXED/lib.path.before"
if rewrite_sharun_lib_path "$REWRITE_MIXED"; then
fail "rewrite_sharun_lib_path was not idempotent"
fi
cmp -s "$REWRITE_MIXED/lib.path.before" "$REWRITE_MIXED/shared/lib/lib.path" \
|| fail "idempotent rewrite changed lib.path bytes"
REWRITE_INVALID="$WORK/rewrite-invalid"
make_sharun_appdir "$REWRITE_INVALID"
printf '%s\n' '/opt/unrelated/lib' >"$REWRITE_INVALID/shared/lib/lib.path"
if ( rewrite_sharun_lib_path "$REWRITE_INVALID" ) 2>"$WORK/rewrite.err"; then
fail "rewrite accepted a lib.path with no AppDir library root"
fi
grep -F "no valid AppDir library entries" "$WORK/rewrite.err" >/dev/null \
|| fail "rewrite failure did not explain why lib.path was rejected"
assert_lib_path_contents "$REWRITE_INVALID" '/opt/unrelated/lib'
REWRITE_BARE_RELATIVE="$WORK/rewrite-bare-relative"
make_sharun_appdir "$REWRITE_BARE_RELATIVE"
printf '%s\n' '+' 'shared/lib' \
>"$REWRITE_BARE_RELATIVE/shared/lib/lib.path"
if ( rewrite_sharun_lib_path "$REWRITE_BARE_RELATIVE" ) \
2>"$REWRITE_BARE_RELATIVE/error.log"; then
fail "rewrite accepted a mixed lib.path containing a bare-relative entry"
fi
grep -F "malformed entry: 'shared/lib'" \
"$REWRITE_BARE_RELATIVE/error.log" >/dev/null \
|| fail "rewrite bare-relative error did not identify 'shared/lib'"
assert_lib_path_contents "$REWRITE_BARE_RELATIVE" "$(printf '+\nshared/lib')"
REWRITE_MARKER_INJECTION="$WORK/rewrite-marker-injection"
make_sharun_appdir "$REWRITE_MARKER_INJECTION"
mkdir -p "$REWRITE_MARKER_INJECTION/shared/lib/plugins+extra"
printf '%s\n' '+/plugins+extra' \
>"$REWRITE_MARKER_INJECTION/shared/lib/lib.path"
if ( rewrite_sharun_lib_path "$REWRITE_MARKER_INJECTION" ) \
2>"$REWRITE_MARKER_INJECTION/error.log"; then
fail "rewrite accepted an extra marker in a canonical suffix"
fi
grep -F "malformed entry: '+/plugins+extra'" \
"$REWRITE_MARKER_INJECTION/error.log" >/dev/null \
|| fail "rewrite marker-injection error did not identify '+/plugins+extra'"
REWRITE_DERIVED_MARKER="$WORK/rewrite-derived-marker"
make_sharun_appdir "$REWRITE_DERIVED_MARKER"
derived_marker_path="$WORK/squashfs-root/shared/lib/plugins+extra"
printf '%s\n' "$derived_marker_path" \
>"$REWRITE_DERIVED_MARKER/shared/lib/lib.path"
if ( rewrite_sharun_lib_path "$REWRITE_DERIVED_MARKER" ) \
2>"$REWRITE_DERIVED_MARKER/error.log"; then
fail "rewrite accepted an extra marker in a derived suffix"
fi
grep -F "malformed entry: '$derived_marker_path'" \
"$REWRITE_DERIVED_MARKER/error.log" >/dev/null \
|| fail "derived marker-injection error did not identify '$derived_marker_path'"
echo "[test-rpaths] ok: sharun lib.path normalization is canonical and idempotent"
# --- Case 0b: validate_sharun_lib_path rejects unsafe loader entries --------
assert_lib_path_rejected() {
local label="$1"
local value="$2"
local expected_entry="$3"
local fixture="$WORK/reject-$label"
make_sharun_appdir "$fixture"
printf '%s\n' "$value" >"$fixture/shared/lib/lib.path"
if ( validate_sharun_lib_path "$fixture" ) 2>"$fixture/error.log"; then
fail "validate_sharun_lib_path accepted $label"
fi
grep -F "invalid sharun lib.path entry '$expected_entry':" \
"$fixture/error.log" >/dev/null \
|| fail "$label error did not identify offending entry '$expected_entry'"
}
assert_lib_path_rejected bare-relative 'shared/lib' 'shared/lib'
assert_lib_path_rejected runner-absolute \
'/home/runner/work/openhuman/squashfs-root/shared/lib' \
'/home/runner/work/openhuman/squashfs-root/shared/lib'
assert_lib_path_rejected actions-absolute \
'/__w/openhuman/openhuman/appimage_deb/data/usr/lib' \
'/__w/openhuman/openhuman/appimage_deb/data/usr/lib'
assert_lib_path_rejected parent-traversal '+/../outside' '+/../outside'
assert_lib_path_rejected nested-traversal \
'+/plugins/../../outside' '+/plugins/../../outside'
assert_lib_path_rejected missing-directory '+/missing' '+/missing'
assert_lib_path_rejected loader-separator \
'+/plugins:shared/lib' '+/plugins:shared/lib'
assert_lib_path_rejected leading-whitespace ' +' ' +'
assert_lib_path_rejected trailing-whitespace '+ ' '+ '
VALID_ROOT="$WORK/valid-root"
make_sharun_appdir "$VALID_ROOT"
printf '%s\n' '+' >"$VALID_ROOT/shared/lib/lib.path"
validate_sharun_lib_path "$VALID_ROOT" \
|| fail "validate_sharun_lib_path rejected canonical root marker"
VALID_DESCENDANT="$WORK/valid-descendant"
make_sharun_appdir "$VALID_DESCENDANT"
printf '%s\n' '+' '+/plugins' >"$VALID_DESCENDANT/shared/lib/lib.path"
cp "$VALID_DESCENDANT/shared/lib/lib.path" "$VALID_DESCENDANT/lib.path.before"
validate_sharun_lib_path "$VALID_DESCENDANT" \
|| fail "validate_sharun_lib_path rejected canonical descendant marker"
validate_sharun_lib_path "$VALID_DESCENDANT" \
|| fail "validate_sharun_lib_path rejected a second validation pass"
cmp -s \
"$VALID_DESCENDANT/lib.path.before" \
"$VALID_DESCENDANT/shared/lib/lib.path" \
|| fail "validate_sharun_lib_path mutated a normalized file"
MARKER_INJECTION="$WORK/marker-injection"
make_sharun_appdir "$MARKER_INJECTION"
mkdir -p "$MARKER_INJECTION/shared/lib/plugins+extra"
printf '%s\n' '+/plugins+extra' >"$MARKER_INJECTION/shared/lib/lib.path"
if ( validate_sharun_lib_path "$MARKER_INJECTION" ) \
2>"$MARKER_INJECTION/error.log"; then
fail "validate_sharun_lib_path accepted an extra marker in a suffix"
fi
grep -F "invalid sharun lib.path entry '+/plugins+extra':" \
"$MARKER_INJECTION/error.log" >/dev/null \
|| fail "marker-injection error did not identify '+/plugins+extra'"
ESCAPE_DIR="$WORK/escape-symlink"
make_sharun_appdir "$ESCAPE_DIR"
mkdir -p "$WORK/outside"
ln -s "$WORK/outside" "$ESCAPE_DIR/shared/lib/escape"
printf '%s\n' '+/escape' >"$ESCAPE_DIR/shared/lib/lib.path"
if ( validate_sharun_lib_path "$ESCAPE_DIR" ) 2>"$ESCAPE_DIR/error.log"; then
fail "validate_sharun_lib_path accepted a symlink escaping shared/lib"
fi
grep -F "invalid sharun lib.path entry '+/escape':" \
"$ESCAPE_DIR/error.log" >/dev/null \
|| fail "symlink escape error did not identify offending entry '+/escape'"
RELEASED_LAYOUT="$WORK/released-layout"
make_sharun_appdir "$RELEASED_LAYOUT"
printf '%s\n' '+' >"$RELEASED_LAYOUT/shared/lib/lib.path"
[ "$RELEASED_LAYOUT/AppRun" -ef "$RELEASED_LAYOUT/sharun" ] \
|| fail "AppRun fixture is not hard-linked to sharun"
[ "$RELEASED_LAYOUT/bin/OpenHuman" -ef "$RELEASED_LAYOUT/sharun" ] \
|| fail "bin/OpenHuman fixture is not hard-linked to sharun"
is_executable_elf "$RELEASED_LAYOUT/AppRun" \
|| fail "released-style AppRun fixture is not ELF"
uses_sharun_launcher "$RELEASED_LAYOUT" \
|| fail "released-style hard-linked ELF launcher was not detected"
validate_sharun_lib_path "$RELEASED_LAYOUT" \
|| fail "released-style AppDir has an invalid canonical lib.path"
CALLER="$WORK/caller"
mkdir -p "$CALLER"
(
cd "$CALLER"
expanded="$(
sed "s|+|$RELEASED_LAYOUT/shared/lib|g" \
"$RELEASED_LAYOUT/shared/lib/lib.path" |
paste -sd ':' -
)"
[ "$expanded" = "$RELEASED_LAYOUT/shared/lib" ] \
|| fail "sharun marker did not expand beneath the fixture AppDir"
)
released_apprun_inode="$(ls -di "$RELEASED_LAYOUT/AppRun")"
if patch_apprun_sharun_cwd "$RELEASED_LAYOUT"; then
fail "patch_apprun_sharun_cwd modified a released-style ELF AppRun"
fi
[ "$released_apprun_inode" = "$(ls -di "$RELEASED_LAYOUT/AppRun")" ] \
|| fail "patch_apprun_sharun_cwd changed the released-style ELF AppRun inode"
echo "[test-rpaths] ok: sharun lib.path validation is fail-closed for the released ELF launcher"
# --- Case 0c: final extracted runtime layout is validated before signing ----
make_runtime_appdir() {
local appdir="$1"
make_sharun_appdir "$appdir"
printf '%s\n' '+' >"$appdir/shared/lib/lib.path"
cp "$HOST_ELF" "$appdir/shared/lib/anylinux.so"
cp "$HOST_ELF" "$appdir/shared/lib/libxdo.so.3"
cp "$HOST_ELF" "$appdir/shared/lib/libcef.so"
local elf
for elf in \
"$appdir/AppRun" \
"$appdir/sharun" \
"$appdir/bin/OpenHuman" \
"$appdir/shared/bin/OpenHuman" \
"$appdir/shared/lib/anylinux.so" \
"$appdir/shared/lib/libxdo.so.3" \
"$appdir/shared/lib/libcef.so"; do
patchelf --remove-rpath "$elf"
done
}
assert_runtime_layout_rejected() {
local label="$1"
local expected="$2"
shift 2
local fixture="$WORK/runtime-$label"
make_runtime_appdir "$fixture"
"$@" "$fixture"
if ( APPIMAGE_EXPECTED_NEEDED="" validate_extracted_appdir "$fixture" ) \
>"$fixture/output.log" 2>&1; then
fail "validate_extracted_appdir accepted $label"
fi
grep -F "$expected" "$fixture/output.log" >/dev/null \
|| fail "$label failure did not report '$expected'"
}
remove_anylinux() { rm -f "$1/shared/lib/anylinux.so"; }
remove_libxdo() { rm -f "$1/shared/lib/libxdo.so.3"; }
remove_libcef() { rm -f "$1/shared/lib/libcef.so"; }
remove_real_app() { rm -f "$1/shared/bin/OpenHuman"; }
replace_real_app_with_text() {
rm -f "$1/shared/bin/OpenHuman"
printf '%s\n' 'not an ELF' >"$1/shared/bin/OpenHuman"
chmod +x "$1/shared/bin/OpenHuman"
}
replace_apprun() {
rm -f "$1/AppRun"
cp "$HOST_ELF" "$1/AppRun"
chmod +x "$1/AppRun"
}
replace_bin_launcher() {
rm -f "$1/bin/OpenHuman"
cp "$HOST_ELF" "$1/bin/OpenHuman"
chmod +x "$1/bin/OpenHuman"
}
inject_runner_rpath() {
patchelf --set-rpath \
'/home/runner/work/openhuman/openhuman/shared/lib' \
"$1/shared/lib/libcef.so"
}
inject_actions_rpath() {
patchelf --set-rpath \
'/__w/openhuman/openhuman/shared/lib' \
"$1/shared/lib/libcef.so"
}
RUNTIME_COMPLETE="$WORK/runtime-complete"
make_runtime_appdir "$RUNTIME_COMPLETE"
APPIMAGE_EXPECTED_NEEDED="" validate_extracted_appdir "$RUNTIME_COMPLETE" \
|| fail "validate_extracted_appdir rejected a complete runtime fixture"
RUNTIME_EQUIVALENT="$WORK/runtime-equivalent-launchers"
make_runtime_appdir "$RUNTIME_EQUIVALENT"
rm "$RUNTIME_EQUIVALENT/AppRun" "$RUNTIME_EQUIVALENT/bin/OpenHuman"
cp "$RUNTIME_EQUIVALENT/sharun" "$RUNTIME_EQUIVALENT/AppRun"
cp "$RUNTIME_EQUIVALENT/sharun" "$RUNTIME_EQUIVALENT/bin/OpenHuman"
chmod +x "$RUNTIME_EQUIVALENT/AppRun" "$RUNTIME_EQUIVALENT/bin/OpenHuman"
APPIMAGE_EXPECTED_NEEDED="" validate_extracted_appdir "$RUNTIME_EQUIVALENT" \
|| fail "validate_extracted_appdir rejected byte-equivalent launchers"
FAKE_APPIMAGE="$WORK/final-fixture.AppImage"
printf '%s\n' \
'#!/usr/bin/env bash' \
'[ "$1" = "--appimage-extract" ] || exit 9' \
'cp -R "$EXTRACT_SOURCE" squashfs-root' \
>"$FAKE_APPIMAGE"
chmod +x "$FAKE_APPIMAGE"
export EXTRACT_SOURCE="$RUNTIME_COMPLETE"
APPIMAGE_EXPECTED_NEEDED="" validate_final_appimage "$FAKE_APPIMAGE" \
|| fail "validate_final_appimage rejected a complete extracted fixture"
if APPIMAGE_EXPECTED_NEEDED="" \
"$RUNTIME_VALIDATOR" "$FAKE_APPIMAGE" \
>"$WORK/direct-validator.log" 2>&1; then
fail "direct validator allowed a fixture override to disable production NEEDED checks"
fi
grep -F "missing NEEDED entry 'libxdo.so.3'" \
"$WORK/direct-validator.log" >/dev/null \
|| fail "direct validator did not enforce the production NEEDED contract"
RUNTIME_BAD_LIBPATH="$WORK/runtime-bad-libpath"
cp -R "$RUNTIME_COMPLETE" "$RUNTIME_BAD_LIBPATH"
printf '%s\n' 'shared/lib' >"$RUNTIME_BAD_LIBPATH/shared/lib/lib.path"
if ( APPIMAGE_EXPECTED_NEEDED="" \
validate_extracted_appdir "$RUNTIME_BAD_LIBPATH" ) \
>"$RUNTIME_BAD_LIBPATH/conditional.log" 2>&1; then
fail "conditional validate_extracted_appdir suppressed lib.path validation failure"
fi
RUNTIME_FINAL_INVALID="$WORK/runtime-final-invalid"
cp -R "$RUNTIME_COMPLETE" "$RUNTIME_FINAL_INVALID"
rm -f "$RUNTIME_FINAL_INVALID/shared/lib/anylinux.so"
if ( EXTRACT_SOURCE="$RUNTIME_FINAL_INVALID" APPIMAGE_EXPECTED_NEEDED="" \
validate_final_appimage "$FAKE_APPIMAGE" ) \
>"$WORK/conditional-final.log" 2>&1; then
fail "conditional validate_final_appimage suppressed extracted-layout failure"
fi
REAL_PATCHELF="$(command -v patchelf)"
PATCHELF_FAIL_BIN="$WORK/patchelf-fail-bin"
mkdir -p "$PATCHELF_FAIL_BIN"
printf '%s\n' \
'#!/usr/bin/env bash' \
'if [ "$1" = "--print-needed" ]; then exit 42; fi' \
'exec "$REAL_PATCHELF" "$@"' \
>"$PATCHELF_FAIL_BIN/patchelf"
chmod +x "$PATCHELF_FAIL_BIN/patchelf"
export REAL_PATCHELF
if ( PATH="$PATCHELF_FAIL_BIN:$PATH" APPIMAGE_EXPECTED_NEEDED="" \
validate_extracted_appdir "$RUNTIME_COMPLETE" ) \
>"$WORK/patchelf-failure.log" 2>&1; then
fail "conditional validate_extracted_appdir suppressed patchelf failure"
fi
SMOKE_BIN="$WORK/smoke-bin"
mkdir -p "$SMOKE_BIN"
SMOKE_RECORD="$WORK/smoke-command-record"
SMOKE_PROBE="$WORK/smoke-probe-record"
REAL_ENV="$(command -v env)"
printf '%s\n' \
'#!/usr/bin/env bash' \
'printf "timeout-cwd=%s\n" "$PWD" >>"$SMOKE_RECORD"' \
'while [ "$#" -gt 0 ]; do' \
' case "$1" in' \
' --signal=*|--kill-after=*|15s) shift ;;' \
' *) break ;;' \
' esac' \
'done' \
'"$@"' \
'[ -n "${SMOKE_TIMEOUT_MESSAGE:-}" ] && printf "%s\n" "$SMOKE_TIMEOUT_MESSAGE" >&2' \
'exit "${SMOKE_TIMEOUT_STATUS:-124}"' \
>"$SMOKE_BIN/timeout"
printf '%s\n' \
'#!/usr/bin/env bash' \
'printf "xvfb-invoked\n" >>"$SMOKE_RECORD"' \
'while [ "$#" -gt 0 ]; do' \
' case "$1" in' \
' -a|--server-args=*) shift ;;' \
' *) break ;;' \
' esac' \
'done' \
'exec "$@"' \
>"$SMOKE_BIN/xvfb-run"
printf '%s\n' \
'#!/usr/bin/env bash' \
'args=("$@")' \
'index=0' \
'while [ "$index" -lt "${#args[@]}" ]; do' \
' arg="${args[$index]}"' \
' case "$arg" in' \
' -u)' \
' next=$((index + 1))' \
' printf "env-unset=%s\n" "${args[$next]}" >>"$SMOKE_RECORD"' \
' index=$((index + 2))' \
' ;;' \
' HOME=*|XDG_CONFIG_HOME=*|XDG_DATA_HOME=*|XDG_CACHE_HOME=*)' \
' printf "env-assignment=%s\n" "$arg" >>"$SMOKE_RECORD"' \
' index=$((index + 1))' \
' ;;' \
' OPENHUMAN_CEF_PREWARM=*|OPENHUMAN_DISABLE_GPU=*) index=$((index + 1)) ;;' \
' *) break ;;' \
' esac' \
'done' \
'exec "$REAL_ENV" "${args[@]}"' \
>"$SMOKE_BIN/env"
chmod +x "$SMOKE_BIN/timeout" "$SMOKE_BIN/xvfb-run" "$SMOKE_BIN/env"
SMOKE_APPDIR="$WORK/smoke-appdir"
mkdir -p "$SMOKE_APPDIR"
printf '%s\n' \
'#!/usr/bin/env bash' \
'printf "apprun-cwd=%s\n" "$PWD" >>"$SMOKE_PROBE"' \
'printf "home=%s\n" "$HOME" >>"$SMOKE_PROBE"' \
'printf "config=%s\n" "$XDG_CONFIG_HOME" >>"$SMOKE_PROBE"' \
'printf "data=%s\n" "$XDG_DATA_HOME" >>"$SMOKE_PROBE"' \
'printf "cache=%s\n" "$XDG_CACHE_HOME" >>"$SMOKE_PROBE"' \
'[ -z "${GITHUB_TOKEN+x}" ] || exit 71' \
'[ -z "${GH_TOKEN+x}" ] || exit 72' \
'[ -z "${TAURI_SIGNING_PRIVATE_KEY+x}" ] || exit 73' \
'[ -z "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD+x}" ] || exit 74' \
'[ -z "${SENTRY_AUTH_TOKEN+x}" ] || exit 75' \
'[ -z "${OPENAI_API_KEY+x}" ] || exit 76' \
'printf "probe-executed\n" >>"$SMOKE_PROBE"' \
>"$SMOKE_APPDIR/AppRun"
chmod +x "$SMOKE_APPDIR/AppRun"
export SMOKE_RECORD SMOKE_PROBE REAL_ENV
SMOKE_ROOT="$WORK/smoke-runtime"
mkdir -p "$SMOKE_ROOT/foreign-cwd"
GITHUB_TOKEN='secret-github-value' \
GH_TOKEN='secret-gh-value' \
TAURI_SIGNING_PRIVATE_KEY='secret-signing-value' \
TAURI_SIGNING_PRIVATE_KEY_PASSWORD='secret-password-value' \
SENTRY_AUTH_TOKEN='secret-sentry-value' \
OPENAI_API_KEY='secret-openai-value' \
PATH="$SMOKE_BIN:$PATH" smoke_extracted_apprun \
"$SMOKE_APPDIR" \
"$SMOKE_ROOT/foreign-cwd" \
"$SMOKE_ROOT/success.log" \
|| fail "smoke_extracted_apprun did not accept timeout status 124"
grep -Fx "timeout-cwd=$SMOKE_ROOT/foreign-cwd" "$SMOKE_RECORD" >/dev/null \
|| fail "smoke did not invoke timeout from the foreign CWD"
grep -Fx "xvfb-invoked" "$SMOKE_RECORD" >/dev/null \
|| fail "smoke did not invoke xvfb-run"
for secret_name in \
GITHUB_TOKEN \
GH_TOKEN \
OPENHUMAN_CEF_NO_SANDBOX \
TAURI_SIGNING_PRIVATE_KEY \
TAURI_SIGNING_PRIVATE_KEY_PASSWORD \
SENTRY_AUTH_TOKEN \
OPENAI_API_KEY; do
grep -Fx "env-unset=$secret_name" "$SMOKE_RECORD" >/dev/null \
|| fail "smoke env did not unset $secret_name"
done
grep -Fx "env-assignment=HOME=$SMOKE_ROOT/home" "$SMOKE_RECORD" >/dev/null \
|| fail "smoke HOME was not isolated"
grep -Fx "env-assignment=XDG_CONFIG_HOME=$SMOKE_ROOT/config" "$SMOKE_RECORD" >/dev/null \
|| fail "smoke XDG_CONFIG_HOME was not isolated"
grep -Fx "env-assignment=XDG_DATA_HOME=$SMOKE_ROOT/data" "$SMOKE_RECORD" >/dev/null \
|| fail "smoke XDG_DATA_HOME was not isolated"
grep -Fx "env-assignment=XDG_CACHE_HOME=$SMOKE_ROOT/cache" "$SMOKE_RECORD" >/dev/null \
|| fail "smoke XDG_CACHE_HOME was not isolated"
grep -Fx "apprun-cwd=$SMOKE_ROOT/foreign-cwd" "$SMOKE_PROBE" >/dev/null \
|| fail "smoke AppRun did not execute from the foreign CWD"
grep -Fx "probe-executed" "$SMOKE_PROBE" >/dev/null \
|| fail "smoke AppRun probe did not execute"
if grep -F 'secret-' "$SMOKE_RECORD" "$SMOKE_PROBE" >/dev/null; then
fail "smoke command records exposed a secret value"
fi
if ( PATH="$SMOKE_BIN:$PATH" SMOKE_TIMEOUT_STATUS=0 \
smoke_extracted_apprun \
"$SMOKE_APPDIR" \
"$SMOKE_ROOT/foreign-cwd" \
"$SMOKE_ROOT/early-exit.log" ) >/dev/null 2>&1; then
fail "smoke_extracted_apprun accepted an immediate clean exit"
fi
if ( PATH="$SMOKE_BIN:$PATH" \
SMOKE_TIMEOUT_MESSAGE='libcef.so: cannot open shared object file' \
smoke_extracted_apprun \
"$SMOKE_APPDIR" \
"$SMOKE_ROOT/foreign-cwd" \
"$SMOKE_ROOT/loader-error.log" ) >/dev/null 2>&1; then
fail "smoke_extracted_apprun accepted a forbidden loader diagnostic"
fi
(
set +e
PATH="$SMOKE_BIN:$PATH" smoke_extracted_apprun \
"$SMOKE_APPDIR" \
"$SMOKE_ROOT/foreign-cwd" \
"$SMOKE_ROOT/errexit-disabled.log" \
|| exit 81
case "$-" in
*e*) exit 82 ;;
esac
) || fail "smoke_extracted_apprun changed a sourced caller's disabled errexit state"
APPARMOR_BIN="$WORK/apparmor-bin"
APPARMOR_RECORD="$WORK/apparmor-command-record"
APPARMOR_PROFILE="$WORK/openhuman-appimage-smoke.profile"
APPARMOR_PROFILE_SNAPSHOT="$WORK/openhuman-appimage-smoke.profile.loaded"
mkdir -p "$APPARMOR_BIN"
printf '%s\n' '#!/usr/bin/env bash' 'exit 0' \
>"$APPARMOR_BIN/apparmor_parser"
printf '%s\n' \
'#!/usr/bin/env bash' \
'printf "%s\n" "$*" >>"$APPARMOR_RECORD"' \
'if [ "$2" = "apparmor_parser" ] && [ "$3" = "--replace" ]; then' \
' cp "$4" "$APPARMOR_PROFILE_SNAPSHOT"' \
'fi' \
>"$APPARMOR_BIN/sudo"
chmod +x "$APPARMOR_BIN/apparmor_parser" "$APPARMOR_BIN/sudo"
export APPARMOR_RECORD APPARMOR_PROFILE_SNAPSHOT
PATH="$APPARMOR_BIN:$PATH" install_smoke_userns_profile \
"$RUNTIME_COMPLETE" "$APPARMOR_PROFILE" \
|| fail "install_smoke_userns_profile rejected the fixture AppDir"
grep -F "\"$RUNTIME_COMPLETE/shared/bin/OpenHuman\"" \
"$APPARMOR_PROFILE_SNAPSHOT" >/dev/null \
|| fail "AppArmor profile did not attach to the extracted real executable"
grep -Fx " userns," "$APPARMOR_PROFILE_SNAPSHOT" >/dev/null \
|| fail "AppArmor profile did not preserve Chromium's userns sandbox"
grep -Fx -- \
"--non-interactive apparmor_parser --replace $APPARMOR_PROFILE" \
"$APPARMOR_RECORD" >/dev/null \
|| fail "AppArmor profile was not loaded through non-interactive sudo"
PATH="$APPARMOR_BIN:$PATH" remove_smoke_userns_profile "$APPARMOR_PROFILE" \
|| fail "remove_smoke_userns_profile rejected the loaded profile"
grep -Fx -- \
"--non-interactive apparmor_parser --remove $APPARMOR_PROFILE" \
"$APPARMOR_RECORD" >/dev/null \
|| fail "AppArmor profile was not removed after the smoke"
: >"$APPARMOR_RECORD"
set +e
(
smoke_extracted_apprun() {
printf '%s\n' "smoke" >>"$APPARMOR_RECORD"
return 37
}
PATH="$APPARMOR_BIN:$PATH" smoke_extracted_apprun_with_userns \
"$RUNTIME_COMPLETE" \
"$SMOKE_ROOT/foreign-cwd" \
"$SMOKE_ROOT/apparmor-failure.log" \
"$APPARMOR_PROFILE"
)
apparmor_smoke_status=$?
set -e
[ "$apparmor_smoke_status" -eq 37 ] \
|| fail "AppArmor wrapper did not preserve smoke failure status 37"
printf '%s\n' \
"--non-interactive apparmor_parser --replace $APPARMOR_PROFILE" \
"smoke" \
"--non-interactive apparmor_parser --remove $APPARMOR_PROFILE" \
>"$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"
APPARMOR_TERM_RECORD="$WORK/apparmor-term-command-record"
APPARMOR_TERM_MARKER="$WORK/apparmor-term-smoke-started"
APPARMOR_TERM_RUNNER="$WORK/apparmor-term-runner.sh"
printf '%s\n' \
'#!/usr/bin/env bash' \
'set -euo pipefail' \
'# shellcheck source=/dev/null' \
'source "$RUNTIME_VALIDATOR"' \
'smoke_extracted_apprun() {' \
' printf "%s\n" "smoke-start" >>"$APPARMOR_RECORD"' \
' : >"$APPARMOR_TERM_MARKER"' \
' while :; do sleep 1; done' \
'}' \
'smoke_extracted_apprun_with_userns \' \
' "$RUNTIME_COMPLETE" \' \
' "$SMOKE_ROOT/foreign-cwd" \' \
' "$SMOKE_ROOT/apparmor-term.log" \' \
' "$APPARMOR_PROFILE"' \
>"$APPARMOR_TERM_RUNNER"
chmod +x "$APPARMOR_TERM_RUNNER"
export \
APPARMOR_TERM_MARKER \
RUNTIME_COMPLETE \
RUNTIME_VALIDATOR \
SMOKE_ROOT \
APPARMOR_PROFILE
APPARMOR_RECORD="$APPARMOR_TERM_RECORD" \
PATH="$APPARMOR_BIN:$PATH" \
bash "$APPARMOR_TERM_RUNNER" &
apparmor_term_pid=$!
for _ in $(seq 1 100); do
if [ -s "$APPARMOR_TERM_RECORD" ] && [ -f "$APPARMOR_TERM_MARKER" ]; then
break
fi
sleep 0.05
done
[ -s "$APPARMOR_TERM_RECORD" ] && [ -f "$APPARMOR_TERM_MARKER" ] || {
kill -TERM "$apparmor_term_pid" 2>/dev/null || true
fail "AppArmor TERM fixture did not reach the smoke"
}
kill -TERM "$apparmor_term_pid"
set +e
wait "$apparmor_term_pid"
apparmor_term_status=$?
set -e
[ "$apparmor_term_status" -eq 143 ] \
|| fail "AppArmor TERM fixture exited $apparmor_term_status instead of 143"
printf '%s\n' \
"--non-interactive apparmor_parser --replace $APPARMOR_PROFILE" \
"smoke-start" \
"--non-interactive apparmor_parser --remove $APPARMOR_PROFILE" \
>"$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"
echo "[test-rpaths] ok: CI smoke grants userns only to the extracted executable"
assert_runtime_layout_rejected missing-anylinux \
"missing anylinux.so" remove_anylinux
assert_runtime_layout_rejected missing-libxdo \
"missing libxdo.so.*" remove_libxdo
assert_runtime_layout_rejected missing-libcef \
"missing libcef.so" remove_libcef
assert_runtime_layout_rejected missing-real-app \
"shared/bin/OpenHuman is not an executable ELF" remove_real_app
assert_runtime_layout_rejected text-real-app \
"shared/bin/OpenHuman is not an executable ELF" replace_real_app_with_text
assert_runtime_layout_rejected mismatched-apprun \
"AppRun does not match sharun" replace_apprun
assert_runtime_layout_rejected mismatched-bin-launcher \
"bin/OpenHuman does not match sharun" replace_bin_launcher
assert_runtime_layout_rejected runner-rpath \
"/home/runner/work/openhuman/openhuman/shared/lib" inject_runner_rpath
assert_runtime_layout_rejected actions-rpath \
"/__w/openhuman/openhuman/shared/lib" inject_actions_rpath
RUNTIME_NEEDED="$WORK/runtime-needed"
make_runtime_appdir "$RUNTIME_NEEDED"
if ( APPIMAGE_EXPECTED_NEEDED="libxdo.so.3" \
validate_extracted_appdir "$RUNTIME_NEEDED" ) \
>"$RUNTIME_NEEDED/missing-xdo.log" 2>&1; then
fail "validate_extracted_appdir accepted a missing libxdo NEEDED entry"
fi
grep -F "missing NEEDED entry 'libxdo.so.3'" \
"$RUNTIME_NEEDED/missing-xdo.log" >/dev/null \
|| fail "missing libxdo NEEDED diagnostic was not specific"
patchelf --add-needed libxdo.so.3 "$RUNTIME_NEEDED/shared/bin/OpenHuman"
if ( APPIMAGE_EXPECTED_NEEDED="libcef.so" \
validate_extracted_appdir "$RUNTIME_NEEDED" ) \
>"$RUNTIME_NEEDED/missing-cef.log" 2>&1; then
fail "validate_extracted_appdir accepted a missing libcef NEEDED entry"
fi
grep -F "missing NEEDED entry 'libcef.so'" \
"$RUNTIME_NEEDED/missing-cef.log" >/dev/null \
|| fail "missing libcef NEEDED diagnostic was not specific"
patchelf --add-needed libcef.so "$RUNTIME_NEEDED/shared/bin/OpenHuman"
APPIMAGE_EXPECTED_NEEDED="libxdo.so.3 libcef.so" \
validate_extracted_appdir "$RUNTIME_NEEDED" \
|| fail "validate_extracted_appdir rejected complete NEEDED entries"
VALIDATOR_RECORD="$WORK/runtime-validator-record"
runtime_validator_stub() {
[ -f "$1" ] || return 10
[ "${#MODIFIED_PATHS[@]}" -eq 0 ] || return 11
printf '%s\n' "$1" >"$VALIDATOR_RECORD"
}
REBUILT_FIXTURE="$WORK/rebuilt.AppImage"
: >"$REBUILT_FIXTURE"
MODIFIED_PATHS=()
APPIMAGE_RUNTIME_VALIDATOR=runtime_validator_stub \
validate_rebuilt_appimage "$REBUILT_FIXTURE" \
|| fail "validate_rebuilt_appimage did not forward to its configured command"
[ "$(cat "$VALIDATOR_RECORD")" = "$REBUILT_FIXTURE" ] \
|| fail "validate_rebuilt_appimage forwarded the wrong artifact path"
runtime_validator_failure_stub() { return 37; }
if APPIMAGE_RUNTIME_VALIDATOR=runtime_validator_failure_stub \
validate_rebuilt_appimage "$REBUILT_FIXTURE"; then
fail "conditional validate_rebuilt_appimage suppressed validator failure"
fi
CLEANUP_IMAGE="$WORK/cleanup-invalid.AppImage"
printf '%s\n' \
'#!/usr/bin/env bash' \
'[ "$1" = "--appimage-extract" ] || exit 9' \
'mkdir -p squashfs-root/shared/lib' \
>"$CLEANUP_IMAGE"
chmod +x "$CLEANUP_IMAGE"
CLEANUP_WORKDIR="$WORK/cleanup-invalid-workdir"
CLEANUP_LOG="$WORK/cleanup-invalid.log"
set +e
TARGET="$TARGET" \
CLEANUP_IMAGE="$CLEANUP_IMAGE" \
CLEANUP_WORKDIR="$CLEANUP_WORKDIR" \
bash -c '
set -euo pipefail
# shellcheck source=/dev/null
source "$TARGET"
mktemp() {
if [ "${1:-}" = "-d" ]; then
mkdir -p "$CLEANUP_WORKDIR"
printf "%s\n" "$CLEANUP_WORKDIR"
else
command mktemp "$@"
fi
}
ensure_sharun_interpreter() { return 1; }
rewrite_sharun_lib_path() { return 1; }
patch_apprun_sharun_cwd() { return 1; }
sanitize_elf_rpaths() { return 1; }
validate_sharun_lib_path() {
echo "intentional lib.path validation failure" >&2
return 47
}
validate_appimage_required_libs() { return 0; }
validate_rebuilt_appimage() { return 0; }
strip_one_appimage "$CLEANUP_IMAGE"
' >"$CLEANUP_LOG" 2>&1
cleanup_status=$?
set -e
[ "$cleanup_status" -ne 0 ] \
|| fail "strip_one_appimage accepted a failing lib.path validation"
grep -F "intentional lib.path validation failure" "$CLEANUP_LOG" >/dev/null \
|| fail "strip_one_appimage hid the original lib.path validation error"
[ ! -e "$CLEANUP_WORKDIR" ] \
|| fail "strip_one_appimage leaked its workdir after lib.path validation failed"
NOOP_IMAGE="$WORK/noop-final.AppImage"
printf '%s\n' \
'#!/usr/bin/env bash' \
'[ "$1" = "--appimage-extract" ] || exit 9' \
'mkdir -p squashfs-root' \
>"$NOOP_IMAGE"
chmod +x "$NOOP_IMAGE"
NOOP_IMAGE_BEFORE="$(sha256sum "$NOOP_IMAGE" | awk '{print $1}')"
NOOP_VALIDATOR_RECORD="$WORK/noop-validator-record"
noop_runtime_validator_stub() {
[ -f "$1" ] || return 20
[ "${#MODIFIED_PATHS[@]}" -eq 0 ] || return 21
printf '%s\n' "$1" >>"$NOOP_VALIDATOR_RECORD"
}
MODIFIED_PATHS=()
APPIMAGE_RUNTIME_VALIDATOR=noop_runtime_validator_stub \
strip_one_appimage "$NOOP_IMAGE" \
|| fail "strip_one_appimage rejected a valid no-change final artifact"
[ "$(wc -l <"$NOOP_VALIDATOR_RECORD" | tr -d ' ')" -eq 1 ] \
|| fail "no-change AppImage was not validated exactly once"
[ "$(cat "$NOOP_VALIDATOR_RECORD")" = "$(realpath "$NOOP_IMAGE")" ] \
|| fail "no-change validation did not inspect the original final AppImage"
[ "${#MODIFIED_PATHS[@]}" -eq 0 ] \
|| fail "no-change AppImage was incorrectly registered for re-signing"
[ "$NOOP_IMAGE_BEFORE" = "$(sha256sum "$NOOP_IMAGE" | awk '{print $1}')" ] \
|| fail "no-change AppImage bytes were unexpectedly replaced"
mv_line="$(grep -nF 'mv "$rebuilt" "$original"' "$TARGET" | cut -d: -f1)"
validate_line="$(
grep -nF 'validate_rebuilt_appimage "$original"' "$TARGET" \
| tail -n 1 \
| cut -d: -f1
)"
modified_line="$(grep -nF 'MODIFIED_PATHS+=("$original")' "$TARGET" | cut -d: -f1)"
[ -n "$mv_line" ] && [ -n "$validate_line" ] && [ -n "$modified_line" ] \
|| fail "post-repack validation ordering statements are missing"
[ "$mv_line" -lt "$validate_line" ] && [ "$validate_line" -lt "$modified_line" ] \
|| fail "final artifact validation does not run after mv and before signing registration"
echo "[test-rpaths] ok: final runtime layout and pre-signing handoff are fail-closed"
# --- Case 1: sanitize_elf_rpaths strips a CI build-machine RPATH ------------
APPDIR="$WORK/squashfs-root"
mkdir -p "$APPDIR/usr/lib" "$APPDIR/shared/lib"
@@ -130,20 +907,28 @@ chmod +x "$GUARD_DIR/sharun"
# guard's early return would make this case vacuously pass.
uses_sharun_launcher "$GUARD_DIR" || fail "fixture did not register as sharun launcher"
# 2a — libxdo absent → must exit non-zero.
# 2a — the sharun preload library absent → must exit non-zero.
if ( validate_appimage_required_libs "$GUARD_DIR" ) 2>/dev/null; then
fail "validate_appimage_required_libs passed despite missing anylinux.so"
fi
echo "[test-rpaths] ok: guard fails when anylinux.so is absent"
: > "$GUARD_DIR/shared/lib/anylinux.so"
# 2b — libxdo absent → must exit non-zero.
if ( validate_appimage_required_libs "$GUARD_DIR" ) 2>/dev/null; then
fail "validate_appimage_required_libs passed despite missing libxdo.so.*"
fi
echo "[test-rpaths] ok: guard fails when libxdo.so.* is absent"
# 2b — libxdo present but libcef absent → must still fail.
# 2c — libxdo present but libcef absent → must still fail.
: > "$GUARD_DIR/shared/lib/libxdo.so.3"
if ( validate_appimage_required_libs "$GUARD_DIR" ) 2>/dev/null; then
fail "validate_appimage_required_libs passed despite missing libcef.so"
fi
echo "[test-rpaths] ok: guard fails when libcef.so is absent"
# 2cboth required runtime libraries present → must pass.
# 2dall required runtime libraries present → must pass.
mkdir -p "$GUARD_DIR/usr/lib"
: > "$GUARD_DIR/shared/lib/libxdo.so.3"
: > "$GUARD_DIR/usr/lib/libcef.so"
+368
View File
@@ -0,0 +1,368 @@
#!/usr/bin/env bash
# Validate the final, repacked AppImage bytes before release signing.
#
# This file is sourceable for fixture tests. When executed directly it extracts
# exactly one AppImage from a foreign working directory, validates the released
# sharun layout, and optionally performs a bounded Xvfb startup smoke.
#
# Usage: validate-appimage-runtime.sh <final.AppImage>
#
# 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
set -euo pipefail
RUNTIME_VALIDATOR_SCRIPT_DIR="$(
cd "$(dirname "${BASH_SOURCE[0]}")" && pwd
)"
# shellcheck source=strip-appimage-graphics-libs.sh
source "$RUNTIME_VALIDATOR_SCRIPT_DIR/strip-appimage-graphics-libs.sh"
runtime_validation_error() {
echo "[appimage-runtime] ERROR: $*" >&2
return 1
}
validate_extracted_appdir() {
local appdir="$1"
local lib_path="$appdir/shared/lib/lib.path"
echo "[appimage-runtime] Validating extracted AppDir: $appdir"
if [ -f "$lib_path" ]; then
echo "[appimage-runtime] shared/lib/lib.path:"
sed 's/^/[appimage-runtime] /' "$lib_path"
else
echo "[appimage-runtime] shared/lib/lib.path: <missing>"
fi
if ! uses_sharun_launcher "$appdir"; then
runtime_validation_error "released AppDir does not use a detectable sharun launcher"
return 1
fi
local executable
for executable in \
"$appdir/AppRun" \
"$appdir/sharun" \
"$appdir/bin/OpenHuman" \
"$appdir/shared/bin/OpenHuman"; do
if ! is_executable_elf "$executable"; then
runtime_validation_error "${executable#"$appdir"/} is not an executable ELF"
return 1
fi
done
local alias
for alias in "$appdir/AppRun" "$appdir/bin/OpenHuman"; do
if [ "$alias" -ef "$appdir/sharun" ] || cmp -s "$alias" "$appdir/sharun"; then
continue
fi
runtime_validation_error "${alias#"$appdir"/} does not match sharun"
return 1
done
validate_sharun_lib_path "$appdir" || return 1
validate_appimage_required_libs "$appdir" || return 1
local real_app="$appdir/shared/bin/OpenHuman"
local needed
if ! needed="$(patchelf --print-needed "$real_app")"; then
runtime_validation_error \
"could not read NEEDED entries from shared/bin/OpenHuman"
return 1
fi
echo "[appimage-runtime] shared/bin/OpenHuman NEEDED entries:"
if [ -n "$needed" ]; then
printf '%s\n' "$needed" | sed 's/^/[appimage-runtime] /'
else
echo "[appimage-runtime] <none>"
fi
local expected_needed="${APPIMAGE_EXPECTED_NEEDED-libxdo.so.3 libcef.so}"
local needed_name
for needed_name in $expected_needed; do
if ! printf '%s\n' "$needed" | grep -Fx "$needed_name" >/dev/null; then
runtime_validation_error "shared/bin/OpenHuman is missing NEEDED entry '$needed_name'"
return 1
fi
done
local root elf rpath
for root in \
"$appdir/shared/bin" \
"$appdir/shared/lib" \
"$appdir/bin" \
"$appdir/lib" \
"$appdir/usr/bin" \
"$appdir/usr/lib"; do
[ -d "$root" ] || continue
while IFS= read -r -d '' elf; do
is_elf "$elf" || continue
rpath="$(patchelf --print-rpath "$elf" 2>/dev/null || true)"
case "$rpath" in
*"/home/runner/"*|*"/__w/"*)
runtime_validation_error \
"${elf#"$appdir"/} retains forbidden build-runner RPATH '$rpath'"
return 1
;;
esac
done < <(find "$root" -type f -print0)
done
}
smoke_extracted_apprun() {
local appdir="$1"
local foreign_cwd="$2"
local log_file="$3"
command -v timeout >/dev/null 2>&1 \
|| { runtime_validation_error "timeout is required for the AppImage smoke"; return 1; }
command -v xvfb-run >/dev/null 2>&1 \
|| { runtime_validation_error "xvfb-run is required for the AppImage smoke"; return 1; }
echo "[appimage-runtime] Smoking AppRun from AppDir: $appdir"
echo "[appimage-runtime] Smoke caller CWD: $foreign_cwd"
local temp_root
temp_root="$(dirname "$foreign_cwd")"
local smoke_home="$temp_root/home"
local smoke_config="$temp_root/config"
local smoke_data="$temp_root/data"
local smoke_cache="$temp_root/cache"
mkdir -p \
"$foreign_cwd" \
"$smoke_home" \
"$smoke_config" \
"$smoke_data" \
"$smoke_cache"
local -a unset_args=()
local secret_name
for secret_name in \
GITHUB_TOKEN \
GH_TOKEN \
OPENHUMAN_CEF_NO_SANDBOX \
TAURI_SIGNING_PRIVATE_KEY \
TAURI_SIGNING_PRIVATE_KEY_PASSWORD \
SENTRY_AUTH_TOKEN; do
unset_args+=(-u "$secret_name")
done
while IFS= read -r secret_name; do
[ -n "$secret_name" ] && unset_args+=(-u "$secret_name")
done < <(compgen -v OPENAI_ || true)
local status
if (
cd "$foreign_cwd"
timeout --signal=TERM --kill-after=5s 15s \
xvfb-run -a --server-args="-screen 0 1280x960x24" \
env "${unset_args[@]}" \
HOME="$smoke_home" \
XDG_CONFIG_HOME="$smoke_config" \
XDG_DATA_HOME="$smoke_data" \
XDG_CACHE_HOME="$smoke_cache" \
OPENHUMAN_CEF_PREWARM=0 \
OPENHUMAN_DISABLE_GPU=1 \
"$appdir/AppRun"
) >"$log_file" 2>&1; then
status=0
else
status=$?
fi
local forbidden=0
if grep -Eiq \
'anylinux\.so.*cannot be preloaded|cannot be preloaded.*anylinux\.so' \
"$log_file"; then
forbidden=1
fi
if grep -Eiq \
'libxdo\.so\.3.*cannot open shared object file|cannot open shared object file.*libxdo\.so\.3' \
"$log_file"; then
forbidden=1
fi
if grep -Eiq \
'libcef\.so.*cannot open shared object file|cannot open shared object file.*libcef\.so' \
"$log_file"; then
forbidden=1
fi
if grep -Eiq 'error while loading shared libraries' "$log_file"; then
forbidden=1
fi
# The desktop process is expected to remain alive until timeout ends the
# startup window. An earlier clean exit is a failure as well as loader output.
if [ "$forbidden" -ne 0 ] || [ "$status" -ne 124 ]; then
echo "[appimage-runtime] AppImage startup smoke failed (status $status):" >&2
sed 's/^/[appimage-runtime] /' "$log_file" >&2
return 1
fi
grep -Ei 'loader|loading|cef|startup|started|ready' "$log_file" \
| sed 's/^/[appimage-runtime] /' \
|| true
echo "[appimage-runtime] Application remained alive for the 15-second startup window"
}
install_smoke_userns_profile() {
local appdir="$1"
local profile_file="$2"
local executable="$appdir/shared/bin/OpenHuman"
[ -x "$executable" ] \
|| { runtime_validation_error "AppArmor target is not executable: $executable"; return 1; }
command -v sudo >/dev/null 2>&1 \
|| { runtime_validation_error "sudo is required for the AppImage smoke AppArmor profile"; return 1; }
command -v apparmor_parser >/dev/null 2>&1 \
|| { runtime_validation_error "apparmor_parser is required for the AppImage smoke AppArmor profile"; return 1; }
local profile_name="openhuman_appimage_runtime_smoke_$$"
printf '%s\n' \
'abi <abi/4.0>,' \
'include <tunables/global>' \
'' \
"profile $profile_name \"$executable\" flags=(unconfined) {" \
' userns,' \
'}' \
>"$profile_file"
sudo --non-interactive apparmor_parser --replace "$profile_file" >/dev/null \
|| { runtime_validation_error "could not load the AppImage smoke AppArmor profile"; return 1; }
echo "[appimage-runtime] Loaded temporary AppArmor userns profile for: $executable"
}
remove_smoke_userns_profile() {
local profile_file="$1"
sudo --non-interactive apparmor_parser --remove "$profile_file" >/dev/null \
|| { runtime_validation_error "could not remove the AppImage smoke AppArmor profile"; return 1; }
echo "[appimage-runtime] Removed temporary AppArmor userns profile: $profile_file"
}
smoke_extracted_apprun_with_userns() {
local appdir="$1"
local foreign_cwd="$2"
local log_file="$3"
local profile_file="$4"
local profile_loaded=0
local previous_hup_trap previous_int_trap previous_term_trap
previous_hup_trap="$(trap -p HUP)"
previous_int_trap="$(trap -p INT)"
previous_term_trap="$(trap -p TERM)"
restore_smoke_signal_traps() {
trap - HUP INT TERM
[ -z "$previous_hup_trap" ] || eval "$previous_hup_trap"
[ -z "$previous_int_trap" ] || eval "$previous_int_trap"
[ -z "$previous_term_trap" ] || eval "$previous_term_trap"
}
cleanup_smoke_userns_profile() {
if [ "$profile_loaded" -eq 1 ]; then
profile_loaded=0
remove_smoke_userns_profile "$profile_file" \
|| echo "[appimage-runtime] ERROR: AppArmor cleanup failed after smoke interruption" >&2
fi
}
interrupt_smoke_with_userns() {
local exit_status="$1"
trap - HUP INT TERM
cleanup_smoke_userns_profile
exit "$exit_status"
}
trap 'interrupt_smoke_with_userns 129' HUP
trap 'interrupt_smoke_with_userns 130' INT
trap 'interrupt_smoke_with_userns 143' TERM
if ! install_smoke_userns_profile "$appdir" "$profile_file"; then
restore_smoke_signal_traps
return 1
fi
profile_loaded=1
local smoke_status
if smoke_extracted_apprun "$appdir" "$foreign_cwd" "$log_file"; then
smoke_status=0
else
smoke_status=$?
fi
profile_loaded=0
local remove_status
if remove_smoke_userns_profile "$profile_file"; then
remove_status=0
else
remove_status=$?
fi
restore_smoke_signal_traps
# Preserve the original smoke failure even if profile cleanup also fails.
[ "$smoke_status" -eq 0 ] || return "$smoke_status"
return "$remove_status"
}
validate_final_appimage() (
local image="$1"
if ! image="$(realpath "$image")"; then
runtime_validation_error "could not resolve AppImage path: $1"
return 1
fi
if [ ! -f "$image" ] || [ ! -x "$image" ]; then
runtime_validation_error "AppImage must be an executable regular file: $image"
return 1
fi
command -v patchelf >/dev/null 2>&1 \
|| { runtime_validation_error "patchelf is required for final AppImage validation"; return 1; }
local temp_root
temp_root="$(mktemp -d)"
trap 'rm -rf -- "$temp_root"' EXIT
local foreign_cwd="$temp_root/foreign-cwd"
local extraction_log="$temp_root/extraction.log"
local smoke_log="$temp_root/smoke.log"
mkdir -p "$foreign_cwd"
if ! (
cd "$foreign_cwd"
"$image" --appimage-extract
) >"$extraction_log" 2>&1; then
echo "[appimage-runtime] AppImage extraction failed:" >&2
sed 's/^/[appimage-runtime] /' "$extraction_log" >&2
return 1
fi
local appdir="$foreign_cwd/squashfs-root"
if [ ! -d "$appdir" ]; then
runtime_validation_error "extraction did not create $appdir"
return 1
fi
validate_extracted_appdir "$appdir" || return 1
if [ "${APPIMAGE_RUNTIME_SMOKE:-0}" = "1" ]; then
if [ "${APPIMAGE_RUNTIME_APPARMOR_USERNS:-0}" = "1" ]; then
smoke_extracted_apprun_with_userns \
"$appdir" \
"$foreign_cwd" \
"$smoke_log" \
"$temp_root/openhuman-appimage-smoke.profile" \
|| return 1
else
smoke_extracted_apprun "$appdir" "$foreign_cwd" "$smoke_log" || return 1
fi
else
echo "[appimage-runtime] Static validation complete; executable smoke disabled for this architecture"
fi
)
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
[ "$#" -eq 1 ] || {
echo "Usage: $0 <final.AppImage>" >&2
exit 2
}
APPIMAGE_EXPECTED_NEEDED="libxdo.so.3 libcef.so" \
validate_final_appimage "$1"
fi