fix(host): HOS_SAMPLE_ALL can crash the game: the sampler's stack walk (RtlVirtualUnwind) is unguarded #383

Open
opened 2026-11-14 07:16:54 +00:00 by madebyjordan · 0 comments
Contributor

What happens

With HOS_PROFILE=5 HOS_SAMPLE_ALL=<file> (and <file>.go present), the game can die with an access violation in the
sampler's own thread. Seen once in 8 headless 300 s runs of nightly-34 (Windows 11, Ryzen 5 5600, RX 6700 XT), 43 s in,
at the title screen:

[hos host] CRASH: exception 0xc0000005 (read 0x6e0ca01098, guest 0xfffff06e0ca01098) at ntdll.dll+0x20191
[hos host] CRASH: host backtrace: ntdll.dll+0x20191 ntdll.dll+0x1f549 ntdll.dll+0x213cb KERNEL32.DLL+0x281c8 longshot.exe+0x18cb50ae KERNEL32.DLL+0x2dda7 ntdll.dll+0xc9c5c

longshot.exe+0x18cb50ae is sample_all_thread+0xa9e (from the build's longshot.map); the frames above it are the
unwinder. The other 7 runs, same build and route, went the full 300 s.

Why

sample_all_thread in host/mkw_glue.c suspends each guest thread, takes its context and walks up to 16 frames with
RtlLookupFunctionEntry + RtlVirtualUnwind, with nothing around it. The unwinder reads the target thread's stack
(saved registers, return addresses). If a step goes wrong (a thread stopped where its unwind data does not describe
the frame, a stack being switched, a stale context), it reads an address that is not mapped, here
0x6e0ca01098, and the access violation is raised in the sampler's thread, which takes the process down.

Suggested fix

Make the walk unable to fault:

  • check each step's RSP (and the address the unwind reads from) against the target thread's stack limits (its TEB's
    StackBase / StackLimit, via NtQueryInformationThread once per thread) and stop the walk when it leaves them; and/or
  • run the walk under a guard (__try/__except with clang's SEH support, or a vectored handler that recognises a fault
    in the sampler thread during a walk and ends that sample).
    Either way the sample is just cut short; nothing else changes.

Notes

  • Development tool only (HOS_SAMPLE_ALL is off unless set); players never run it.
  • Found while measuring the giant-function split (local branch bold/split): no split code was on any sampled stack in
    that run, and the same crash can happen in any build.
  • The same unguarded RtlLookupFunctionEntry + RtlVirtualUnwind walk is in two more places in host/mkw_glue.c on main 0caf0618 (around lines 149 and 388, the other samplers); a shared guarded helper would cover all three.
## What happens With `HOS_PROFILE=5 HOS_SAMPLE_ALL=<file>` (and `<file>.go` present), the game can die with an access violation in the sampler's own thread. Seen once in 8 headless 300 s runs of nightly-34 (Windows 11, Ryzen 5 5600, RX 6700 XT), 43 s in, at the title screen: ``` [hos host] CRASH: exception 0xc0000005 (read 0x6e0ca01098, guest 0xfffff06e0ca01098) at ntdll.dll+0x20191 [hos host] CRASH: host backtrace: ntdll.dll+0x20191 ntdll.dll+0x1f549 ntdll.dll+0x213cb KERNEL32.DLL+0x281c8 longshot.exe+0x18cb50ae KERNEL32.DLL+0x2dda7 ntdll.dll+0xc9c5c ``` `longshot.exe+0x18cb50ae` is `sample_all_thread+0xa9e` (from the build's longshot.map); the frames above it are the unwinder. The other 7 runs, same build and route, went the full 300 s. ## Why `sample_all_thread` in `host/mkw_glue.c` suspends each guest thread, takes its context and walks up to 16 frames with `RtlLookupFunctionEntry` + `RtlVirtualUnwind`, with nothing around it. The unwinder reads the target thread's stack (saved registers, return addresses). If a step goes wrong (a thread stopped where its unwind data does not describe the frame, a stack being switched, a stale context), it reads an address that is not mapped, here `0x6e0ca01098`, and the access violation is raised in the sampler's thread, which takes the process down. ## Suggested fix Make the walk unable to fault: - check each step's RSP (and the address the unwind reads from) against the target thread's stack limits (its TEB's StackBase / StackLimit, via `NtQueryInformationThread` once per thread) and stop the walk when it leaves them; and/or - run the walk under a guard (`__try`/`__except` with clang's SEH support, or a vectored handler that recognises a fault in the sampler thread during a walk and ends that sample). Either way the sample is just cut short; nothing else changes. ## Notes - Development tool only (HOS_SAMPLE_ALL is off unless set); players never run it. - Found while measuring the giant-function split (local branch `bold/split`): no split code was on any sampled stack in that run, and the same crash can happen in any build. - The same unguarded `RtlLookupFunctionEntry` + `RtlVirtualUnwind` walk is in two more places in `host/mkw_glue.c` on main 0caf0618 (around lines 149 and 388, the other samplers); a shared guarded helper would cover all three.
Sign in to join this conversation.
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#383
No description provided.