fix(build): a game image past 2 GB fails the link instead of running broken #356

Open
madebyjordan wants to merge 3 commits from madebyjordan/Longshot:fix/image-guard into main
Contributor

Why

If the game's linked image ever reaches 2 GB, the game reads the wrong memory and the linker doesn't say so. The build should stop with a plain message instead of handing the player a broken game. Today's image is 588 MB, so nothing changes now; it matters as soon as someone builds more code, or code at a lower optimisation level.

What changed

  • build: image_span() and image_too_big() in nx2/build.py read the linked image's span: a PE's SizeOfImage, or an ELF's PT_LOAD range from the lowest address to the highest end. At 2 GB or more, the build prints link FAILED: the game's image is N MB, past the 2048 MB its code can address ..., deletes the image so nothing can start it, and exits 1.
  • build: main() checks after each of the three links of the lifted code: Linux longshot, Windows longshot.exe, and --dll's longshot_game.dll.
  • tests: tests/test_image_size_guard.py covers:
    • made-up PE and ELF headers on both sides of the limit;
    • an image at or past it failing and being deleted;
    • one under it kept;
    • a file that is neither left alone;
    • every link of the lifted code in main() followed by the check;
    • small programs linked with clang and lld for Windows and Linux reading as small images.

Why the linker misses it: the game's code reaches its data RIP-relative, through a signed 32-bit offset. lld doesn't range-check those fixups on x86-64 COFF. I found this compiling the lifted code at -O0 for a test: lld linked a 2,262 MB longshot.exe without an error. .data started past 2^31, and the first read of a64_dirty (movq -0x7d457b8e(%rip)) landed 4 GB below .refptr.a64_dirty. On Linux, lld does range-check these fixups and fails the link itself, so there the guard is a second check; the gap it closes is Windows'.

Flags and settings

None.

Measurements

A change to the build scripts that the game doesn't run, so it's measured by its game-free checks.

main: cf605c85 (2026-11-14)
this PR: fix/image-guard @ 53cbc23c
Hardware: Ryzen 5 5600, 16 GB, Windows 11, llvm-mingw clang 23 (nightly-34's builder tools)
Method: tests/test_image_size_guard.py, plus python -I -B tests/run_all.py --jobs 4 on main and on this PR merged with the other four build PRs of this batch.

main @ cf605c85 this PR @ 53cbc23c
tests/test_image_size_guard.py fails (no image_span) passes, 9 tests
the same test with the --dll check taken out n/a fails
tests/run_all.py --jobs 4 483 passed, 6 failed, 70 skipped 486 passed, 6 failed (the same 6 as main), 70 skipped
nightly-34's bin\longshot.exe, image_span n/a 588 MB: passes

Testing

Tested:

  • The guard's checks above, on Windows with llvm-mingw clang 23 and lld, including real links for both the Windows and Linux targets.
  • image_span on nightly-34's bin\longshot.exe (588 MB).
  • The 2,262 MB -O0 image from the experiment above, which is the case that fails.

Not tested:

  • A whole build through make_game.py. The check runs only after a link and changes nothing under 2 GB, so a normal build is unchanged.
  • A real Linux build. It has the same check after its link, behind lld's own range check.

Needs other testers: None.

Screenshots

Nothing on screen.

Risk and rollback

A wrong reading of a valid image would stop a build that would have worked. The span is read straight from the image's headers, and the test checks it against real links from clang and lld. Reverting the commit removes the check.

Reviewer notes

image_too_big deletes the image it refuses, so a broken game can't be started from the build folder by hand.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
## Why If the game's linked image ever reaches 2 GB, the game reads the wrong memory and the linker doesn't say so. The build should stop with a plain message instead of handing the player a broken game. Today's image is 588 MB, so nothing changes now; it matters as soon as someone builds more code, or code at a lower optimisation level. ## What changed - build: `image_span()` and `image_too_big()` in `nx2/build.py` read the linked image's span: a PE's `SizeOfImage`, or an ELF's `PT_LOAD` range from the lowest address to the highest end. At 2 GB or more, the build prints `link FAILED: the game's image is N MB, past the 2048 MB its code can address ...`, deletes the image so nothing can start it, and exits 1. - build: `main()` checks after each of the three links of the lifted code: Linux `longshot`, Windows `longshot.exe`, and `--dll`'s `longshot_game.dll`. - tests: `tests/test_image_size_guard.py` covers: - made-up PE and ELF headers on both sides of the limit; - an image at or past it failing and being deleted; - one under it kept; - a file that is neither left alone; - every link of the lifted code in `main()` followed by the check; - small programs linked with clang and lld for Windows and Linux reading as small images. Why the linker misses it: the game's code reaches its data RIP-relative, through a signed 32-bit offset. lld doesn't range-check those fixups on x86-64 COFF. I found this compiling the lifted code at `-O0` for a test: lld linked a 2,262 MB `longshot.exe` without an error. `.data` started past 2^31, and the first read of `a64_dirty` (`movq -0x7d457b8e(%rip)`) landed 4 GB below `.refptr.a64_dirty`. On Linux, lld does range-check these fixups and fails the link itself, so there the guard is a second check; the gap it closes is Windows'. ## Flags and settings None. ## Measurements A change to the build scripts that the game doesn't run, so it's measured by its game-free checks. main: cf605c85 (2026-11-14) this PR: fix/image-guard @ 53cbc23c Hardware: Ryzen 5 5600, 16 GB, Windows 11, llvm-mingw clang 23 (nightly-34's builder tools) Method: `tests/test_image_size_guard.py`, plus `python -I -B tests/run_all.py --jobs 4` on main and on this PR merged with the other four build PRs of this batch. | | main @ cf605c85 | this PR @ 53cbc23c | |---|---|---| | `tests/test_image_size_guard.py` | fails (no `image_span`) | passes, 9 tests | | the same test with the `--dll` check taken out | n/a | fails | | `tests/run_all.py --jobs 4` | 483 passed, 6 failed, 70 skipped | 486 passed, 6 failed (the same 6 as main), 70 skipped | | nightly-34's `bin\longshot.exe`, `image_span` | n/a | 588 MB: passes | ## Testing **Tested:** - The guard's checks above, on Windows with llvm-mingw clang 23 and lld, including real links for both the Windows and Linux targets. - `image_span` on nightly-34's `bin\longshot.exe` (588 MB). - The 2,262 MB `-O0` image from the experiment above, which is the case that fails. **Not tested:** - A whole build through `make_game.py`. The check runs only after a link and changes nothing under 2 GB, so a normal build is unchanged. - A real Linux build. It has the same check after its link, behind lld's own range check. **Needs other testers:** None. ## Screenshots Nothing on screen. ## Risk and rollback A wrong reading of a valid image would stop a build that would have worked. The span is read straight from the image's headers, and the test checks it against real links from clang and lld. Reverting the commit removes the check. ## Reviewer notes `image_too_big` deletes the image it refuses, so a broken game can't be started from the build folder by hand. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened.
The game's code reaches its data RIP-relative, a signed 32-bit offset, and lld does not range-check those fixups on
x86-64 COFF. Lifted code compiled at -O0 linked a 2,262 MB longshot.exe with no error; .data started past 2^31 and the
first read of a64_dirty landed 4 GB below it. Any change that grows the image (a lower optimisation level for some
code, a bigger lift, a new code tier) could ship that silently.

After each game link, build.py reads the image's span (a PE's SizeOfImage, an ELF's PT_LOAD range) and fails the
build with a plain message, deleting the image, when it reaches 2 GB. nightly-34's longshot.exe is 588 MB, so nothing
changes for today's builds.

tests/test_image_size_guard.py: PE and ELF headers either side of the limit, a file that is neither, and small
programs linked for Windows and Linux with clang + lld.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
longshot_game.dll holds the same lifted code as longshot.exe and reaches its data the same way, so a 2 GB image breaks
it the same way. tests/test_image_size_guard.py now checks that every link of the lifted code in build.py's main()
(Linux, Windows, --dll) is followed by image_too_big.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs(build): the image guard is only a second check on ELF
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 3s
Website / Website on Cloudflare (pull_request_target) Has been skipped
53cbc23cb8
lld range-checks x86-64 PC-relative fixups on ELF and fails the link itself; the gap the guard closes is COFF's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
madebyjordan changed title from WIP: fix(build): a game image past 2 GB fails the link instead of running broken to fix(build): a game image past 2 GB fails the link instead of running broken 2026-11-14 04:22:48 +00:00
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 3s
Website / Website on Cloudflare (pull_request_target) Has been skipped
This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'fix/image-guard:madebyjordan-fix/image-guard'
git switch 'madebyjordan-fix/image-guard'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!356
No description provided.