Skip to content

fix(relinker): refuse GS traps that overwrite a code entry point in Windows output - #11090

Merged
oneandonlydean merged 3 commits into
boykopovar:mainfrom
roboterspinne:fix/relinker-windows-gs-trap-entry-roots
Oct 27, 2026
Merged

oneandonlydean merged 3 commits into
boykopovar:mainfrom
roboterspinne:fix/relinker-windows-gs-trap-entry-roots

Conversation

@roboterspinne

@roboterspinne roboterspinne commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

What

--windows replaces a guest GS memory instruction with UD2 and NOPs and refuses only a relative branch into it (#10710). On untouched 4017310, 65 48 8b 04 25 28 00 00 00 c3 at 0x210 with an entry root at 0x214 (a STT_FUNC symbol, an R_X86_64_RELATIVE pointer, a REX.W lea target or a DT_INIT_ARRAY entry) converted with exit 0, and the PE held 0f 0b 90 90 90 90 90 90 90 c3 at RVA 0x10210, so the entry at 0x214 ran NOPs and ret instead of and eax,0x28; ret, which Linux output keeps. CodeInstructionCollector does not refuse this layout: its overlap rule fires only when the overlapped span holds a 0x64 byte or the previous instruction has an FS prefix, and the same layout with 64 already fails with Code analysis: overlapping instruction boundaries.

  • Rule. WindowsTlsBuilder::Build records for each trapped GS memory instruction [start, start + length) the first collector entry root with start < root < start + length, found with the same entryRoots.upper_bound lookup that TLS rewrites use (fix(relinker): protect Windows TLS code entry roots #5890). In the trap loop, after the existing Branch enters a guest GS instruction check, such a root throws GS trap would overwrite an executable entry point at the root's file offset; the CLI exits 2 and writes no output. entryRoots also contains relative branch targets, so the branch check stays first and keeps its message and offset. A root at the instruction start is still trapped as before. entryRoots also holds pointer targets the collector takes as data, so such a target inside a trapped instruction is refused too, as TLS rewrites already do. checkProtectedEntry is not reused because its message names a TLS rewrite. Windows only; the macOS counterpart in MacOsImage.cpp is a follow-up PR, because ScanTls collects without entryRoots on the line fix(relinker): analyze referenced code beyond SCE scan boundaries #10861 rewrites. A new entry in docs/dev/debt/relinker.md, before the null-symbol relocation entry, states both refusals, that the messages give the position of the entered byte, that no option skips the checks, and that Linux output keeps the instruction with libkernel's sysarch setting the GS base; the Windows GS entry and the macOS GS entry are unchanged.
  • Tests. New test_gs_trap_entry_roots.py, built on make_image and pe_bytes_at from test_windows_tls_protected_entries.py, with the GS load at 0x210: roots relative, symbol, lea and initializer must fail with the message, (offset 0x214) and no output; controls safe and access-start must convert with 0f 0b 90 90 90 90 90 90 90 c3 at RVA 0x10210. Registered in core/relinker/CMakeLists.txt after windows_tls_protected_entries.
  • Overlap. Checked by hand on main f95acfc: 32 other open pull requests touch core/relinker/CMakeLists.txt or WindowsTlsBuilder.cpp. All of them except feat(aot): consolidate native ARM64 compiler, runtime and consumers #5021, fix(libc): use the default heap for an executable without process parameters #6713 and fix(relinker): analyze referenced code beyond SCE scan boundaries #10861 merge with main and with this branch without conflicts. Those three conflict with main, none of them in lines of this PR. feat(aot): consolidate native ARM64 compiler, runtime and consumers #5021 adds macos_fixtures after macos_output, more than 100 lines below gs_trap_entry_roots (line 554, after windows_tls_protected_entries). fix(libc): use the default heap for an executable without process parameters #6713 adds windows_process_parameters after elf_header and conflicts with main only in docs/dev/debt/libc.md. fix(relinker): analyze referenced code beyond SCE scan boundaries #10861 changes core/relinker/CMakeLists.txt at its lines 248 and 774. In WindowsTlsBuilder.cpp it changes the signature of Build and the Collect call three lines below the gsTraps declaration that this PR changes. It conflicts with main in GuestModuleBuilder.cpp, RelinkerPipeline.cpp and docs/dev/debt/relinker.md. Its extraEntries become collector entry roots, so after both merge, a GS trap that holds one after its first byte is refused as well. docs/dev/debt/relinker.md is touched by 27 open pull requests. With the entry inserted before the null-symbol entry, git merge-tree --write-tree against each of them from its merge base with this branch reports the same conflicting files as without the entry, and the existing conflict hunk of fix(relinker): analyze referenced code beyond SCE scan boundaries #10861 in that file is unchanged; an extension of the Windows GS entry in place would have conflicted with fix(relinker): reject an executable PT_LOAD alignment that is not a power of two #10518, which inserts an entry directly before it.

Tested

Windows 11, GCC 15.2.0 (WinLibs UCRT), relinker-only Release build (-DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON); synthetic fixtures only, no title or homebrew, relinked outputs not run.

  • Counter-check (run on 4017310, the previous base; rebased onto 3a2c51a with no relinker changes since): gs_trap_entry_roots on untouched 4017310 fails with AssertionError: ('relative', 0, ''); run case by case, all four root inputs exited 0 with an output file and 0f 0b 90 90 90 90 90 90 90 c3 at RVA 0x10210, and both controls passed. With the change all four exit 2 with FAIL: GS trap would overwrite an executable entry point (offset 0x214) and no output, the controls are unchanged, and the test passes in 0.4 s.
  • On 3a2c51a with the change: ctest -R 'gs_trap|tls_function_coverage|windows_tls|windows_gs' -j 4: 38 tests passed in 26.4 s, among them gs_trap_entry_roots (0.8 s), windows_tls_protected_entries, windows_tls_zero_fill, windows_tls_thread_pointer, tls_function_coverage_gs_traps and tls_function_coverage_gs_traps_implicit. macos_gs_traps also passed.
  • Merged current main (f95acfc) into the branch without conflicts (66f6fa3); since 3a2c51a, main changed WindowsTlsBuilder.cpp only through fix(relinker): accept empty TLS images beyond EOF on Windows #11000 (an empty-TLS check after the trap loop) and core/relinker/CMakeLists.txt away from this PR's registration. On 66f6fa3, ctest -R 'gs_trap|tls_function_coverage|windows_tls|windows_gs' -j 6: 39 tests passed, among them gs_trap_entry_roots (1.3 s), windows_tls_protected_entries, windows_tls_zero_fill, windows_tls_thread_pointer, macos_gs_traps, tls_function_coverage_gs_traps and tls_function_coverage_gs_traps_implicit.
  • Full suite on 66f6fa3: ctest --test-dir build-relinker -j 6: 311 tests, 308 passed, long_paths skipped; failures known on this machine on main as well: select_tests (Windows symlink fixture) and patch_advisor_unit (path with spaces, which open test(tools): compare quoted and resolved paths in the patch advisor test #10667 fixes); both failed again alone.
  • The debt entry is a separate commit (823251d) that changes only docs/dev/debt/relinker.md; the test runs above are on 66f6fa3, whose code and tests are the same. python tools/check_conventions.py --base upstream/main with this title and description passed on 823251d.
  • Not tested on Linux or macOS; CI runs the tests on all three hosts.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes (Claude Code)

…indows output

--windows replaced a guest GS memory instruction with UD2 and NOPs even
when a symbol, a relocation pointer, a lea target or a DT_INIT_ARRAY
entry started inside it, so that entry ran padding instead of the
original code. A trapped instruction that holds a collector entry root
after its first byte now fails with "GS trap would overwrite an
executable entry point" at the root's file offset, the rule that TLS
rewrites already follow. The branch check runs first, so a relative
branch into the instruction keeps its message, and outputs without such
a root are unchanged.
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 5021 core/relinker/CMakeLists.txt
docs/dev/debt/relinker.md
PR 6713 core/relinker/CMakeLists.txt
PR 11366 core/relinker/CMakeLists.txt

@MarcZX10

Copy link
Copy Markdown
Contributor

I ran gs_trap_entry_roots locally from the exact PR head 33b48ba39f0155d466ae028e6d838a8611e83bb1; it passed 1/1. The hosted macOS x86_64 relinker job completed with 1 failure out of 289: masked_store_lowering reported Masked store stub changed the flags. The PR does not change shader/compiler sources, so this appears unrelated to the GS-trap change, but the required matrix is not fully green. Please check whether it reproduces on the PR base or rerun the job, and record the result before treating CI as passing.

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed exact head 33b48ba39f0155d466ae028e6d838a8611e83bb1. The Windows GS trap now checks the next executable entry root strictly inside the instruction span before replacing the bytes, while preserving the earlier branch-target error path. The fixture covers relocation, symbol, LEA and initializer roots, plus an entry at the instruction start and a safe trap. I ran gs_trap_entry_roots and windows_tls_protected_entries locally (2/2 pass), git diff --check upstream/main...HEAD, and tools/check_conventions.py --base upstream/main --head HEAD (pass). Hosted Linux, Windows, macOS/arm64 and ASan relinker checks pass. Hosted macOS/x86_64 reports one failure in unrelated masked_store_lowering (“Masked store stub changed the flags”); the PR’s new and adjacent tests passed there. I have not run a macOS build locally.

@roboterspinne

Copy link
Copy Markdown
Contributor Author

@MarcZX10 Thanks. masked_store_lowering is untouched by this PR, which only changes WindowsTlsBuilder.cpp, adds test_gs_trap_entry_roots.py and registers it in CMakeLists.txt; that test runs the --to-rosetta masked-store stubs natively and checks the flags after a deliberate fault on a protected page. It does not reproduce on this PR's base: #11082 and #11084 are on the same base (3a2c51a), and their relinker (macos-latest, clang++, x86_64) jobs passed at the same time (runs 40215713269 and 40215895505). Across the Build runs since 2026-10-24, the 15 failed jobs of that kind failed in other tests, and run 40216485859 here is the only one where masked_store_lowering failed. I cannot re-run the job from the fork; a re-run of that job or the next push to this branch will show it.

@klosixgaming-arch klosixgaming-arch left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed head 33b48ba39f by reading the diff and the existing review thread. I did not build or run this, so everything below is from the code, not from a test of mine.

What it does. The Windows TLS builder used to replace a guest GS-prefixed memory instruction with a trap (ud2 plus padding) without checking whether an executable entry point lay inside the bytes it overwrote. Now, if an entry root falls strictly inside the instruction (after its first byte), conversion fails with "GS trap would overwrite an executable entry point" and the offset; a root exactly at the start of the instruction is still fine.

Checked: entryRoots.upper_bound(*instruction) gives the first root after the instruction start, and *entry - *instruction < info.Length limits it to inside the instruction, so a root at the start is excluded by upper_bound and one at start + Length is excluded by the comparison. The file offset is computed from the section header the same way as the neighbouring code. The new gs_trap_entry_roots test covers four ways an entry root can land there (relative, symbol, LEA, initializer) and two cases that must still convert (a safe image and an entry root at the instruction start), and it checks the trapped bytes.

Notes:

  • This turns a silent corruption into a hard conversion failure. That is the right call, but it also means an image that used to convert (and then misbehave) will now be refused with no workaround. A debt entry or a line in the relinker docs saying what to do when you hit it would help the next user.
  • The hosted macOS x86_64 job reported one failure, masked_store_lowering. The author says the PR does not touch it, and the diff agrees (only WindowsTlsBuilder.cpp, a new test and its registration). MarcZX10 ran the new test locally from the head. I'd still re-run that job once to confirm it was a flake, because a "pre-existing failure" is easy to claim and cheap to check.

Approving.

AI-assisted: yes. Written with AI assistance and reviewed by me.

@roboterspinne

Copy link
Copy Markdown
Contributor Author

@klosixgaming-arch Merged current main f95acfc into the branch (66f6fa3, no conflicts), which also re-ran the hosted matrix, including the macOS x86_64 job. Since the base, main changed WindowsTlsBuilder.cpp only through #11000, an empty-TLS check after the trap loop, so there is no interaction, and since #9719 GS-prefixed lea and nop are no longer trapped, so the new check does not apply to them. gs_trap_entry_roots is still registered directly after windows_tls_protected_entries. On 66f6fa3, ctest -R 'gs_trap|tls_function_coverage|windows_tls|windows_gs' passed 39/39 and the full suite 308 of 311, with long_paths skipped and the same two known local failures as on main.

A Windows conversion now fails when a code entry lies after the first
byte of a guest GS instruction that the TLS builder replaces with UD2.
The new debt entry names both messages, says that no option skips the
checks and that Linux output keeps the instruction, so a user who hits
the refusal knows which target to use.
@roboterspinne

Copy link
Copy Markdown
Contributor Author

@klosixgaming-arch Thanks for the review. Merged current main f95acfc into the branch (66f6fa3, no conflicts), which also re-ran the hosted matrix, including the macOS x86_64 job. Since the base, main changed WindowsTlsBuilder.cpp only through #11000, an empty-TLS check after the trap loop, so there is no interaction, and since #9719 GS-prefixed lea and nop are no longer trapped, so the new check does not apply to them. On 66f6fa3, ctest -R 'gs_trap|tls_function_coverage|windows_tls|windows_gs' passed 39/39 and the full suite 308 of 311, with long_paths skipped and the same two known local failures as on main.
On your debt point: 823251d adds an entry to docs/dev/debt/relinker.md, before the null-symbol relocation entry. It names both refusals (Branch enters a guest GS instruction, GS trap would overwrite an executable entry point), says that the messages give the position of the entered byte and that no option skips the checks, and that Linux output keeps the instruction, where libkernel's sysarch sets the GS base. I placed it as a separate entry rather than extending the Windows GS entry because #10518 inserts a line directly before that entry and an in-place extension would conflict with it. The macOS writer still traps GS instructions without this check; that is the planned follow-up PR, so the macOS entry is unchanged here.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants