Skip to content

fix(appimage): unblock release smoke by relaxing the userns sysctl (main copy of #5251) - #5252

Merged
M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/appimage-smoke-userns-main-DAPPIMG-39513
Jul 28, 2026
Merged

fix(appimage): unblock release smoke by relaxing the userns sysctl (main copy of #5251)#5252
M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/appimage-smoke-userns-main-DAPPIMG-39513

Conversation

@M3gA-Mind

@M3gA-Mind M3gA-Mind commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Parallel main copy of the release fix in #5251. Base is main.
#5251 lands this on release to unblock the production cut; this PR carries the identical commit onto main so the next mainrelease promotion does not reintroduce the failure.
Cherry-pick of 0be92171b (release branch) — applied cleanly, no conflicts; the two files are byte-identical to the release PR's versions.
Release PR: #5251
Failed run that motivated both: https://github.com/tinyhumansai/openhuman/actions/runs/30361949794 (job Build desktop matrix / Desktop: ubuntu, release @ 9c1e2e65)

Summary

  • Unblocks Release Production: the Linux x86_64 AppImage startup smoke added by fix(appimage): anchor sharun library paths to AppDir #5189 has never passed and fails deterministically with exit 133 instead of the expected 124.
  • Root cause is Ubuntu 23.10+'s kernel.apparmor_restrict_unprivileged_userns=1, which denies unprivileged user namespaces to unconfined processes, so CEF's zygote aborts with No usable sandbox!.
  • fix(appimage): anchor sharun library paths to AppDir #5189's mitigation — a per-executable AppArmor profile — loads successfully but never takes effect, because AppArmor attaches profiles by execve path and the sharun launcher does not exec shared/bin/OpenHuman directly.
  • Applies Chromium's other documented remedy instead: relax the sysctl for the smoke window and restore it afterwards. Path-independent, so it cannot miss the way profile attachment does.
  • No workflow change and no change to the validator's public contract; the fix is entirely inside validate-appimage-runtime.sh and its fixture tests.

Problem

The Strip host graphics libs and validate final AppImage step fails on ubuntu (x86_64). The AppImage itself is fine — it builds, signs, bundles libcef.so, and boots far enough to log [cef-startup]. What fails is the new bounded startup smoke:

[appimage-runtime] AppImage startup smoke failed (status 133):
[appimage-runtime]   unshare: write failed /proc/self/uid_map: Operation not permitted
[appimage-runtime]   ...
[appimage-runtime]   [59873:59873:...:FATAL:content/browser/zygote_host/zygote_host_impl_linux.cc:128]
                     No usable sandbox! If you are running on Ubuntu 23.10+ or another Linux distro
                     that has disabled unprivileged user namespaces with AppArmor ...
[appimage-runtime]   Trace/breakpoint trap (core dumped)

smoke_extracted_apprun treats timeout's status 124 as success. CEF dies on SIGTRAP about one second in, so the smoke sees 133 and fails the release.

This is a regression from #5189, and the smoke has never passed

Run Head SHA Result
30240103654 e3a7f5217 ✅ last green — pre-#5189 script
30242402885 2e5b5e7b2Promote main → release (#5203), which carried #5189
30351759443 a40fbb79d
30361949794 9c1e2e657

The failure is byte-identical across all three. #5189 merged into main, where the production AppImage build does not run, so this was never exercised before promotion. ubuntu-arm64 is unaffected only because build-desktop.yml enables the smoke for x86_64 alone.

Why #5189's AppArmor profile does not work

install_smoke_userns_profile implements Chromium's per-executable-profile remedy against $appdir/shared/bin/OpenHuman. The profile loads — the log confirms Loaded temporary AppArmor userns profile for: .../shared/bin/OpenHuman — yet unshare is still denied. AppArmor attaches profiles by the execve'd path, and the sharun launcher runs the app through the AppDir's bundled dynamic loader rather than exec'ing shared/bin/OpenHuman; Chromium then re-execs /proc/self/exe for the zygote. The confinement target never matches, so the userns, grant is never in force.

Solution

In scripts/release/validate-appimage-runtime.sh:

  • Add relax_smoke_userns_restriction / restore_smoke_userns_restriction, which set kernel.apparmor_restrict_unprivileged_userns=0 for the smoke window and restore the host's original value afterwards — including on the existing HUP/INT/TERM interrupt path and when AppArmor setup itself fails. The runner is left exactly as it was found.
  • The toggle is a no-op when the key is already 0 or absent on the host, so it never reaches for sudo outside the restricted-Ubuntu case it exists for.
  • The AppArmor profile is retained alongside the sysctl: it is harmless, and it keeps the narrower per-executable grant in place for hosts where the sysctl is unavailable.
  • The four-argument contract of smoke_extracted_apprun_with_userns is unchanged; the saved-state file is derived as $profile_file.sysctl. No change to build-desktop.yml is required.

Resulting privileged-command sequence, pinned by fixtures:

sudo sysctl -q -w …userns=0 → apparmor_parser --replace → smoke → apparmor_parser --remove → sudo sysctl -q -w …userns=<original>

Rejected alternative: OPENHUMAN_CEF_NO_SANDBOX / --no-sandbox

The smoke currently unsets OPENHUMAN_CEF_NO_SANDBOX, so setting it looks like the obvious fix. It is not:

Keeping the sandbox enabled is also the better outcome — the smoke goes on exercising the real production configuration rather than a weakened one.

Impact

  • CI/release only. No shipped code changes: no Rust, no frontend, no bundling behaviour, no change to the AppImage bytes.
  • Security posture is unchanged in the product. The sysctl is relaxed only on the ephemeral hosted runner, only for the ~15-second smoke window, and is restored afterwards; Chromium's sandbox stays enabled in the smoked app.
  • If a maintainer needs a second escape hatch, APPIMAGE_RUNTIME_SMOKE: '0' for x86_64 in .github/workflows/build-desktop.yml disables the gate in one line.

Verification

scripts/release/test-strip-appimage-rpaths.sh is Linux-only and skips on macOS (SKIP: patchelf not installed), so the new logic was additionally driven directly against the same command fakes:

  • restricted host → relax, save 1, restore 1, state file removed;
  • permissive host (0) → no sudo, no state file;
  • absent sysctl key → no sudo, no state file;
  • full wrapper ordering, preservation of a failing smoke's status 37, and no leaked state.

The observed sequence matches the fixture expectations exactly. This cannot be validated end-to-end by normal PR CI — see the note below.

Related

Verification by a maintainer is required

Regular PR CI does not run the production Linux AppImage build, so neither this PR's checks nor #5251's can prove the fix. A maintainer must re-dispatch the Release Production workflow (against release, via #5251) to confirm the Desktop: ubuntu job now reaches status 124 and passes. This main PR needs no separate dispatch — it is the identical commit.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy — test-strip-appimage-rpaths.sh: updated both expected privileged-command-order fixtures (failing-smoke and TERM-interrupt paths), added no-op coverage for permissive-host and absent-key, and assertions that the saved-state file never leaks.
  • Diff coverage ≥ 80% — changed lines are Bash in scripts/release/**, covered by the test-strip-appimage-rpaths.sh fixture suite. diff-cover over Vitest + cargo-llvm-cov does not instrument shell; no TS/Rust lines changed.
  • Coverage matrix updated — N/A: CI/release tooling change, no product feature row affected.
  • All affected feature IDs from the matrix are listed under ## RelatedN/A: no feature IDs affected.
  • No new external network dependencies introduced — no new downloads or endpoints; sysctl/sudo are already required by this script.
  • Manual smoke checklist updated if this touches release-cut surfaces — N/A: restores the intended behaviour of an existing automated release gate; the manual checklist is unchanged.
  • Linked issue closed via Closes #NNNN/A: no tracking issue; this PR is driven by the failed release run linked above.

AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

Validation Run

  • pnpm --filter openhuman-app format:checkN/A: no files under app/ changed.
  • pnpm typecheckN/A: no TypeScript changed.
  • Focused tests: bash -n on both scripts; new relax/restore functions and smoke_extracted_apprun_with_userns driven against the fixture command fakes (restricted / permissive / absent-key / wrapper ordering / failure-status preservation). scripts/release/test-strip-appimage-rpaths.sh skips on macOS (patchelf is Linux-only) and runs in CI.
  • Rust fmt/check (if changed): N/A: no Rust changed.
  • Tauri fmt/check (if changed): N/A: no Tauri code changed.

Validation Blocked

  • command: Release ProductionBuild desktop matrix / Desktop: ubuntu
  • error: not runnable locally — Linux-only AppImage bundling plus an Xvfb/AppArmor runtime smoke; this work was done on macOS, and the job is not triggered by regular PR CI.
  • impact: the end-to-end fix can only be confirmed by a maintainer re-dispatching Release Production. Diagnosis was derived from the full failing log and the scripts; the new shell logic is verified locally against command fakes.

Behavior Changes

  • Intended behavior change: during the release AppImage smoke only, unprivileged user namespaces are permitted on the runner so CEF's zygote sandbox can initialise; the host value is restored immediately afterwards.
  • User-visible effect: none. No shipped artifact changes.

Parity Contract

  • Legacy behavior preserved: the AppArmor profile install/remove path, the four-argument signature of smoke_extracted_apprun_with_userns, the 124-means-alive success rule, the forbidden-loader-diagnostic greps, secret scrubbing, and signal-trap restore are all unchanged.
  • Guard/fallback/dispatch parity checks: the sysctl toggle is skipped entirely when the key reads 0 or is absent, so non-Ubuntu and already-permissive hosts follow exactly the previous code path; restore is idempotent and runs on the success, failure, AppArmor-setup-failure, and interrupt paths.

Duplicate / Superseded PR Handling

Summary by CodeRabbit

  • Bug Fixes

    • Improved AppImage runtime smoke validation on newer Ubuntu environments by temporarily relaxing the AppArmor user-namespace restriction for the test window.
    • Added robust restoration logic for both normal completion and interruption cleanup, with correct precedence of the test outcome over restore failures.
    • Added protections to avoid unnecessary or persisted sysctl changes on permissive or missing-key hosts.
  • Tests

    • Expanded AppArmor userns restriction smoke tests to be deterministic via controlled sysctl toggling.
    • Added assertions around state-file cleanup and handling of TERM-interrupted execution.

The Linux desktop job has failed every Release Production run since tinyhumansai#5189
reached release (promote tinyhumansai#5203). The AppImage itself builds, signs and boots
fine; the new bounded startup smoke is what fails, deterministically, with
exit 133 instead of the expected 124:

    FATAL:content/browser/zygote_host/zygote_host_impl_linux.cc:128]
    No usable sandbox! If you are running on Ubuntu 23.10+ ...

Ubuntu 23.10+ sets kernel.apparmor_restrict_unprivileged_userns=1, which
denies unprivileged user-namespace creation to every unconfined process on
the runner, so CEF's zygote cannot build its sandbox and aborts on SIGTRAP.

tinyhumansai#5189 reached for Chromium's second documented remedy, a per-executable
AppArmor profile carrying `userns,`. The profile loads successfully but never
takes effect, because AppArmor attaches profiles by execve path while the
sharun launcher runs the app through the AppDir's bundled dynamic loader
rather than exec'ing shared/bin/OpenHuman directly (Chromium then re-execs
/proc/self/exe for the zygote).

Apply Chromium's first documented remedy instead: relax the sysctl for the
smoke window and restore its original value afterwards, including on
interrupt. It is path-independent, so it cannot miss the way profile
attachment does. The AppArmor profile is retained alongside it -- it is
harmless and keeps the narrower grant for hosts without the sysctl.

Setting OPENHUMAN_CEF_NO_SANDBOX is deliberately not the fix: it is
#[cfg(debug_assertions)]-gated in app/src-tauri/src/lib.rs and therefore
inert in the release AppImage, and CEF's command line is built explicitly
via command_line_args() so a --no-sandbox argv flag would not reach it
either. Keeping the sandbox enabled also means the smoke keeps exercising
the real production configuration.

The toggle is a no-op on hosts that are already permissive or that have no
such sysctl key, so it never reaches for sudo outside the restricted-Ubuntu
case it exists for.

Refs: https://github.com/tinyhumansai/openhuman/actions/runs/30361949794
@M3gA-Mind
M3gA-Mind requested a review from a team July 28, 2026 19:52
@M3gA-Mind

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@M3gA-Mind
M3gA-Mind merged commit 9750b80 into tinyhumansai:main Jul 28, 2026
20 of 21 checks passed
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c20667bf-b6c8-4c33-9987-d1a90f6ec85f

📥 Commits

Reviewing files that changed from the base of the PR and between 4ce2a8c and def15a2.

📒 Files selected for processing (2)
  • scripts/release/test-strip-appimage-rpaths.sh
  • scripts/release/validate-appimage-runtime.sh

📝 Walkthrough

Walkthrough

The AppImage runtime smoke now temporarily relaxes the AppArmor user namespace sysctl restriction, records and restores its prior value, handles interruption cleanup, and adds deterministic tests for failure, TERM, permissive-host, and missing-key scenarios.

Changes

AppImage user namespace restriction handling

Layer / File(s) Summary
Sysctl relaxation and restoration helpers
scripts/release/validate-appimage-runtime.sh
Adds the user namespace sysctl constant and helpers to read, relax, record, and restore its value.
Smoke setup and cleanup integration
scripts/release/validate-appimage-runtime.sh
Integrates sysctl state-file handling into smoke setup, interruption cleanup, and final status precedence.
Sysctl behavior and cleanup tests
scripts/release/test-strip-appimage-rpaths.sh
Adds deterministic sysctl fixtures and verifies command ordering, cleanup, and permissive-host no-op behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AppImageSmoke
  participant SysctlHelpers
  participant SudoSysctl
  participant Cleanup
  AppImageSmoke->>SysctlHelpers: Relax user namespace restriction
  SysctlHelpers->>SudoSysctl: Read and set sysctl to 0
  AppImageSmoke->>AppImageSmoke: Run AppImage userns smoke
  AppImageSmoke->>Cleanup: Remove profile and restore sysctl
  Cleanup->>SysctlHelpers: Restore recorded value
  SysctlHelpers->>SudoSysctl: Set previous sysctl value
Loading

Possibly related PRs

  • tinyhumansai/openhuman#5189: Covers the same AppImage/AppArmor user namespace smoke area extended here with sysctl toggling and cleanup assertions.

Suggested labels: bug

Suggested reviewers: senamakel

Poem

I’m a rabbit guarding the smoke-test gate,
With sysctl toggles arriving right on time.
I stash the old value, clean every trace,
Restore through signals, failure, and grace.
No-op hosts hop happily in line.


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added the bug label Jul 28, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: def15a2273

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +256 to +259
sudo --non-interactive sysctl -q -w "$SMOKE_USERNS_SYSCTL=0" \
|| { runtime_validation_error "could not relax $SMOKE_USERNS_SYSCTL for the AppImage smoke"; return 1; }

printf '%s\n' "$current" >"$previous_file"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Save sysctl state before the privileged write

When the release workflow is canceled or receives SIGTERM after this sysctl ...=0 succeeds but before the next line records $previous_file, the interrupt cleanup has no saved value and restore_smoke_userns_restriction no-ops, leaving kernel.apparmor_restrict_unprivileged_userns globally relaxed on that runner. Record the original value before changing the sysctl (and delete that record only after a successful restore or failed relax) so the existing signal trap can always put the host back.

Useful? React with 👍 / 👎.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The state file is removed before the sudo sysctl call. If sudo fails transiently, the previous value is unrecoverable — the runner stays with userns=0 and there is nothing left to inspect or retry. Moving rm -f to after the successful restore is the safer order.

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

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown

Greptile Summary

Fixes the deterministically failing AppImage startup smoke on Ubuntu 23.10+ by temporarily relaxing kernel.apparmor_restrict_unprivileged_userns for the ~15-second smoke window and restoring it afterwards, rather than relying on per-executable AppArmor profile attachment (which was silently missing the sharun execve path). No shipped code changes; the fix is entirely within the CI release scripts.

  • Adds relax_smoke_userns_restriction / restore_smoke_userns_restriction that toggle the sysctl via sudo, guarded by no-op paths for already-permissive or absent-key hosts, and wires them into smoke_extracted_apprun_with_userns ahead of the existing AppArmor profile install, covering the normal, AppArmor-failure, and signal-interrupt cleanup paths.
  • Extends test-strip-appimage-rpaths.sh with a fake sysctl binary (simulating a restricted Ubuntu 24.04 host), updated command-order assertions for both the failing-smoke and TERM-interrupt fixtures, and a dedicated no-op loop covering value=0 and absent-key scenarios.

Confidence Score: 4/5

Safe to merge — CI/release tooling only, no shipped code changes, and the sysctl toggle is guarded by no-op paths for non-Ubuntu or already-permissive hosts.

The sysctl state file is deleted before the restore sudo call; a transient sudo failure would leave the runner with userns=0 and no state file to recover from, though on an ephemeral runner this has no lasting effect. All other paths — normal, AppArmor-setup failure, and signal interrupt — are correctly wired and well-covered by the fixture tests.

Files Needing Attention: validate-appimage-runtime.sh — specifically the delete-before-restore ordering in restore_smoke_userns_restriction.

Important Files Changed

Filename Overview
scripts/release/validate-appimage-runtime.sh Adds relax/restore functions for kernel.apparmor_restrict_unprivileged_userns, wiring them into smoke_extracted_apprun_with_userns before/after the AppArmor profile install and on all interrupt paths; state file is deleted before the restore sudo call (minor ordering concern).
scripts/release/test-strip-appimage-rpaths.sh Adds a fake sysctl binary to the restricted-host fixture and extends expected command-order assertions for both the failing-smoke and TERM-interrupt paths; adds a dedicated no-op loop covering the permissive-host (value=0) and absent-key (sysctl exits 1) scenarios.

Sequence Diagram

sequenceDiagram
    participant W as smoke_extracted_apprun_with_userns
    participant R as relax_smoke_userns_restriction
    participant A as install_smoke_userns_profile
    participant S as smoke_extracted_apprun
    participant RA as remove_smoke_userns_profile
    participant RS as restore_smoke_userns_restriction

    W->>W: set HUP/INT/TERM traps
    W->>W: rm -f userns_sysctl_file
    W->>R: relax(userns_sysctl_file)
    R->>R: sysctl -n key (read current)
    alt "current == 0 or absent"
        R-->>W: no-op, return 0
    else "current == 1"
        R->>R: "sudo sysctl -q -w key=0"
        R->>R: write current to state file
        R-->>W: return 0
    end
    W->>A: install AppArmor profile
    alt AppArmor fails
        A-->>W: return 1
        W->>RS: restore(userns_sysctl_file)
        W->>W: restore signal traps, return 1
    end
    W->>S: smoke (timeout 15s)
    S-->>W: smoke_status
    W->>RA: remove AppArmor profile
    W->>RS: restore(userns_sysctl_file)
    RS->>RS: "sudo sysctl -q -w key=previous"
    RS->>RS: rm -f state file
    W->>W: restore signal traps
    W-->>W: return smoke_status (or remove/restore status)
Loading

Reviews (1): Last reviewed commit: "fix(appimage): unblock release smoke by ..." | Re-trigger Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants