Skip to content

JIT: OSR compiles task code mid-task — jit_helper_call has no suspend check (state corruption after ~threshold iterations) #533

Description

@InauguralPhysicist

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:

  1. 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.
  2. The OP_CALL / OP_DISPATCH thunk entries — a chunk JIT'd before the first spawn can be entered from inside a task.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions