mirror of
https://github.com/mruby/mruby
synced 2026-06-08 16:11:16 +00:00
vm.c: defer task switch across C call boundary
A task switch (early return from mrb_vm_exec) is unsafe when execution has re-entered the VM from C (mrb_funcall, mrb_yield, mrb_vm_run). The C stack frame between the scheduler's mrb_vm_exec and the current frame cannot be suspended, and returning early from the inner mrb_vm_exec leaves the call-info stack drifted, so the enclosing mrb_vm_run trips its `c->ci == c->cibase || ...` assertion (or corrupts state in a non-debug build). This happens when a block yielded from a C function wakes a task, for example Task::Queue#push from inside a block passed to a C method via mrb_yield_argv. Defer the switch via task_across_c_boundary, which walks the call-info stack for a C frame (cci > 0), mirroring the cooperative guard in Task.pass. The switch resumes once execution unwinds back to a frame with no C boundary. The gc.iterating short-circuit from #6862 is kept as a cheap pre-check. Fixes #6868. Refs #6864. Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1588,13 +1588,33 @@ prepare_tagged_break(mrb_state *mrb, uint32_t tag, const mrb_callinfo *return_ci
|
||||
#define CALL_CODE_HOOKS() do { insn = BYTECODE_DECODER(*ci->pc); CODE_FETCH_HOOK(mrb, irep, ci->pc, regs); } while (0)
|
||||
|
||||
#ifdef MRB_USE_TASK_SCHEDULER
|
||||
/* TRUE when the current context is executing across a C call boundary, i.e.
|
||||
a C function on the stack re-entered the VM (mrb_funcall / mrb_yield /
|
||||
mrb_vm_run). A task cannot be suspended at such a point: the C stack
|
||||
frame between the scheduler's mrb_vm_exec and the current frame cannot
|
||||
be saved or restored, and returning early from the inner mrb_vm_exec
|
||||
would leave the call-info stack drifted, tripping the assertion in
|
||||
mrb_vm_run (issues #6864, #6868). The scheduler defers the switch until
|
||||
execution unwinds back to a frame with no C boundary. This mirrors the
|
||||
cooperative guard in Task.pass, which raises rather than defers.
|
||||
cibase is excluded: it is the entry frame of this mrb_vm_exec. */
|
||||
static mrb_bool
|
||||
task_across_c_boundary(mrb_state *mrb)
|
||||
{
|
||||
for (mrb_callinfo *ci = mrb->c->ci; ci > mrb->c->cibase; ci--) {
|
||||
if (ci->cci > 0) return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Defer task switches while a C-level ObjectSpace walk holds gc.iterating
|
||||
true. The walk runs callbacks (which may call back into mrb_vm_exec via
|
||||
mrb_yield); returning early from an inner exec while the outer C
|
||||
iteration is still active drifts the call-info stack and eventually
|
||||
crashes (issue #6862). Switches resume at the next OP boundary after
|
||||
the walk releases gc.iterating. A pending MRB_TASK_STOPPED is not
|
||||
deferred, since the task is going away.
|
||||
the walk releases gc.iterating. A pending switch is also deferred while
|
||||
executing across a C call boundary (see task_across_c_boundary). A
|
||||
pending MRB_TASK_STOPPED is not deferred, since the task is going away.
|
||||
|
||||
mrb->jmp is restored to prev_jmp before returning, exactly as the
|
||||
normal return paths below do. mrb_vm_exec set mrb->jmp to its own
|
||||
@@ -1604,7 +1624,8 @@ prepare_tagged_break(mrb_state *mrb, uint32_t tag, const mrb_callinfo *return_ci
|
||||
This macro must only be expanded where prev_jmp is in scope, i.e.
|
||||
inside mrb_vm_exec (via NEXT / END_DISPATCH). */
|
||||
#define RETURN_IF_TASK_STOPPED(mrb) do { \
|
||||
if (((mrb)->task.switching && !(mrb)->gc.iterating) || \
|
||||
if (((mrb)->task.switching && !(mrb)->gc.iterating && \
|
||||
!task_across_c_boundary(mrb)) || \
|
||||
(mrb)->c->status == MRB_TASK_STOPPED) { \
|
||||
(mrb)->jmp = prev_jmp; \
|
||||
return mrb_nil_value(); \
|
||||
|
||||
Reference in New Issue
Block a user