3 Commits
Author SHA1 Message Date
WallisSkillandClaude Opus 5.5 273c8c2563 test(libkernel): prove the alertable wait entry in guest_raise_leaves_host_code
The test declares the libkernel exports it calls through the public API
header instead of local declarations.

The alertable case published its round before calling WaitForSingleObjectEx,
so a raise could land before the wait and be counted as delivered inside it.
Each round now first holds the target at that boundary in guest code and
raises there: that delivery must happen before the wait (counted apart, ten
of ten). The second raise is sent only once the target's context shows it
inside ntdll, that is in the wait, and its handler must run after the wait
is released.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:43:01 +07:00
WallisSkillandClaude Opus 5.5 540364a24f fix(libkernel): queue a raise's APC only for a thread in a libkernel wait
A target in host code got the delivery as an APC as well as through the
return breakpoint and the sampling. An APC runs in any alertable wait,
so host code that waits alertably while holding a lock would have run
the guest handler inside that wait with the lock held. The APC is now
queued only while the target is in a libkernel wait (Pthread waitCount),
the point where the handler already ran before; a target elsewhere in
host code takes the delivery when it returns into guest code.

guest_raise_leaves_host_code adds a target in an alertable
WaitForSingleObjectEx: each raise must return in under 500 ms and the
handler must run in the executable after the wait was released. The
earlier behaviour (an APC for every host-code target) fails it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:06:33 +07:00
WallisSkillandClaude Opus 5.5 1b9c5feb30 fix(libkernel): deliver a raised exception at the guest return of a thread in host code
RaiseOn suspended the target and, while it sat in host code, resampled
it until a 50 ms limit and then redirected it into the handler there.
A handler that waits, as Unity's GC thread-stop handler does, then held
whatever host lock the thread had: in Graveyard Keeper 2 a job worker
stopped inside shader specialization kept the shader registration lock,
another worker blocked on that lock never ran its own stop, and the
load froze with 39 of 40 threads acknowledged (about 1 in 15 loads).

A target in host code is never redirected now. The raise queues the
delivery as an APC (taken when the thread waits alertably), arms a
debug register 0 execute breakpoint at the frame's return address into
guest code (found with the image's own .pdata and RtlVirtualUnwind,
stopping at frames that push a machine frame, guarded against faults),
and hands the target to a host deliverer thread that keeps sampling it.
The first of the three to reach the thread delivers, through one taken
flag per delivery. A target in guest code is still redirected at once;
a target inside NtContinue still makes the raise throw after 1 s.

guest_raise_leaves_host_code checks that a handler never runs in host
code, for a target blocked in WaitForSingleObject and one busy in
MultiByteToWideChar, and that each raise of the blocked one returns in
under 500 ms.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:06:32 +07:00