vm.c: defer task switches during gc.iterating

RETURN_IF_TASK_STOPPED used to bail out of mrb_vm_exec as soon as
task.switching was set, even when the exec was called re-entrantly
from inside a heap-walk callback (e.g. ObjectSpace.each_object via
mrb_yield). Bailing in that situation only unwinds the inner exec,
leaving the outer C iteration to keep calling the callback. The
call-info stack drifts on each subsequent mrb_yield, and the program
either trips the cibase assertion in mrb_vm_run or crashes in
__longjmp.

Hold off the switch while mrb->gc.iterating is set so the heap walk
finishes intact; the switch then fires at the next OP boundary once
the walk releases the flag. MRB_TASK_STOPPED is intentionally not
deferred, since exiting promptly is still correct when the task
itself is going away.

Fixes #6862.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-05-28 07:27:37 +09:00
parent f5a5bfcbf6
commit d1289ac8f9
+9 -1
View File
@@ -1588,8 +1588,16 @@ 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
/* 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 -- the task is going away. */
#define RETURN_IF_TASK_STOPPED(mrb) do { \
if ((mrb)->task.switching || (mrb)->c->status == MRB_TASK_STOPPED) \
if (((mrb)->task.switching && !(mrb)->gc.iterating) || \
(mrb)->c->status == MRB_TASK_STOPPED) \
return mrb_nil_value(); \
} while (0)
#define TASK_STOP(mrb) do { \