Skip to content

fix(relinker): convert Windows guest TLS reads after zero alignment padding - #22

Closed
AlxcBTW wants to merge 1 commit into
mainfrom
fix/relinker-windows-tls-reads
Closed

AlxcBTW wants to merge 1 commit into
mainfrom
fix/relinker-windows-tls-reads

Conversation

@AlxcBTW

@AlxcBTW AlxcBTW commented Oct 10, 2026

Copy link
Copy Markdown
Owner

What

On Windows the relinker rewrites FS-relative guest TLS reads, but only for instructions that CodeInstructionCollector decodes. Code that no symbol, unwind range or root reaches is covered by a linear sweep. That sweep starts after int3 padding and stops at the first instruction beginning with two zero bytes, so zero fill is not read as code.

The .NET NativeAOT homebrew titles pad between functions with zero bytes instead of int3, and they have unwind entries for only part of their code. In Prospero Vibrate (PPSA99920) the sweep ended at the padding after the first function. Functions reached only through switch jump tables (which the control-flow graph does not follow) or through unresolved pointers kept 105 of 670 FS loads unpatched: 84 mov reg, fs:[0x28] stack-guard reads and 21 mov rax, fs:[0] thread-pointer reads. The title faulted within a second of start:

FATAL: unhandled exception 0xc0000005 on thread ...
  rip ... app.exe+0x30c7e
  read of 0x0000000000000028

The sweep now treats a zero run that follows a jump, call, return or trap and ends at the next 16-byte boundary as alignment padding, and continues at that boundary. A call counts because a function can end in a call to a noreturn function. Zero fill after straight-line code, or a whole zero 16-byte block, still ends the sweep as before. Every site found still goes through the TLS builder's existing checks, so an unsupported form still fails hard.

Tested

Windows 11, WinLibs GCC 15.2.0 r7, NVIDIA RTX 5070 Laptop.

  • tls_function_coverage gains padding cases. Loads behind 13, 2, 1 and 5 zero bytes after a return, a call, a return and a jump are patched. A load behind zero fill after a nop is left as it was. The new cases fail with the old relinker. zero-filled-after-padding still passes.
  • Unpatched FS loads in the produced app.exe: PPSA99920 105 → 0, Prospero Explorer (PPSA99109) 102 → 0, Prospero Multi Tools (PPSA99110) 103 → 0.
  • All 49 relinkable catalog titles relinked with both relinkers. The other 39 app.exe and all 42 converted guest modules are identical apart from their time stamps.
  • Non-GPU tests: 443/443. Relinker, TLS and decoder tests: 39/39. check_conventions.py passed.
  • Run of PPSA99920: the fault is gone, the title stays up for the full 40 s and draws its UI ("Prospero Vibrate – Haptics that move You"). It needs pthread_sigmask (feat(libkernel): implement pthread_sigmask boykopovar/AnyPS5#2106) and preadv/pwritev/sync (feat(libkernel): export preadv, pwritev and sync #21) to get that far, so those were merged locally for that run only.

Not handled: switch jump tables are still not followed, so code behind other fill patterns could still be missed.

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 in TechnicalDebt
  • No notes, investigation .md files or images added to the repository
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none (the title run additionally used feat(libkernel): implement pthread_sigmask boykopovar/AnyPS5#2106 and feat(libkernel): export preadv, pwritev and sync #21)
  • AI-assisted: yes (Claude Code)

🤖 Generated with Claude Code

…adding

The Windows TLS rewriter only patches FS-relative instructions that
CodeInstructionCollector decodes. Code that no symbol, unwind range or
root reaches is covered by a linear sweep that starts after int3 padding
and stops at the first instruction beginning with two zero bytes, so
zero-filled bytes are not read as code.

The .NET NativeAOT homebrew titles pad between functions with zero bytes
instead of int3 and have unwind entries for only part of their code. In
Prospero Vibrate (PPSA99920) the sweep ended at the padding after the
first function. Functions reached only through switch jump tables, which
the control-flow graph does not follow, or through pointers it cannot
resolve kept 105 of 670 FS loads unpatched: 84 `mov reg, fs:[0x28]`
stack-guard reads (65 into rax, 19 into rcx) and 21 `mov rax, fs:[0]`
thread-pointer reads. The title faulted reading address 0x28 within a
second of start.

The sweep now treats a zero run that follows a jump, call, return or
trap and ends at the next 16-byte boundary as alignment padding, and
continues at that boundary. A call counts because a function can end in
a call to a noreturn function. Zero fill after straight-line code, or a
whole zero 16-byte block, still ends the sweep as before.

All 670 FS loads in PPSA99920 are now patched, as are the 102 of 685 in
Prospero Explorer (PPSA99109) and the 103 of 718 in Prospero Multi Tools
(PPSA99110). New tls_function_coverage padding cases: loads behind zero
padding after a return, a call and a jump, with 13, 2, 1 and 5 zero
bytes, are patched; a load behind zero fill after a nop is left
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AlxcBTW

AlxcBTW commented Oct 10, 2026

Copy link
Copy Markdown
Owner Author

Merged upstream as boykopovar#2821 (fccfbee).

@AlxcBTW AlxcBTW closed this Oct 10, 2026
@AlxcBTW
AlxcBTW deleted the fix/relinker-windows-tls-reads branch October 10, 2026 18:14
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