Skip to content

ntdll iOS: fill missing control registers in a cross-thread GetThread… - #160

Merged
willfaust merged 1 commit into
willfaust:mainfrom
bahacan16:pr/getthreadcontext-control-regs
Oct 4, 2026
Merged

willfaust merged 1 commit into
willfaust:mainfrom
bahacan16:pr/getthreadcontext-control-regs

Conversation

@bahacan16

Copy link
Copy Markdown
Contributor

…Context

Problem

Ghost of Tsushima froze on a loading screen. The log showed RtlpWaitForCriticalSection ... blocked by <tid>: the blocking thread was calling GetThreadContext on another thread thousands of times, every answer rip=0 rsp=0 flags=00100002 (no CONTEXT_CONTROL), while it held a critical section the main thread was waiting for.

Cause

For a cross-thread NtGetContextThread the server sometimes has no native capture of the target (have_native = 0) and answers with the integer registers only. The ARM64EC wrapper then hands the x64 caller a context with Rip = Rsp = 0 and no CONTEXT_CONTROL, and a caller that samples another thread's instruction pointer (profilers, job systems, watchdogs) keeps retrying.

Change

build/ntdll-unix/signal_arm64_ios.c: when the server reply lacks the control registers (or has Pc = 0) and the caller asked for them, get_context_from_amd64_area fills them -- and the integer registers, if requested -- from the target's last saved x64 state, TEB->ChpeV2CpuAreaInfo->ContextAmd64 (written whenever the thread leaves emulated code). The values are returned in the ARM64EC register mapping, so the wrapper's context_arm_to_x64() turns them back into x64 registers; X23/X28 are cleared so the wrapper does not substitute an emulator frame's Pc/Sp. Only threads of the calling process are handled (the TEB pointer is read in this address space); nothing changes when the server's answer is complete. The first 8 uses log [ctx] get handle=....

Evidence

Ghost of Tsushima, iPhone 17 Pro Max, iOS 27: the frozen loading screen's log had the blocked critical section and the stream of rip=0 rsp=0 contexts described above; the change shipped in the fork after that log.

Notes / risks

  • Not confirmed on device. No later log recorded the freeze again, but none recorded the [ctx] line either, so whether this was the fix is unproven. Treat it as a correctness improvement: a stale but real Rip/Rsp instead of zeros.
  • The saved state is the thread's last exit from emulated code, not a suspended snapshot; a running thread's registers may have moved on (Windows callers are expected to suspend first).
  • If ContextAmd64 is empty or Pc/Sp are 0, the server's answer is returned unchanged (still without CONTEXT_CONTROL).
  • Split out of the D3D12 tessellation topic; unrelated to it.

🤖 Generated with Claude Code

Claude-Session: https://claude.ai/code/session_0189oLHghpaYKLk4f786a6bc

…Context

## Problem

Ghost of Tsushima froze on a loading screen. The log showed
`RtlpWaitForCriticalSection ... blocked by <tid>`: the blocking thread was
calling `GetThreadContext` on another thread thousands of times, every answer
`rip=0 rsp=0 flags=00100002` (no `CONTEXT_CONTROL`), while it held a critical
section the main thread was waiting for.

## Cause

For a cross-thread `NtGetContextThread` the server sometimes has no native
capture of the target (`have_native = 0`) and answers with the integer
registers only. The ARM64EC wrapper then hands the x64 caller a context with
`Rip = Rsp = 0` and no `CONTEXT_CONTROL`, and a caller that samples another
thread's instruction pointer (profilers, job systems, watchdogs) keeps
retrying.

## Change

`build/ntdll-unix/signal_arm64_ios.c`: when the server reply lacks the
control registers (or has `Pc = 0`) and the caller asked for them,
`get_context_from_amd64_area` fills them -- and the integer registers, if
requested -- from the target's last saved x64 state,
`TEB->ChpeV2CpuAreaInfo->ContextAmd64` (written whenever the thread leaves
emulated code). The values are returned in the ARM64EC register mapping, so
the wrapper's `context_arm_to_x64()` turns them back into x64 registers;
X23/X28 are cleared so the wrapper does not substitute an emulator frame's
Pc/Sp. Only threads of the calling process are handled (the TEB pointer is
read in this address space); nothing changes when the server's answer is
complete. The first 8 uses log `[ctx] get handle=...`.

## Evidence

Ghost of Tsushima, iPhone 17 Pro Max, iOS 27: the frozen loading screen's log
had the blocked critical section and the stream of `rip=0 rsp=0` contexts
described above; the change shipped in the fork after that log.

## Notes / risks

- **Not confirmed on device.** No later log recorded the freeze again, but
  none recorded the `[ctx]` line either, so whether this was the fix is
  unproven. Treat it as a correctness improvement: a stale but real
  Rip/Rsp instead of zeros.
- The saved state is the thread's last exit from emulated code, not a
  suspended snapshot; a running thread's registers may have moved on (Windows
  callers are expected to suspend first).
- If `ContextAmd64` is empty or `Pc`/`Sp` are 0, the server's answer is
  returned unchanged (still without `CONTEXT_CONTROL`).
- Split out of the D3D12 tessellation topic; unrelated to it.

---
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0189oLHghpaYKLk4f786a6bc
Signed-off-by: bahacan16 <190844990+bahacan16@users.noreply.github.com>
willfaust pushed a commit that referenced this pull request Oct 4, 2026
…Context (#160)

Squashed from #160.

Signed-off-by: bahacan16 <190844990+bahacan16@users.noreply.github.com>
@willfaust
willfaust merged commit 6b79d56 into willfaust:main Oct 4, 2026
willfaust added a commit that referenced this pull request Oct 4, 2026
…ol-only

#160 fills a cross-thread GetThreadContext's missing control registers from
the target's saved x64 state. Now:
- opt-in, MADEIRA_CTX_CPU_AREA=1 (madeira.cfg or a game's own config): the
  state is the thread's last exit from emulated code, and a caller that
  writes the context back (a .NET runtime redirecting a thread) would move
  the thread back there;
- the TEB, CPU area and saved context are read with mach_vm_read_overwrite,
  so a target that exits meanwhile cannot fault the caller;
- only Pc, Sp, Fp and Lr are filled; the integer registers stay the
  server's, and Cpsr is left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

3 participants