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
+16 -9
View File
@@ -172,6 +172,8 @@ jobs:
rustup target add aarch64-apple-darwin x86_64-apple-darwin
- name: Install Tauri dependencies (ubuntu only)
if: startsWith(matrix.settings.platform, 'ubuntu-')
env:
MATRIX_TARGET: ${{ matrix.settings.target }}
run: |
sudo apt-get update
sudo apt-get install -y \
@@ -180,7 +182,12 @@ jobs:
libevdev-dev libssl-dev libclang-dev desktop-file-utils \
libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
libgbm1 libpango-1.0-0 libcairo2 libatspi2.0-0 libxshmfence1 libu2f-udev
libgbm1 libpango-1.0-0 libcairo2 libatspi2.0-0 libxshmfence1 libu2f-udev \
xvfb
if [ "$MATRIX_TARGET" = "x86_64-unknown-linux-gnu" ] \
&& ! command -v apparmor_parser >/dev/null 2>&1; then
sudo apt-get install -y apparmor
fi
# NOTE: The post-build dump lives further down (after `cargo tauri build`)
# so the binary actually exists when ldd runs. Running it here pre-build
# silently no-op'd and gave us no signal during the
@@ -528,19 +535,19 @@ jobs:
done
# Strip host graphics libraries (Mesa/libdrm/libva/libgbm) that lib4bin
# ldd-walks into the AppImage. Those drivers must come from the user's
# host system — even on a current Ubuntu runner the bundled Mesa can
# fail to initialize newer GPUs (RDNA3, Intel Arc, Lovelace) and
# crashes CEF on launch because the bundled libs shadow the host's
# working ones on LD_LIBRARY_PATH. CEF's own SwiftShader subdir is
# untouched. Re-signs the AppImage + updater tarball when signing
# is enabled.
- name: Strip host graphics libs from AppImage
# ldd-walks into the AppImage. Final-artifact validation runs before
# re-signing: amd64 also gets a bounded Xvfb launch from a foreign CWD,
# while arm64 receives static validation in this change. CEF's own
# SwiftShader subdir is untouched, and updater archives are rebuilt only
# after the validated image is final.
- name: Strip host graphics libs and validate final AppImage
if: startsWith(matrix.settings.platform, 'ubuntu-')
shell: bash
env:
MATRIX_TARGET: ${{ matrix.settings.target }}
PROFILE: ${{ inputs.build_profile }}
APPIMAGE_RUNTIME_SMOKE: ${{ matrix.settings.target == 'x86_64-unknown-linux-gnu' && '1' || '0' }}
APPIMAGE_RUNTIME_APPARMOR_USERNS: ${{ matrix.settings.target == 'x86_64-unknown-linux-gnu' && '1' || '0' }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY || secrets.UPDATER_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD || secrets.UPDATER_PRIVATE_KEY_PASSWORD }}
run: |
@@ -0,0 +1,943 @@
# AppImage sharun library-path repair implementation plan
> Approved design:
> `docs/specs/2026-07-24-appimage-sharun-lib-path-design.md` at
> `baae2b02c73e48826e156a111b7e8aff82f214b9`.
## Goal
Repair the Linux AppImage postprocessor so sharun's generated
`shared/lib/lib.path` contains only AppDir-anchored `+` entries, reject malformed
paths before release, and prove that the final repacked amd64 AppImage reaches
normal application startup when invoked from outside its extracted AppDir.
The implementation changes only the AppImage release path:
- `scripts/release/strip-appimage-graphics-libs.sh`
- `scripts/release/test-strip-appimage-rpaths.sh`
- a new final-artifact validator at
`scripts/release/validate-appimage-runtime.sh`
- the Linux portion of `.github/workflows/build-desktop.yml`
The existing graphics-library removal, interpreter replacement, RPATH repair,
artifact signing, updater tarball rebuilding, x86_64/aarch64 build matrix, and
non-Linux workflow branches remain in place.
## Scope guardrails
This plan intentionally does **not** implement the broader generated triage plan
currently embedded in issue #5037:
- The unbundled `openhuman-core`/AUR failure remains a downstream packaging and
optional-`enigo` follow-up. Do not change `Cargo.toml`, feature gates, voice
input, computer tools, `INSTALL.md`, or AUR metadata in this work.
- Do not change bundled `libssl.so.3`/`libcrypto.so.3`, NSS exclusions, or claim
to fix the later OpenSSL symbol-skew/segfault behavior in #3716. The smoke
catches an immediate loader failure, not every later CEF/OpenSSL crash.
- Do not pin the mutable `quick-sharun.sh` or uruntime downloads in the vendored
Tauri bundler. Supply-chain pinning is a separate follow-up.
- Do not add an Arch/Fedora/Tumbleweed matrix or make
`.github/workflows/pr-quality.yml`'s advisory AppImage guard blocking. The
release-builder smoke is the selected acceptance test.
- Do not replace sharun with a custom `LD_LIBRARY_PATH` wrapper.
## Confirmed sharun contract
Pin the tests and implementation to the actual sharun grammar:
- `shared/lib/lib.path` is one entry per line.
- `+` represents the directory containing `lib.path`, which is the extracted
AppDir's canonical `shared/lib` directory.
- `+/subdir` represents a descendant of that directory.
- sharun trims the complete file, replaces newlines with `:`, and then replaces
every `+` with the absolute `shared/lib` path.
- `+` is therefore a marker inside an entry, **not** an entry delimiter.
The current `tr '+:' '\n\n'` parser destroys this marker and must be removed.
Tests must use the accepted textual forms `+` and `+/subdir`, with newline
separation. They must not introduce an invented plus- or colon-separated input
format.
## Sequencing decision
`strip-appimage-graphics-libs.sh` currently performs four phases in one command:
1. extract and postprocess an AppDir;
2. repack and replace the final `.AppImage`;
3. re-sign the `.AppImage`;
4. rebuild and re-sign its updater tarball.
The final-artifact validator must run after phase 2 but before phase 3. Add the
validator call immediately after the rebuilt file is moved over the original
and before the image is appended to `MODIFIED_PATHS`. This satisfies both
requirements that the inspected bytes are the uploaded bytes and that a bad
launcher fails before signing. It also avoids a defer/finalize mode that could
accidentally validate one image and then repack another.
Static validation runs for every Linux AppImage. The executable smoke is
enabled only for `x86_64-unknown-linux-gnu` in this issue because that is the
reported architecture and acceptance target. The helper remains
architecture-neutral: a later change can enable the same smoke on arm64 by
setting the same environment flag on an executable arm64 runner.
---
## Task 1: Replace the destructive parser with sharun-native normalization
**Files:**
- Modify: `scripts/release/test-strip-appimage-rpaths.sh`
- Modify: `scripts/release/strip-appimage-graphics-libs.sh`
### Step 1.1: Add failing normalization fixtures
Extend `test-strip-appimage-rpaths.sh` after the host-ELF fixture is established
and the target script is sourced. Add a reusable released-style AppDir builder:
```bash
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"
}
```
Use separate fixture directories so one failure cannot contaminate the next.
Add these assertions:
1. An input with one entry
`"$WORK/squashfs-root/shared/lib"` rewrites to exactly `+`.
2. An input rooted in the bundler staging layout
`"$WORK/appimage_deb/data/usr/lib"` rewrites to exactly `+`.
3. AppDir descendants map as follows:
- `"$WORK/squashfs-root/shared/lib/plugins"` becomes `+/plugins`;
- `"$WORK/appimage_deb/data/usr/lib/plugins"` becomes `+/plugins`.
4. A file containing these newline-delimited entries:
```text
+
+/plugins
+
/opt/unrelated/lib
```
normalizes to exactly:
```text
+
+/plugins
```
Stable first-seen order is preserved and the unrelated host path is
excluded.
5. Running `rewrite_sharun_lib_path` a second time leaves the bytes unchanged
and returns the no-change status expected by `strip_one_appimage`.
6. An input containing only `/opt/unrelated/lib` fails normalization and is not
replaced with `shared/lib`.
Capture expected failures in subshells because the release helpers are allowed
to terminate fail-closed:
```bash
if ( rewrite_sharun_lib_path "$fixture" ) 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"
```
Run the focused test on Linux:
```bash
bash scripts/release/test-strip-appimage-rpaths.sh
```
Expected RED: at least the canonical-marker case fails because the current
implementation splits on `+` and writes `shared/lib`; the no-valid-root case
also fails because the current fallback silently writes `shared/lib`.
On macOS, first run:
```bash
bash scripts/release/test-strip-appimage-rpaths.sh
```
Expected local result: a clean `SKIP` when `patchelf` or a host ELF is
unavailable. Do not treat that skip as execution evidence; the RED/GREEN result
for this task must come from the existing Ubuntu PR job or another Linux host.
### Step 1.2: Implement newline-based normalization
Refactor `rewrite_sharun_lib_path` without changing its public signature:
```bash
rewrite_sharun_lib_path() {
local appdir="$1"
# Return 1 for non-sharun/no-file/no-change.
# Return 0 after writing a changed valid file.
# Exit non-zero with an ERROR for malformed input from which no valid
# AppDir-rooted entry can be derived.
}
```
Implement the following exact algorithm:
1. Keep the `uses_sharun_launcher` and non-empty-file guards.
2. Read with `while IFS= read -r entry || [ -n "$entry" ]`; do not use `tr`,
word splitting, `eval`, or colon splitting.
3. Reject empty interior lines, carriage returns, embedded `:`, and surrounding
whitespace as malformed rather than letting the loader interpret them
differently from the validator.
4. Normalize each entry:
- `+` stays `+`;
- `+/suffix` stays `+/suffix` after the descendant checks in Task 2;
- an absolute path containing `/squashfs-root/shared/lib` maps that root to
`+` and its suffix to `+/suffix`;
- an absolute path containing `/appimage_deb/data/usr/lib` or `/data/usr/lib`
maps that root to `+` and its suffix to `+/suffix`;
- another absolute path is dropped;
- a bare relative path is malformed and causes normalization to fail.
5. For derived suffixes, remove one leading slash and no other content. A root
with an empty suffix becomes `+`.
6. Deduplicate using exact whole-entry equality while preserving first-seen
order. Use an indexed Bash array and a small equality loop so the script
remains syntax-compatible with macOS Bash 3.2; do not introduce associative
arrays.
7. If no valid AppDir-rooted entries remain, print:
```text
[strip-libs] ERROR: shared/lib/lib.path contains no valid AppDir library entries
```
and return non-zero without modifying the file.
8. Join entries with newline characters and write through a temporary file in
the same directory followed by `mv`. Preserve the final format as one entry
per line; a trailing newline is acceptable but must be deterministic.
9. Compare the normalized temporary file with the original using `cmp -s`.
Delete the temporary and return `1` on an idempotent no-op. On change, move
it into place, log the normalized entries, and return `0`.
Remove the early grep that only invokes rewriting when `/home/runner` or `/__w`
is present. Valid marker files still need the idempotency path, and malformed
bare-relative files must not bypass normalization.
### Step 1.3: Run the GREEN checks
Linux:
```bash
bash -n scripts/release/strip-appimage-graphics-libs.sh
bash -n scripts/release/test-strip-appimage-rpaths.sh
bash scripts/release/test-strip-appimage-rpaths.sh
```
Expected GREEN: all old RPATH/required-library tests and all new normalization
cases pass.
macOS:
```bash
bash -n scripts/release/strip-appimage-graphics-libs.sh
bash -n scripts/release/test-strip-appimage-rpaths.sh
bash scripts/release/test-strip-appimage-rpaths.sh
```
Expected: both syntax checks pass and the execution test skips cleanly if Linux
ELF tooling is absent.
### Step 1.4: Commit the normalization
```bash
atomic-commit "fix(appimage): emit sharun-native library paths" -- \
scripts/release/test-strip-appimage-rpaths.sh \
scripts/release/strip-appimage-graphics-libs.sh
```
---
## Task 2: Make `lib.path` validation fail closed and cover the shipped ELF launcher
**Files:**
- Modify: `scripts/release/test-strip-appimage-rpaths.sh`
- Modify: `scripts/release/strip-appimage-graphics-libs.sh`
### Step 2.1: Add failing validator and launcher-layout cases
Reuse `make_sharun_appdir` and add a table-driven helper:
```bash
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 "$expected_entry" "$fixture/error.log" >/dev/null \
|| fail "$label error did not identify offending entry '$expected_entry'"
}
```
Add rejection cases for:
- `shared/lib` (bare relative);
- `/home/runner/work/openhuman/squashfs-root/shared/lib` (CI absolute);
- `/__w/openhuman/openhuman/appimage_deb/data/usr/lib` (CI absolute);
- `+/../outside` (parent traversal);
- `+/plugins/../../outside` (nested traversal);
- `+/missing` (marker-relative directory absent from the fixture);
- `+/plugins:shared/lib` (embedded loader separator);
- ` +` and `+ ` (whitespace changes loader semantics).
Each error assertion must match the complete offending entry in the diagnostic,
not only a generic error category.
Add accepted cases:
- `+`;
- `+` followed by `+/plugins` on a new line;
- a normalized file passed twice, proving validation is non-mutating.
Add explicit released-layout assertions:
```bash
[ "$fixture/AppRun" -ef "$fixture/sharun" ] \
|| fail "AppRun fixture is not hard-linked to sharun"
[ "$fixture/bin/OpenHuman" -ef "$fixture/sharun" ] \
|| fail "bin/OpenHuman fixture is not hard-linked to sharun"
is_executable_elf "$fixture/AppRun" \
|| fail "released-style AppRun fixture is not ELF"
uses_sharun_launcher "$fixture" \
|| fail "released-style hard-linked ELF launcher was not detected"
```
Write `+` to its `lib.path`, validate it, change to an unrelated directory, and
model sharun's pinned expansion:
```bash
caller="$WORK/caller"
mkdir -p "$caller"
(
cd "$caller"
expanded="$(
sed "s|+|$fixture/shared/lib|g" "$fixture/shared/lib/lib.path" |
paste -sd ':' -
)"
[ "$expanded" = "$fixture/shared/lib" ] \
|| fail "sharun marker did not expand beneath the fixture AppDir"
)
```
Also assert that `patch_apprun_sharun_cwd "$fixture"` returns its no-change
status and does not mutate the ELF inode. This pins the compatibility helper as
shell-wrapper-only hardening, not the fix for released ELF AppRuns.
Run on Linux:
```bash
bash scripts/release/test-strip-appimage-rpaths.sh
```
Expected RED: the current validator accepts bare relatives, traversal, missing
directories, and an ELF AppRun without any meaningful library-path checks.
### Step 2.2: Implement exact fail-closed validation
Keep the `validate_sharun_lib_path "$appdir"` interface. Parse the file by
newline using the same helper/loop as normalization so rewrite and validation
cannot disagree.
For every non-empty entry:
1. Accept only `+` or a string beginning `+/`.
2. Reject `:`, carriage returns, leading/trailing whitespace, absolute paths,
and bare relative paths.
3. Split the marker-relative suffix on `/` and reject empty, `.`, or `..`
components. This rejects traversal before filesystem canonicalization.
4. Resolve `+` to `$appdir/shared/lib`; resolve `+/suffix` to
`$appdir/shared/lib/suffix`.
5. Require the resolved path to be a directory.
6. Canonicalize both the library root and resolved directory with `realpath`,
then require the result to equal the root or start with
`"$canonical_root/"`. This prevents an in-tree symlink from escaping the
AppDir even when the textual path contains no `..`.
7. On failure, print one line with the offending entry:
```text
[strip-libs] ERROR: invalid sharun lib.path entry '<entry>': <reason>
```
and return/exit non-zero before repacking.
Require at least one entry and retain the existing missing-file diagnostic.
Do not mutate the file in validation.
Update the comment above `patch_apprun_sharun_cwd` and its success/warning logs:
- state that valid `+` paths are the primary released-layout fix;
- state that the helper remains for historical shell `AppRun` layouts;
- remove claims that changing CWD is required for an ELF sharun launcher;
- preserve the current shell patch behavior and return convention.
### Step 2.3: Run GREEN checks
Linux:
```bash
bash -n scripts/release/strip-appimage-graphics-libs.sh
bash scripts/release/test-strip-appimage-rpaths.sh
```
Expected GREEN: accepted marker paths pass; every malformed entry fails with
the exact entry in stderr; the ELF hard-link fixture is detected and remains
unchanged by the shell compatibility patch.
macOS:
```bash
bash -n scripts/release/strip-appimage-graphics-libs.sh
bash -n scripts/release/test-strip-appimage-rpaths.sh
bash scripts/release/test-strip-appimage-rpaths.sh
```
Expected: syntax passes and the execution test cleanly skips if required.
### Step 2.4: Commit fail-closed validation
```bash
atomic-commit "test(appimage): reject malformed sharun library paths" -- \
scripts/release/test-strip-appimage-rpaths.sh \
scripts/release/strip-appimage-graphics-libs.sh
```
---
## Task 3: Inspect and smoke-test the final repacked AppImage before signing
**Files:**
- Create: `scripts/release/validate-appimage-runtime.sh`
- Modify: `scripts/release/test-strip-appimage-rpaths.sh`
- Modify: `scripts/release/strip-appimage-graphics-libs.sh`
### Step 3.1: Add failing tests for extracted-layout validation
The new script must be sourceable for fixture tests and executable for a real
AppImage. Before implementing it, extend
`test-strip-appimage-rpaths.sh` to source it after sourcing the postprocessor:
```bash
RUNTIME_VALIDATOR="$SCRIPT_DIR/validate-appimage-runtime.sh"
[ -f "$RUNTIME_VALIDATOR" ] \
|| fail "$RUNTIME_VALIDATOR not found"
# shellcheck source=/dev/null
source "$RUNTIME_VALIDATOR"
```
Add fixture tests for the function interface:
```bash
validate_extracted_appdir "$appdir"
validate_final_appimage "$appimage"
smoke_extracted_apprun "$appdir" "$foreign_cwd" "$log_file"
```
`validate_extracted_appdir` is the unit-test seam; it must not extract or launch
anything. Build a complete fixture with:
- hard-linked ELF `AppRun`, `sharun`, and `bin/OpenHuman`;
- executable ELF `shared/bin/OpenHuman`;
- `shared/lib/lib.path` containing `+`;
- `shared/lib/anylinux.so`;
- `shared/lib/libxdo.so.3`;
- `shared/lib/libcef.so`;
- clean RPATHs on all copied ELFs.
Because `/bin/true` does not naturally declare the production `NEEDED`
libraries, make the needed-name check injectable only in fixture mode:
```bash
APPIMAGE_EXPECTED_NEEDED="${APPIMAGE_EXPECTED_NEEDED:-libxdo.so.3 libcef.so}"
```
Set `APPIMAGE_EXPECTED_NEEDED=""` only around unit fixtures. The executable CLI
and release workflow must leave the production default intact.
Assert the complete fixture passes. Then clone it into isolated cases and
assert rejection when:
- `anylinux.so` is absent;
- `libxdo.so.3` is absent;
- `libcef.so` is absent;
- `shared/bin/OpenHuman` is absent or is not ELF;
- `AppRun` is neither the same inode nor byte-equivalent to `sharun`;
- `bin/OpenHuman` is neither the same inode nor byte-equivalent to `sharun`;
- an ELF RPATH contains
`/home/runner/work/openhuman/openhuman/shared/lib`;
- an ELF RPATH contains `/__w/openhuman/openhuman/shared/lib`;
- the real app's `NEEDED` list lacks `libxdo.so.3`;
- the real app's `NEEDED` list lacks `libcef.so`.
For equivalent launcher support, copy rather than hard-link `sharun` to
`AppRun` and `bin/OpenHuman`; byte-identical executable copies must pass. This
covers a repacker that preserves launcher contents but not hard-link metadata.
Add a source-order regression that invokes a stub validator from
`strip_one_appimage` and proves it runs after the final `mv` but before the path
is appended to `MODIFIED_PATHS`. Do this through an overridable command:
```bash
APPIMAGE_RUNTIME_VALIDATOR="${APPIMAGE_RUNTIME_VALIDATOR:-$SCRIPT_DIR/validate-appimage-runtime.sh}"
```
The test stub must record that the supplied final path exists and that
`MODIFIED_PATHS` is still empty. Do not execute or repack a real AppImage in
this unit test; factor the post-repack handoff into:
```bash
validate_rebuilt_appimage() {
local rebuilt_path="$1"
"$APPIMAGE_RUNTIME_VALIDATOR" "$rebuilt_path"
}
```
Call this seam directly with a fixture file to pin its command/argument
contract, and use a source-text order assertion for the three adjacent
statements:
```text
mv "$rebuilt" "$original"
validate_rebuilt_appimage "$original"
MODIFIED_PATHS+=("$original")
```
Run on Linux:
```bash
bash scripts/release/test-strip-appimage-rpaths.sh
```
Expected RED: the validator file/functions and the post-repack handoff do not
exist.
### Step 3.2: Implement the sourceable final-artifact validator
Create `scripts/release/validate-appimage-runtime.sh` with `set -euo pipefail`,
three shell functions, and a direct-execution guard. The exact interfaces are:
- `validate_extracted_appdir "$appdir"` performs the eight ordered static checks
immediately below.
- `smoke_extracted_apprun "$appdir" "$foreign_cwd" "$log_file"` performs the
eight ordered launch checks immediately below.
- `validate_final_appimage "$image"` performs the five ordered extraction
checks immediately below.
Use this direct-execution guard:
```bash
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
[ "$#" -eq 1 ] || {
echo "Usage: $0 <final.AppImage>" >&2
exit 2
}
validate_final_appimage "$1"
fi
```
Do not duplicate `lib.path` grammar. Resolve the script directory and source
`strip-appimage-graphics-libs.sh`, whose direct-execution guard prevents
`main()` from running. Reuse:
- `is_elf`;
- `is_executable_elf`;
- `uses_sharun_launcher`;
- `validate_sharun_lib_path`;
- `validate_appimage_required_libs`.
Implement `validate_extracted_appdir "$appdir"` as follows:
1. Print the extracted path and the normalized `lib.path` contents with each
line prefixed for readable CI logs.
2. Require `uses_sharun_launcher "$appdir"`.
3. Require ELF executables at `AppRun`, `sharun`, `bin/OpenHuman`, and
`shared/bin/OpenHuman`.
4. For both launcher aliases, accept either `[ alias -ef sharun ]` or
`cmp -s alias sharun`; reject anything else.
5. Call `validate_sharun_lib_path` and
`validate_appimage_required_libs`.
6. Add `anylinux.so` to the required-library contract in
`validate_appimage_required_libs`; its missing diagnostic must identify the
preload library separately from the existing libxdo/CEF diagnostics.
7. Run `patchelf --print-needed "$appdir/shared/bin/OpenHuman"` and require each
whitespace-delimited name in
`${APPIMAGE_EXPECTED_NEEDED:-libxdo.so.3 libcef.so}` as an exact full line.
Print the NEEDED list before checking it.
8. Walk ELF files under `shared/bin`, `shared/lib`, `bin`, `lib`, `usr/bin`, and
`usr/lib`. For each, read RPATH with `patchelf --print-rpath`; reject
`/home/runner/` and `/__w/`. This is validation only: never patch the final
artifact.
`validate_final_appimage "$image"` must:
1. Resolve the input with `realpath`, require an executable regular file, and
require `patchelf`.
2. Create one temporary root and a distinct `foreign-cwd` below it; install a
trap that removes only that exact temporary root.
3. Change into the foreign CWD before invoking
`"$image" --appimage-extract`, redirecting extraction output to a log. This
ensures extraction itself does not rely on the artifact directory.
4. Require `$foreign_cwd/squashfs-root`, then call
`validate_extracted_appdir` on it.
5. If `APPIMAGE_RUNTIME_SMOKE=1`, call `smoke_extracted_apprun`; otherwise print
an explicit architecture/static-only message.
`smoke_extracted_apprun "$appdir" "$foreign_cwd" "$log_file"` must:
1. Require `timeout` and `xvfb-run`.
2. Print the AppDir and caller CWD.
3. Create isolated `home`, `config`, `data`, and `cache` directories beneath
the temporary root so the release smoke cannot read or mutate the runner's
real OpenHuman state.
4. Launch from `$foreign_cwd` with:
```bash
timeout --signal=TERM --kill-after=5s 15s \
xvfb-run -a --server-args="-screen 0 1280x960x24" \
env \
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"
```
Before `env`, unset `GITHUB_TOKEN`, `GH_TOKEN`,
`TAURI_SIGNING_PRIVATE_KEY`, `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`,
`SENTRY_AUTH_TOKEN`, and all `OPENAI_*` variables present in the environment.
Build the `env -u NAME` arguments from variable **names only**; never print
values.
5. Capture stdout/stderr in `$log_file` and preserve the timeout status despite
`set -e`.
6. Treat status `124` as success: the GUI remained alive through the bounded
startup window. Treat any other status as failure, including an immediate
clean exit, because it did not prove a live application boundary.
7. Regardless of status, search the log case-insensitively for:
- `anylinux.so` combined with `cannot be preloaded`;
- `libxdo.so.3` combined with `cannot open shared object file`;
- `libcef.so` combined with `cannot open shared object file`;
- `error while loading shared libraries`.
8. On a forbidden diagnostic or unexpected exit, print the complete smoke log
and fail. On success, print only lines matching loader/CEF/startup keywords
plus a message that the application remained alive for the startup window.
This keeps logs useful without dumping inherited environment data.
### Step 3.3: Call the validator after repack and before signing
In `strip-appimage-graphics-libs.sh`:
1. Define `SCRIPT_DIR` once from `BASH_SOURCE[0]`.
2. Define
`APPIMAGE_RUNTIME_VALIDATOR="${APPIMAGE_RUNTIME_VALIDATOR:-$SCRIPT_DIR/validate-appimage-runtime.sh}"`.
3. Add `validate_rebuilt_appimage` with the exact one-argument forwarding
contract above.
4. In `strip_one_appimage`, change the final order to:
```bash
mv "$rebuilt" "$original"
validate_rebuilt_appimage "$original"
rm -rf "$workdir"
MODIFIED_PATHS+=("$original")
```
Because `set -e` is active, any static or launch failure stops before
`MODIFIED_PATHS` is populated and before `main` reaches `resign_artifact` or
updater tarball rebuilding.
Keep the intermediate AppDir validation at its current pre-repack location.
It provides a faster diagnostic; the new call proves the repacked bytes.
### Step 3.4: Run GREEN checks
Linux unit/fixture lane:
```bash
bash -n scripts/release/strip-appimage-graphics-libs.sh
bash -n scripts/release/validate-appimage-runtime.sh
bash -n scripts/release/test-strip-appimage-rpaths.sh
bash scripts/release/test-strip-appimage-rpaths.sh
```
Expected GREEN: extracted-layout fixtures cover required libraries, NEEDED
names, RPATHs, equivalent/hard-linked launchers, and post-repack ordering.
Linux real-artifact check, after building an amd64 AppImage:
```bash
APPIMAGE_RUNTIME_SMOKE=1 \
bash scripts/release/validate-appimage-runtime.sh \
app/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
```
If the artifact exists under the root target tree instead, use the one exact
matching file under:
```text
target/x86_64-unknown-linux-gnu/release/bundle/appimage/
```
Do not pass multiple glob matches to the one-image CLI; fail the build if the
bundle root unexpectedly contains zero or multiple amd64 AppImages.
macOS:
```bash
bash -n scripts/release/strip-appimage-graphics-libs.sh
bash -n scripts/release/validate-appimage-runtime.sh
bash -n scripts/release/test-strip-appimage-rpaths.sh
bash scripts/release/test-strip-appimage-rpaths.sh
```
Expected: syntax checks pass and the fixture suite skips cleanly without Linux
ELF tooling. Do not attempt to execute the AppImage or claim runtime validation
from macOS.
### Step 3.5: Commit final-artifact validation
```bash
atomic-commit "fix(appimage): validate the repacked runtime before signing" -- \
scripts/release/validate-appimage-runtime.sh \
scripts/release/test-strip-appimage-rpaths.sh \
scripts/release/strip-appimage-graphics-libs.sh
```
---
## Task 4: Enable the amd64 foreign-CWD smoke in the Linux desktop build
**Files:**
- Modify: `.github/workflows/build-desktop.yml`
### Step 4.1: Establish the wiring gap
Run:
```bash
rg -n 'APPIMAGE_RUNTIME_SMOKE|xvfb' .github/workflows/build-desktop.yml
```
Expected RED: the workflow neither installs Xvfb nor enables the final
AppImage smoke.
The behavior itself is already fixture-tested in Task 3. This task is declarative
workflow wiring; validate it with YAML/action checks rather than duplicating the
shell behavior in a YAML text test.
### Step 4.2: Add Linux runtime prerequisites
In `Install Tauri dependencies (ubuntu only)`, add `xvfb` to the existing
`apt-get install -y` list. `timeout` is supplied by Ubuntu's existing
`coreutils`; do not add another timeout package.
Do not add these dependencies to macOS or Windows.
### Step 4.3: Enable smoke only on the reported architecture
Rename the existing step to:
```text
Strip host graphics libs and validate final AppImage
```
Keep its Linux-only `if:` expression, bundle-root arguments, target/profile
variables, and signing secrets unchanged. Add:
```yaml
APPIMAGE_RUNTIME_SMOKE: ${{ matrix.settings.target == 'x86_64-unknown-linux-gnu' && '1' || '0' }}
```
The postprocessor now performs, in order:
1. graphics stripping/interpreter/RPATH/lib.path repair;
2. repack;
3. final artifact extraction and static validation on both Linux architectures;
4. foreign-CWD Xvfb smoke on amd64;
5. signing and updater archive rebuilding.
Do not use `continue-on-error`. Do not change the advisory status of
`pr-quality.yml`; release/staging desktop callers already consume this reusable
workflow and will fail before upload if the validator fails.
Update the step comment to state that:
- final-artifact validation occurs before re-signing;
- amd64 launch is bounded and runs from a foreign CWD;
- arm64 receives static validation in this change;
- updater archives are still rebuilt only after the validated image is final.
### Step 4.4: Validate workflow syntax and formatting
Run whichever actionlint path the repository provides:
```bash
if command -v actionlint >/dev/null 2>&1; then
actionlint .github/workflows/build-desktop.yml
else
echo "actionlint unavailable locally; rely on GitHub workflow validation"
fi
```
Then run:
```bash
pnpm exec prettier --check .github/workflows/build-desktop.yml
git diff --check
```
Expected GREEN: actionlint (when installed), Prettier, and whitespace checks
pass.
### Step 4.5: Commit workflow wiring
```bash
atomic-commit "ci(appimage): smoke-test the final amd64 launcher" -- \
.github/workflows/build-desktop.yml
```
---
## Task 5: Run local checks and obtain Linux evidence
**Files:**
- No new files; verification only.
### Step 5.1: Run the complete macOS-safe local gate
From the worktree root:
```bash
bash -n scripts/release/strip-appimage-graphics-libs.sh
bash -n scripts/release/validate-appimage-runtime.sh
bash -n scripts/release/test-strip-appimage-rpaths.sh
bash scripts/release/test-strip-appimage-rpaths.sh
pnpm exec prettier --check \
docs/plans/2026-07-24-appimage-sharun-lib-path.md \
.github/workflows/build-desktop.yml
git diff --check
git status --short
```
Expected on macOS:
- all three shell syntax checks pass;
- the Linux fixture script reports `SKIP`, not `FAIL`, if `patchelf`/ELF is
unavailable;
- Prettier and `git diff --check` pass;
- status contains no unrelated files.
Do not run `pnpm format`, because it writes across the repository and can mix
unrelated formatting into these atomic commits.
### Step 5.2: Require Linux unit evidence
The existing advisory PR-quality job must run:
```bash
bash -n scripts/release/strip-appimage-graphics-libs.sh
bash scripts/release/test-strip-appimage-rpaths.sh
```
Confirm its log includes passing cases for:
- CI/build path to `+`;
- `+/subdir`, order, deduplication, and idempotency;
- bare relative, absolute CI, traversal, missing-target, and symlink-escape
rejection;
- hard-linked/equivalent ELF launcher layout;
- anylinux/libxdo/libcef presence;
- NEEDED and RPATH validation.
The job remains advisory by design, but a failure is still a blocker for this
change.
### Step 5.3: Require a real amd64 artifact run
Run the reusable desktop build on an amd64 Ubuntu runner. The log from
`Strip host graphics libs and validate final AppImage` must show:
- final extracted AppDir path;
- caller working directory outside that AppDir;
- normalized `lib.path` containing `+` entries only;
- `shared/bin/OpenHuman` NEEDED entries;
- no CI/build-machine RPATHs;
- presence of `anylinux.so`, `libxdo.so.3`, and `libcef.so`;
- timeout status `124`, documented as the expected live-GUI boundary;
- no preload or loader error for those three libraries;
- signing/updater archive messages only after validation succeeds.
Inspect the later upload step and confirm it consumes the same AppImage path
that was validated. Do not substitute an intermediate `squashfs-root` run for
this evidence.
### Step 5.4: Final history and scope review
Run:
```bash
git status --short
git diff --check HEAD~4..HEAD
git log --oneline -4
git diff --stat baae2b02c..HEAD
git diff --name-only baae2b02c..HEAD
```
Expected:
- four atomic implementation commits after the approved spec;
- only the four implementation paths named at the top of this plan changed;
- no Cargo, AUR, OpenSSL, vendor pin, or PR-quality blocking changes;
- no submodule pointer changes.
If implementation discovers that sharun's bundled version uses a grammar
different from the confirmed newline-plus contract, stop and amend the design
with artifact/source evidence before changing the parser. Do not guess at a
second delimiter grammar in release code.
## Acceptance checklist
- [ ] CI/build paths normalize to `+` or `+/descendant`.
- [ ] `+` is never treated as a delimiter.
- [ ] Output ordering is stable, deduplicated, deterministic, and idempotent.
- [ ] Bare relatives, absolute CI paths, traversal, missing directories,
separator injection, whitespace ambiguity, and symlink escape fail closed.
- [ ] The released ELF hard-link launcher layout is covered; the shell CWD patch
remains compatibility-only.
- [ ] Intermediate and final repacked AppDirs both pass library-path,
required-library, NEEDED, and RPATH validation.
- [ ] The final amd64 AppImage is launched from a foreign CWD under Xvfb and
remains alive through the bounded startup window.
- [ ] Loader logs contain no `anylinux.so`, `libxdo.so.3`, or `libcef.so`
resolution error.
- [ ] Validation completes before signing/updater archive rebuilding/upload.
- [ ] Arm64 static validation and all non-Linux workflow behavior remain intact.
- [ ] #3716/OpenSSL, unbundled AUR/core, optional `enigo`, rolling-distro
matrices, and quick-sharun pinning remain explicit follow-ups.
@@ -0,0 +1,215 @@
# AppImage sharun library-path repair
## Context
Issue #5037 reports that Linux AppImages fail when launched outside their
extracted AppDir. The launcher logs that `anylinux.so` cannot be preloaded and
then fails to resolve `libxdo.so.3`, even though both libraries are bundled.
Running the extracted application with an explicit AppDir-rooted
`LD_LIBRARY_PATH` succeeds.
Read-only analysis verified the final v0.61.2 and v0.63.1 amd64 AppImages:
- `AppRun`, `sharun`, and `bin/OpenHuman` are hard links to the sharun ELF
launcher.
- The real application is `shared/bin/OpenHuman`.
- `lib/` contains `anylinux.so`, `libxdo.so.3`, `libcef.so`, `libssl.so.3`, and
`libcrypto.so.3`; `shared/lib` is a symlink to `../lib`.
- `shared/lib/lib.path` contains the bare relative entry `shared/lib`.
- sharun 2.2.4 and 2.2.5 expand the canonical `+` marker to an absolute path
rooted at the AppDir's `lib/` directory. They do not anchor a bare
`shared/lib` entry.
The release postprocessor causes the malformed entry.
`rewrite_sharun_lib_path` converts absolute CI paths beneath
`squashfs-root/shared/lib` into `shared/lib`. The intended compensation,
`patch_apprun_sharun_cwd`, only edits a shell `AppRun` and intentionally skips
the shipped ELF `AppRun`.
Consequently, `shared/lib` resolves from the caller's current directory. It
works when the caller happens to be the AppDir and fails from locations such as
`~/Downloads` or `/tmp`.
## Evidence boundary
Artifact hashes, SquashFS layouts, ELF metadata, release-script behavior, sharun
path expansion, and caller-directory resolution were verified directly.
This development host is macOS, and its Docker daemon is unavailable, so it
cannot execute the Linux ELF launcher. An identical v0.63.1 process failure is a
strong inference from the verified path semantics and must be confirmed by a
Linux CI smoke test.
## Goals
- Write sharun-native, AppDir-anchored entries to `shared/lib/lib.path`.
- Reject malformed bare-relative or CI-runner paths before an AppImage ships.
- Cover the actual ELF `AppRun` layout used by released artifacts.
- Validate the final repacked AppImage, rather than only the intermediate
AppDir.
- Exercise the final launcher from a working directory outside the AppDir on
Linux.
- Preserve the existing graphics-library stripping, interpreter repair,
signing, updater archive, and multi-architecture behavior.
## Non-goals
- Making the unbundled AUR/core binary independent of system `libxdo`.
- Changing the unconditional `enigo` dependency or voice input behavior.
- Replacing bundled OpenSSL or claiming to resolve issue #3716's later-stage
symbol-skew/segfault report.
- Bypassing sharun with a custom `LD_LIBRARY_PATH` launcher.
- Pinning mutable quick-sharun downloads; that is a separate supply-chain
follow-up.
- Converting the existing PR-quality AppImage job from advisory to blocking.
## Considered approaches
### 1. Emit sharun-native `+` paths and validate them
Rewrite AppDir library roots to sharun's canonical `+` representation. A root
library directory becomes `+`; any retained directory beneath it becomes a
`+/subdir` entry. Preserve the format's entry separator without treating the
marker itself as a separator.
Validation rejects:
- absolute CI runner paths;
- bare relative entries such as `shared/lib`;
- traversal components;
- AppDir-relative targets that do not exist.
This directly restores sharun's intended absolute path expansion and is the
selected approach.
### 2. Change the working directory in `AppRun.sh`
Patching the shipped shell wrapper or setting a sharun working-directory
variable could make `shared/lib` resolve by accident. It leaves malformed
library-path semantics in place, depends on launcher shape, and does not explain
the existing ELF `AppRun` skip clearly enough to be the primary fix.
### 3. Bypass sharun
A wrapper could execute `shared/bin/OpenHuman` with an explicit absolute
`LD_LIBRARY_PATH`. This duplicates sharun's loader and portability behavior and
risks losing its environment and compatibility hooks.
## Design
### Library-path normalization
Refactor `rewrite_sharun_lib_path` in
`scripts/release/strip-appimage-graphics-libs.sh` so CI-origin entries are
normalized into sharun-native AppDir library entries.
The normalizer must:
1. Parse entries without destroying the `+` marker.
2. Recognize CI paths rooted under an extracted `squashfs-root` or the existing
build `data` layout.
3. Map the AppDir's canonical library root to `+`, and valid descendants to
`+/relative/subdir`.
4. Deduplicate entries while preserving stable order.
5. Exclude unrelated absolute host paths.
6. Fail instead of falling back to a bare `shared/lib` entry when no valid
AppDir library root can be derived.
7. Be idempotent when the file already contains valid sharun-native entries.
The implementation must follow sharun's actual `lib.path` delimiter grammar as
confirmed from the bundled sharun version. Tests must pin the accepted textual
forms rather than rely on an informal reconstruction.
### Fail-closed validation
Extend `validate_sharun_lib_path` to reject any non-empty entry that is:
- a CI/build-machine absolute path;
- a bare relative path not beginning with the sharun marker;
- a traversal outside the bundled library root;
- a marker-relative directory absent from the AppDir.
The validation error must identify the offending entry and refuse to repack or
publish the artifact.
`patch_apprun_sharun_cwd` may remain as compatibility hardening for historical
shell AppRun layouts, but the new tests must establish that an ELF AppRun does
not need this patch once `lib.path` is valid. Comments that describe changing
the working directory as the primary fix must be updated to reflect that role.
### Regression tests
Extend `scripts/release/test-strip-appimage-rpaths.sh` with isolated cases for:
- a CI absolute `.../squashfs-root/shared/lib` entry rewriting to the canonical
marker;
- valid marker/subdirectory entries and stable ordering;
- deduplication and idempotency;
- rejection of bare `shared/lib`, CI paths, traversal, and missing targets;
- the released launcher layout where ELF `AppRun`, `sharun`, and
`bin/OpenHuman` are hard-linked or equivalent launcher entries;
- expansion of the rewritten value according to sharun's algorithm, proving it
resolves beneath the fixture AppDir regardless of the caller's directory.
The test remains Linux-only because it needs ELF fixtures and `patchelf`; it
continues to skip cleanly on unsupported development hosts.
### Final-artifact validation and smoke
After postprocessing and repacking, Linux CI must inspect the final `.AppImage`
that will be uploaded:
1. Extract it into a temporary directory.
2. Re-run the sharun `lib.path`, required-library, ELF `NEEDED`, and RPATH
validations against the extracted final layout.
3. Change to an unrelated temporary working directory.
4. Launch the extracted `AppRun` under a bounded timeout and the existing
headless/Xvfb facilities.
5. Fail on preload or loader diagnostics for `anylinux.so`, `libxdo.so.3`, or
`libcef.so`.
6. Treat reaching the normal application initialization boundary as success;
the smoke does not require an interactive desktop session.
The smoke belongs in the Linux desktop build path after
`Strip host graphics libs from AppImage`, so it exercises the same final
artifact that later signing/upload steps consume. It must cover amd64 first,
where #5037 was reproduced, while keeping the validator architecture-neutral so
arm64 can use it when an executable runner is available.
## Failure behavior
An invalid `lib.path`, missing bundled runtime library, failed final extraction,
or foreign-CWD loader error fails the Linux artifact build before signing or
upload. The release must not continue with a warning-only malformed launcher.
The smoke must print the extracted path, caller working directory, normalized
`lib.path`, and relevant loader diagnostics without leaking credentials.
## Validation
Local/macOS:
- `bash -n scripts/release/strip-appimage-graphics-libs.sh`
- the regression script skips cleanly when Linux ELF tooling is unavailable;
- `git diff --check` and repository formatting checks.
Linux:
- run `scripts/release/test-strip-appimage-rpaths.sh` with `patchelf`;
- run the final-artifact validator against a freshly built amd64 AppImage;
- launch from a foreign working directory under timeout/Xvfb;
- confirm no `anylinux.so`, `libxdo.so.3`, or `libcef.so` loader errors;
- confirm the updater archive/signing flow still consumes the validated
postprocessed artifact.
## Acceptance criteria
- Final `lib.path` uses only valid sharun-native AppDir entries.
- Bare `shared/lib` and CI absolute entries fail validation.
- Rewriting is deterministic and idempotent.
- Released-style ELF AppRun fixtures are covered.
- The final repacked amd64 AppImage launches from outside its AppDir without the
#5037 preload/library-resolution errors.
- Required bundled libraries, ELF dependencies, and RPATHs remain valid.
- Existing graphics stripping, signing, updater archive, and non-Linux build
behavior remain unchanged.
+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