Skip to content

fix(desktop): the AppImage shipped a launcher only its builder could execute - #470

Merged
cevheri merged 2 commits into
mainfrom
fix/appimage-perms
Aug 23, 2026
Merged

fix(desktop): the AppImage shipped a launcher only its builder could execute#470
cevheri merged 2 commits into
mainfrom
fix/appimage-perms

Conversation

@cevheri

@cevheri cevheri commented Aug 23, 2026

Copy link
Copy Markdown
Member

What was wrong

linuxdeploy writes AppRun.wrapped into the AppDir as 0770 root:root, and every released AppImage carried that mode. On the released 0.13.2 artifact:

$ unsquashfs -lls fs.sqfs | grep AppRun
-rwxr-xr-x root/root       274  squashfs-root/AppRun
-rwxrwx--- root/root     31552  squashfs-root/AppRun.wrapped

Reproduced as a different uid over root-owned files:

/tmp/AppDir/AppRun: line 12: /tmp/AppDir/AppRun.wrapped: Permission denied

and the same tree starts normally after chmod 0755 on that one file.

Why nothing noticed

The AppImage runtime hides it from the case everyone tests. A type-2 image mounts its squashfs through FUSE privately to the invoking user, and a private FUSE mount skips the kernel permission check. Measured, not assumed: the shipped 0.13.2 image, mounted and run as uid 1000 in a FUSE-capable Ubuntu 22.04 container, opens its window in 1 s exactly as it always did.

What breaks is every reader that applies real permission checks:

  • an extracted AppDir owned by another uid (--appimage-extract then run as someone else)
  • a container or sandbox running the AppDir as non-root
  • firejail's --appimage mount, which is what AppImageHub's review CI uses - it mounts as root and drops to the user, so the mode applies

That last one is how this surfaced: AppImage/appimage.github.io#3861 failed with exactly the line above, then hung until the workflow's 10 minute timeout because no window ever appeared.

The fix

scripts/build-desktop-appimage.sh audits the extracted modes and, when the audit fails, widens them and repacks the image.

The repack reuses the original runtime bytes and the original squashfs parameters instead of calling appimagetool again. The runtime is what decides whether the image mounts on the user's machine at all - appimagetool 13's runtime needs libfuse2, which Ubuntu 24.04 does not ship - so regenerating it to fix a file mode would trade this defect for a worse one. Compression and block size are read back from the bundler's own output rather than hardcoded, so a future appimagetool that changes them does not silently repack differently.

The audit lives in scripts/check-appimage-perms.mjs so it is unit tested rather than asserted in bash, and it runs twice: once on the bundler's output to decide, once on the repacked image to verify, because a repack that dropped the modes would otherwise pass on the strength of its input.

Verification

On Ubuntu 22.04 containers, against the released 0.13.2 artifact and the locally repacked one:

Case 0.13.2 as shipped after the fix
mounted via FUSE, run as uid 1000 window in 1 s window in 1 s
AppDir root-owned, run as uid 1000 Permission denied, no window window in 1 s
check-appimage-perms.mjs 1 offender: 0770 AppRun.wrapped OK
appdir-lint.sh / desktop-file-validate exit 0 / exit 0 exit 0 / exit 0

Local gates: format, lint, typecheck, knip, test (33/33 groups), build. scripts/*.mjs is outside the merged-lcov universe (merge-lcov.mjs keeps src/ only), so the coverage gate is unaffected.

This PR's own Build AppImage (x64) job is the first run of the repack path on the release runner image.

…Redis, ClickHouse, and Apache Druid official documentation
…execute

linuxdeploy writes `AppRun.wrapped` into the AppDir as `0770 root:root`, and the
released image carried that mode. Measured on 0.13.2: `-rwxrwx--- root/root
AppRun.wrapped`, reproduced as uid 1000 over root-owned files as
`/AppRun: line 12: /AppRun.wrapped: Permission denied`, and fixed by that one
file at 0755.

The AppImage runtime hides this from the case everyone tests. A type-2 image
mounts its squashfs through FUSE privately to the invoking user, and a private
FUSE mount skips the kernel permission check - measured too: the shipped 0.13.2
image mounts and opens its window as uid 1000 exactly as it always did. What
breaks is every reader that applies real permission checks: an extracted AppDir
owned by another uid, a container running as non-root, and firejail's
`--appimage` mount, which is what AppImageHub's review CI runs. There the app
never starts, with a bare `Permission denied` and no window to diagnose from.

So the build now audits the extracted modes and repacks when the audit fails. The
repack reuses the original runtime bytes and the original squashfs parameters
rather than calling appimagetool again: the runtime is what decides whether the
image mounts on the user's machine at all, and swapping it to fix a file mode
would trade this defect for a worse one. Verified end to end on Ubuntu 22.04 - the
repacked image mounts and opens its window as uid 1000, and its AppDir also runs
when the files are root-owned, which the shipped one does not.

The audit is `scripts/check-appimage-perms.mjs` so it is unit tested rather than
asserted in bash, and both AppImage builds now install squashfs-tools.
@sonarqubecloud

Copy link
Copy Markdown

@cevheri
cevheri merged commit 135f9e2 into main Aug 23, 2026
26 checks passed
@cevheri
cevheri deleted the fix/appimage-perms branch August 23, 2026 22:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant