fix(linux): prevent implicit game downgrades in the builder #2406

Merged
Arkex-ai merged 5 commits from fix/linux-downgrade-protection into main 2026-11-11 20:30:54 +00:00
Arkex-ai commented 2026-11-11 17:35:10 +00:00 (Migrated from github.com)

Summary

On Linux, the game-folder launcher could select an older installed Builder and offer to rebuild a newer game backwards, even offline. This keeps the recorded Builder associated with its game and prevents an implicit downgrade, while preserving confirmed version changes.

Fixes #2352. Claim. Scope coordinated with #2373; this does not redesign its launcher.

Technical changes

  • Prefer the game's recorded Builder; retain installed-Builder and direct-game fallbacks when it is unavailable. No candidate execution or extra interpreter needed to choose one.
  • Default to Play for a known older update target. Confirm older rebuilds at the common update entry, including DLSS changes and direct launcher updates.
  • Carry accepted downgrade consent once across an exact-version handoff, bound to the unchanged game version. A legacy fetched flag alone grants no consent.
  • Require the download list and fetched tools to match the selected target before rebuilding; report release-list lag instead of silently using another version.

Validation

Current tested head 04ea74bb, rebased onto main 34f05785. The worklist conflict preserves main’s damaged-file row and places this package’s row in score order. Focused version-admission tests pass again on Windows and Linux; Windows skips the POSIX executable-permission case, which passes on Linux. No fresh game rebuild or native Linux GUI run is claimed. Fresh hosted checks are separate exact-head evidence.

Earlier validation evidence and limits

Tested 2dcca138 on main 7759a6f3:

  • 14 focused methods pass on Linux; Windows passes the 13 handler/version methods and skips the POSIX-wrapper method. Tests execute the actual shell wrapper, version selector and complete production Tk handlers with authored UI/thread/download boundaries.
  • Original main fails the regression. 11 broken variants rejected on Linux / 10 on Windows, including lost or stale consent, wrong fetched tools, mismatched release lists and bypassed confirmation.
  • Full Linux: 374 passed / 0 failed / 23 skipped, versus exact base373/0/23.
  • Full Windows: 364 passed / 2 failed / 31 skipped, versus exact base363/2/31. The same recorder negative-control and NUP link-privilege failures remain; no new failure.
  • Existing updater30 and launch6 tests pass; shell syntax passes. The screenshot is the actual Tk launcher on Windows using authored version metadata.

No game files, real downloads or builds were used. Native Linux GUI rendering, a real AppImage upgrade and gameplay remain untested. Unknown hashes/differing nightlies retain the existing ordering policy; version labels are not commit-ancestry proof. Behavior and limits.

Tk launcher preview on Windows with authored game v2.2.0 and Builder v2.1.0 metadata

Current-head workflow Result
Game-free source regressions Passed
Linux AppImage Passed
PR title Passed
Windows builder Passed
## Summary On Linux, the game-folder launcher could select an older installed Builder and offer to rebuild a newer game backwards, even offline. This keeps the recorded Builder associated with its game and prevents an implicit downgrade, while preserving confirmed version changes. Fixes #2352. [Claim](https://discord.com/channels/1568810484818378804/1569019786766188665/1570119562655961089). Scope coordinated with #2373; this does not redesign its launcher. ## Technical changes - Prefer the game's recorded Builder; retain installed-Builder and direct-game fallbacks when it is unavailable. No candidate execution or extra interpreter needed to choose one. - Default to Play for a known older update target. Confirm older rebuilds at the common update entry, including DLSS changes and direct launcher updates. - Carry accepted downgrade consent once across an exact-version handoff, bound to the unchanged game version. A legacy fetched flag alone grants no consent. - Require the download list and fetched tools to match the selected target before rebuilding; report release-list lag instead of silently using another version. ## Validation Current tested head **`04ea74bb`**, rebased onto main **`34f05785`**. The worklist conflict preserves main’s damaged-file row and places this package’s row in score order. Focused version-admission tests pass again on Windows and Linux; Windows skips the POSIX executable-permission case, which passes on Linux. No fresh game rebuild or native Linux GUI run is claimed. Fresh [hosted checks](https://git.magicbean.dev/MagicBeanDev/Hookshot/pulls/2406/checks) are separate exact-head evidence. <details> <summary>Earlier validation evidence and limits</summary> Tested **`2dcca138`** on main **`7759a6f3`**: - **14 focused methods pass on Linux**; Windows passes the 13 handler/version methods and skips the POSIX-wrapper method. Tests execute the actual shell wrapper, version selector and complete production Tk handlers with authored UI/thread/download boundaries. - Original main fails the regression. **11 broken variants rejected on Linux / 10 on Windows**, including lost or stale consent, wrong fetched tools, mismatched release lists and bypassed confirmation. - Full Linux: **374 passed / 0 failed / 23 skipped**, versus exact base373/0/23. - Full Windows: **364 passed / 2 failed / 31 skipped**, versus exact base363/2/31. The same recorder negative-control and NUP link-privilege failures remain; no new failure. - Existing updater30 and launch6 tests pass; shell syntax passes. The screenshot is the actual Tk launcher on Windows using authored version metadata. No game files, real downloads or builds were used. Native Linux GUI rendering, a real AppImage upgrade and gameplay remain untested. Unknown hashes/differing nightlies retain the existing ordering policy; version labels are not commit-ancestry proof. [Behavior and limits](docs/linux-builder-version-selection.md). ![Tk launcher preview on Windows with authored game v2.2.0 and Builder v2.1.0 metadata](https://raw.githubusercontent.com/Arkex-ai/Hookshot/2dcca13886cb8f4dd22126fba45c0090d1300fcb/docs/images/linux-builder-newer-game.png) </details> <!-- pr-ci-status --> | Current-head workflow | Result | |---|---| | [Game-free source regressions](https://github.com/NotRexed/Hookshot/actions/runs/43090325870) | Passed | | [Linux AppImage](https://github.com/NotRexed/Hookshot/actions/runs/43090325735) | Passed | | [PR title](https://github.com/NotRexed/Hookshot/actions/runs/43090324841) | Passed | | [Windows builder](https://github.com/NotRexed/Hookshot/actions/runs/43090325627) | Passed | <!-- /pr-ci-status -->
Joey-Magnus (Migrated from github.com) approved these changes 2026-11-11 20:08:22 +00:00
Joey-Magnus (Migrated from github.com) left a comment

This does what #2352 needs, and the consent handling is tight. choose_mode no longer offers an older target as an update. older_ok now guards every rebuild entry point in begin_update, before any page change or download, so the DLSS-change rebuild and the game's own --update path are covered too. The --allow-downgrade-from handoff only counts when the game version, the requested tag and the running tools all match, and it's cleared after one use. The new manifest.tag != target check also closes the case where an API/list lag silently rebuilt a newer target with the running, older tools. CI is green on 04ea74bb, and test_linux_downgrade, test_linux_update, test_linux_update_skip and test_hookshot_launch pass locally. older_version returns False for anything release_key can't order (hashes, None), so nightlies and unversioned builds keep today's behaviour.

question builder/linux/hookshot-folder:30-33: flipping the order to prefer builder= fixes the "older installed Builder opens a newer game" case, but it makes the reverse case worse. Take a folder built from an AppImage that is still on disk (say ~/Applications/Hookshot-2.0.AppImage), while a newer Builder has since been installed under $XDG_DATA_HOME/hookshot/builder by another folder's update. Today that folder opens the newer installed Builder, which can rebuild it offline with the tools it already has. After this PR it opens the older AppImage. Offline, that one only knows its own version, so it shows Play. Online, it goes back through linux_update.install for a version that is already installed. The downgrade guard in choose_mode already stops the backwards rebuild whichever Builder opens, so would it be enough to pick the newer of the two? The installed one's tag is in $root/current and the recorded one built the game named by version=, so you can compare them without running either. A sort -V in the wrapper would do, and that keeps the no-interpreter property you wanted. If you'd rather keep the simple rule, please mention the offline trade-off in docs/linux-builder-version-selection.md.

minor: the new page and its 45 KB screenshot (docs/images/linux-builder-newer-game.png, a Windows rendering of the Linux Tk launcher with made-up versions) document one launcher behaviour. A short paragraph in the existing Linux section of docs/development.md or the readme, with the screenshot kept in the PR description, would be easier to keep current. Your call, not blocking.

This does what #2352 needs, and the consent handling is tight. `choose_mode` no longer offers an older target as an update. `older_ok` now guards every rebuild entry point in `begin_update`, before any page change or download, so the DLSS-change rebuild and the game's own `--update` path are covered too. The `--allow-downgrade-from` handoff only counts when the game version, the requested tag and the running tools all match, and it's cleared after one use. The new `manifest.tag != target` check also closes the case where an API/list lag silently rebuilt a newer target with the running, older tools. CI is green on `04ea74bb`, and `test_linux_downgrade`, `test_linux_update`, `test_linux_update_skip` and `test_hookshot_launch` pass locally. `older_version` returns False for anything `release_key` can't order (hashes, `None`), so nightlies and unversioned builds keep today's behaviour. **question** `builder/linux/hookshot-folder:30-33`: flipping the order to prefer `builder=` fixes the "older installed Builder opens a newer game" case, but it makes the reverse case worse. Take a folder built from an AppImage that is still on disk (say `~/Applications/Hookshot-2.0.AppImage`), while a newer Builder has since been installed under `$XDG_DATA_HOME/hookshot/builder` by another folder's update. Today that folder opens the newer installed Builder, which can rebuild it offline with the tools it already has. After this PR it opens the older AppImage. Offline, that one only knows its own version, so it shows Play. Online, it goes back through `linux_update.install` for a version that is already installed. The downgrade guard in `choose_mode` already stops the backwards rebuild whichever Builder opens, so would it be enough to pick the *newer* of the two? The installed one's tag is in `$root/current` and the recorded one built the game named by `version=`, so you can compare them without running either. A `sort -V` in the wrapper would do, and that keeps the no-interpreter property you wanted. If you'd rather keep the simple rule, please mention the offline trade-off in `docs/linux-builder-version-selection.md`. **minor**: the new page and its 45 KB screenshot (`docs/images/linux-builder-newer-game.png`, a Windows rendering of the Linux Tk launcher with made-up versions) document one launcher behaviour. A short paragraph in the existing Linux section of `docs/development.md` or the readme, with the screenshot kept in the PR description, would be easier to keep current. Your call, not blocking.
Arkex-ai commented 2026-11-11 20:38:03 +00:00 (Migrated from github.com)

Thanks for the review and merge. The reverse case is a real trade-off of the accepted recorded-Builder priority: an older AppImage can offer only Play offline even when a newer Builder is installed separately, and online it may fetch tools already installed there. Opening the installed Builder and selecting the game explicitly remains available; the downgrade guard still applies whichever Builder opens it.

I would keep a newest-candidate policy coordinated with #2373, with stable/pre-release/nightly and unorderable versions decided explicitly. This PR protects rebuild admission; it does not automatically choose the newest available Builder.

Thanks for the review and merge. The reverse case is a real trade-off of the accepted recorded-Builder priority: an older AppImage can offer only Play offline even when a newer Builder is installed separately, and online it may fetch tools already installed there. Opening the installed Builder and selecting the game explicitly remains available; the downgrade guard still applies whichever Builder opens it. I would keep a newest-candidate policy coordinated with #2373, with stable/pre-release/nightly and unorderable versions decided explicitly. This PR protects rebuild admission; it does not automatically choose the newest available Builder.
Commenting is not possible because the repository is archived.
No description provided.