Surfaced by
The #523 liferaft migration: after the dict fix (#531) and task reaping (#530) unlocked long task runs, all node tasks died en masse ~360 steps into any chaos run, each receiving a runtime null from task_recv and (in liferaft) showing corrupted params. Shrunk to a 70-line repro (raft-shaped ping-pong of recv-loop tasks + sleeping deliverer tasks); root-caused via a suspend-flag ledger + __builtin_return_address on the blocking-recv path.
Root cause
The #408 ruling is that task code runs interpreted ("non-JIT suspending code", V8-generators analogy). The fresh-entry gate enforces it (!g_task_sched around exec_count++ / jit_register_chunk) — but three other JIT entry points don't:
- The OSR back-edge counter + handoff — gated only on
!g_vm_multithreaded. A task's hot loop (a node's loop while 1 mailbox loop) crosses the OSR threshold after ~1000 iterations and gets compiled mid-task.
- The
OP_CALL / OP_DISPATCH thunk entries — a chunk JIT'd before the first spawn can be entered from inside a task.
- Once native, a blocking builtin (
task_recv on an empty mailbox) is invoked via jit_helper_call, which has no g_task_suspend_request check (the interpreter's builtin-call site does). The thunk barrels on: the placeholder null flows onward as the "received" message, and the leaked suspend flag fires at whatever interpreted call site runs next — suspending the task mid-expression with a garbage stack.
Symptoms: tasks receive null from task_recv, suspended-task locals/params read as garbage, mass task death exactly around the OSR threshold. Explains why nothing shorter-lived ever saw it: pre-#530 the 255-spawn lifetime cap killed task-heavy workloads long before any task loop got hot.
Fix
Gate all four sites on !g_task_sched (mirroring the fresh-entry gate): the OSR counter/handoff, both thunk entries, and a belt-and-braces bail in jit_helper_call. Verified: the shrink repro runs 1000 steps with all tasks alive (previously all dead by ~step 360), liferaft's 5-node 1500-step chaos run goes from mass node death to 237/238 committed, invariants clean.
Regression test: a recv-loop task driven past the OSR threshold (EIGS_JIT_OSR lowered) must keep receiving real messages.
Surfaced by
The #523 liferaft migration: after the dict fix (#531) and task reaping (#530) unlocked long task runs, all node tasks died en masse ~360 steps into any chaos run, each receiving a runtime
nullfromtask_recvand (in liferaft) showing corrupted params. Shrunk to a 70-line repro (raft-shaped ping-pong of recv-loop tasks + sleeping deliverer tasks); root-caused via a suspend-flag ledger +__builtin_return_addresson the blocking-recv path.Root cause
The #408 ruling is that task code runs interpreted ("non-JIT suspending code", V8-generators analogy). The fresh-entry gate enforces it (
!g_task_schedaroundexec_count++/jit_register_chunk) — but three other JIT entry points don't:!g_vm_multithreaded. A task's hot loop (a node'sloop while 1mailbox loop) crosses the OSR threshold after ~1000 iterations and gets compiled mid-task.OP_CALL/OP_DISPATCHthunk entries — a chunk JIT'd before the first spawn can be entered from inside a task.task_recvon an empty mailbox) is invoked viajit_helper_call, which has nog_task_suspend_requestcheck (the interpreter's builtin-call site does). The thunk barrels on: the placeholder null flows onward as the "received" message, and the leaked suspend flag fires at whatever interpreted call site runs next — suspending the task mid-expression with a garbage stack.Symptoms: tasks receive
nullfromtask_recv, suspended-task locals/params read as garbage, mass task death exactly around the OSR threshold. Explains why nothing shorter-lived ever saw it: pre-#530 the 255-spawn lifetime cap killed task-heavy workloads long before any task loop got hot.Fix
Gate all four sites on
!g_task_sched(mirroring the fresh-entry gate): the OSR counter/handoff, both thunk entries, and a belt-and-braces bail injit_helper_call. Verified: the shrink repro runs 1000 steps with all tasks alive (previously all dead by ~step 360), liferaft's 5-node 1500-step chaos run goes from mass node death to 237/238 committed, invariants clean.Regression test: a recv-loop task driven past the OSR threshold (
EIGS_JIT_OSRlowered) must keep receiving real messages.