From 696c6b0e528b19cd642661df1cd2bfcf75b328bf Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 9 Sep 2026 20:10:24 +0300 Subject: [PATCH] fix: bind AppImage updates to the running bundle --- AGENTS.md | 3 + docs/launcher-architecture.md | 6 +- docs/updater-release.md | 5 +- src-tauri/src/updater.rs | 179 +++++++++++++++++++++++++++++++++- 4 files changed, 187 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5bcd9f9..7f6112c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,6 +61,9 @@ payload are in `/root/shacraft` on the ShaCraft host; see authenticated with the pinned Tauri/minisign public key before JSON parsing. Each exact version/platform artifact also requires that signature, SHA-256 and size. Never reuse the ShaCraft mod-manifest key or accept IPC URLs/keys. +- Linux self-update requires the original ordinary AppImage file and the frozen + Tauri `APPDIR` bound to the running `usr/bin/shacraft-launcher`. Extracted or + inherited AppImage context is manual-only; never replace a bare binary. - `update_guard.rs` retains one launcher-instance OS lock, drains native writes through `operations::Lifecycle`, and checks the existing detached-game lease. `launcher-state/pending-update.json` survives installer handoff; only startup diff --git a/docs/launcher-architecture.md b/docs/launcher-architecture.md index 514bb87..087c7ca 100644 --- a/docs/launcher-architecture.md +++ b/docs/launcher-architecture.md @@ -220,7 +220,11 @@ The webview supplies no URLs, public keys, executable arguments, release version or arbitrary file path. No generic updater plugin permission is granted to it. The packaged native architecture selects the artifact: Windows preserves MSI versus NSIS, macOS preserves Intel versus Apple Silicon, Linux only replaces an -AppImage. A Debian installation requires the user's package manager. +AppImage. The original non-symlink AppImage must have the expected native header, +and the frozen Tauri `APPDIR` must contain the actual running executable at +`usr/bin/shacraft-launcher`. Extracted binaries and inherited context from another +AppImage require manual installation. A Debian installation requires the user's +package manager. Checks run once per application UI lifecycle and on explicit request. They do not install automatically. The settings drawer shows installed/available diff --git a/docs/updater-release.md b/docs/updater-release.md index 06c22e4..4dc6949 100644 --- a/docs/updater-release.md +++ b/docs/updater-release.md @@ -184,7 +184,10 @@ updates preserve it: into an appropriate Applications directory before use. A protected destination may require OS permission or a manual replacement; an installation error never counts as a completed update. -- Linux x64 AppImage → x64 AppImage at its current writable location. Debian +- Linux x64 AppImage → x64 AppImage at its current writable location. The original + ordinary, non-symlink AppImage must exist, and the frozen Tauri `APPDIR` must + match the running `usr/bin/shacraft-launcher`. Extracted AppDirs and an inherited + environment from another AppImage use manual installation. Debian packages, RPM, bare development binaries and unsupported architectures never enter the self-replacement path. Install a new deb through the system package manager; the launcher does not run privileged package-manager commands. diff --git a/src-tauri/src/updater.rs b/src-tauri/src/updater.rs index 0d9a7df..9e112ae 100644 --- a/src-tauri/src/updater.rs +++ b/src-tauri/src/updater.rs @@ -14,6 +14,8 @@ use std::{ sync::{Arc, Mutex}, time::{Duration, Instant}, }; +#[cfg(target_os = "linux")] +use tauri::Manager; use tauri::{AppHandle, Emitter}; use tauri_plugin_updater::UpdaterExt; @@ -181,11 +183,82 @@ fn package_mode( } } -fn current_mode() -> PackageMode { +// The stamped bundle type survives extracting an AppImage. It does not identify +// the runtime file which the plugin will replace. Only the frozen Tauri Env is +// shared with the plugin's executable_path selection; do not reread process env. +#[cfg(any(target_os = "linux", test))] +fn appimage_context_valid(path: Option<&Path>, ordinary_file: bool, header: &[u8]) -> bool { + path.is_some_and(Path::is_absolute) + && ordinary_file + && format::verify( + &PackageMode::Automatic { + platform: "linux-x86_64", + msi: false, + }, + header, + ) + .is_ok() +} + +#[cfg(any(target_os = "linux", test))] +fn appimage_file_ready(path: Option<&Path>) -> bool { + use std::io::Read; + let Some(path) = path else { return false }; + let ordinary_file = std::fs::symlink_metadata(path) + .map(|metadata| metadata.file_type().is_file()) + .unwrap_or(false); + if !path.is_absolute() || !ordinary_file { + return false; + } + let mut header = [0; 20]; + if std::fs::File::open(path) + .and_then(|mut file| file.read_exact(&mut header)) + .is_err() + { + return false; + } + appimage_context_valid(Some(path), ordinary_file, &header) +} + +// Binding APPDIR to the actual executable rejects APPIMAGE/APPDIR inherited +// from an unrelated parent application. The fixed relative path is the verified +// Tauri AppDir layout for this application's configured binary name. +#[cfg(any(target_os = "linux", test))] +fn appdir_matches_executable(appdir: Option<&Path>, executable: Option<&Path>) -> bool { + let (Some(appdir), Some(executable)) = (appdir, executable) else { + return false; + }; + if !appdir.is_absolute() || !executable.is_absolute() { + return false; + } + let (Ok(appdir), Ok(executable)) = (appdir.canonicalize(), executable.canonicalize()) else { + return false; + }; + appdir.is_dir() + && executable.is_file() + && executable.strip_prefix(&appdir).ok() == Some(Path::new("usr/bin/shacraft-launcher")) +} + +#[cfg(target_os = "linux")] +fn appimage_runtime_ready(app: &AppHandle) -> bool { + let environment = app.env(); + let executable = std::env::current_exe().ok(); + appimage_file_ready(environment.appimage.as_deref().map(Path::new)) + && appdir_matches_executable( + environment.appdir.as_deref().map(Path::new), + executable.as_deref(), + ) +} + +fn current_mode(_app: &AppHandle) -> PackageMode { // Bare binaries, distro packages and dev runs must never be overwritten as an AppImage/.app. if cfg!(debug_assertions) { return PackageMode::Manual; } + #[cfg(target_os = "linux")] + if !appimage_runtime_ready(_app) { + return PackageMode::Manual; + } #[cfg(target_os = "macos")] { let app_bundle = std::env::current_exe() @@ -275,12 +348,12 @@ pub(crate) fn check(app: &AppHandle, state: &UpdaterState) -> Result