Make try_vm_runtime panic hook thread-safe - #3333
Conversation
| } | ||
|
|
||
| #[test] | ||
| fn test_parallel_vm_runtime_preserves_host_hook() { |
There was a problem hiding this comment.
This test is pretty convoluted, but dealing with global state is usually messy. I'm open for suggestions on other ways to do it, or to remove it entirely if we decide it's a maintenance burden.
ljedrz
left a comment
There was a problem hiding this comment.
This is an overall improvement, with one caveat - I think this change could cause halts in the context of rayon's worker threads to not be graceful (i.e. the scary default panic message would be displayed instead), unless the entire closure is wrapped in try_vm_runtime.
Also left one small hardening suggestion.
Co-authored-by: ljedrz <3750347+ljedrz@users.noreply.github.com> Signed-off-by: Eran Rundstein <eran@rundste.in>
Thank you @ljedrz ! You're right about the
I'm leaning towards trying to at the very least locate places inside |
Antonio95
left a comment
There was a problem hiding this comment.
Cool solution. The logic seems solid, although several of its building blocks are new to me. Regarding rayon not using the graceful handlers, I see several solutions have been listed. Would it be possible to simply wrap some rayon macros (e.g. cfg_iter) in our own which incorporate the handler/IN_VM_RUNTIME management?
Green light from me, I defer to @eranrund, @ljedrz and more knowledgeable pleople on the rayon topic.
|
Thank you @Antonio95 Unfortunately I don't think there's a trivial path where wrapping/changing the cfg_iter macro solves this. There are two obstacles. First, the macro only constructs the iterator - the actual work (e.g. the .for_each closure) executes much later, so there's nothing at the macro site to wrap around. Second, and more fundamentally, IN_VM_RUNTIME is a thread-local, and rayon executes the closures on its pool's worker threads, which don't inherit thread-locals from the calling thread - so even wrapping the execution at the call site wouldn't propagate the flag to the threads that actually run the code. It's possible to provide a custom .par_iter()-like method returning a type that implements rayon's iterator traits and intercepts execution deep enough to set up state on each worker, but my intuition is it is non-trivial. The least intrusive option I can think of is a dedicated rayon thread pool for VM operations: try_vm_runtime would run its closure via ThreadPool::install, so any par_iter reached from VM code runs on that pool, and the pool's start_handler can set IN_VM_RUNTIME on every worker thread at spawn time. I haven't tried it yet, and it has its own complications - at minimum it is hard to reason about the potential effects of having double the amount of worker threads in the process. |
Motivation
Make
try_vm_runtimesafe under concurrent execution. Fixes #3327.Problem
try_vm_runtime!does atake_hook()/set_hook()/ restore dance around itscatch_unwind. The panic hook is a process-wide singleton, so running VM operations on multiple threads concurrently races these swaps: a VM halt on one thread can be reported by whatever hook another thread happens to have installed, debug builds leakVM safely halted at ...nondeterministically, and the swaps can interleave such that a host-installed panic hook is silently dropped. Results are unaffected (catch_unwindstill catches). See #3327 for details.Fix
Install one persistent, VM-aware panic hook (guarded by a
Once) and gate the "VM safely halted" handling on a thread-localIN_VM_RUNTIMEflag instead of mutating the global hook per operation:try_vm_runtimesets the flag around itscatch_unwindand restores the previous value afterwards, so nested calls work. I don't think we currently worry about nested calls but it's easy to support this way.VM safely halted ..., preserving the existing behavior.Relation to #2927
This is an alternative to the panic-hook portion of #2927, which also installs a single persistent hook but takes the opposite approach to reporting: its hook never prints and never delegates - it captures the panic message and backtrace into a thread-local so the catcher (
try_vm_runtime/catch_unwind) can log them. A consequence is that any panic not wrapped by those helpers becomes completely silent, since the previous/default hook is replaced and the stored info is never read.The position taken here is to keep the two concerns separate: the panic hook is the right place for reporting (it runs exactly once, at the panic site, with the backtrace available), while
catch_unwind/JoinHandlewrappers are for control flow (propagation, deciding to shut down). Sinceresume_unwinddoes not re-invoke the hook, a propagated panic is reported exactly once and nothing needs to carry the backtrace as data.This PR does not touch the task-management side of #2927 (the
tokio::spawn/spawn_blockingwrappers inutilities/src/task.rs); those are orthogonal and compose with this change.Test Plan
Unit tests cover success/panic payload preservation, nesting, and a multi-threaded test (run in a child process, since hook and
Oncestate are process-global) asserting that 8 concurrent VM halts each print exactly once while an unrelated panic on the same threads still reaches the host's hook.Documentation
N/A
Backwards compatibility
This should be fully backwards compatible.