Project

General

Profile

Actions

Bug #22499

open

SIGSEGV in vm_exec_core inside Monitor#synchronize (4.1.0preview1)

Bug #22499: SIGSEGV in vm_exec_core inside Monitor#synchronize (4.1.0preview1)

Added by altruby (Alt Ruby) 4 days ago. Updated 3 days ago.

Status:
Open
Assignee:
-
Target version:
-
ruby -v:
4.1.0preview1 (2026-10-11 master 2d08b5f42f) +PRISM [x86_64-freebsd16.0]
[ruby-core:127305]

Description

Summary

Ruby 4.1.0preview1 segfaults inside a Monitor#synchronize block while
dispatching an instruction in vm_exec_core. The crash is reproducible and
three core dumps show the same shape: the VM calls a code address that
lives in a writable, non-executable page.

Environment

  ruby         4.1.0preview1 (2026-10-11 master 2d08b5f42f) +PRISM
               [x86_64-freebsd16.0]
  system       FreeBSD 16.0-CURRENT, x86_64
  built with   ruby-install 0.10.1, rbenv, no JIT (yjit not enabled)
  app          que worker process: "que ./config"
               (Foreman-launched; threads named worker.rb:41,
               locker.rb:180, task.rb:157, "AR Pool Reaper")
  gems         pg (pg_ext.so), active_record, que

Reproduction

Reproduces on every run of the worker, typically within minutes:

  cd ~/projects/r.uby.dev
  RBENV_VERSION=4.1.0-preview1-debug \
    RUBY_DEBUG=1 \
    MALLOC_CONF=junk:true \
    bundle exec foreman start

Cores are written to /var/coredumps/1001:

  • ruby.39219.core 22:53
  • ruby.84525.core 23:08
  • ruby.41759.core 23:23

Backtrace (debug build, -O0 -g3, gdb)

  #2  ruby_default_signal (sig=11) at signal.c:421
  #3  rb_bug_for_fatal_signal (sig=11, fmt="Segmentation fault at %p%s")
        at error.c:1171
  #4  sigsegv (sig=11, info=0x826129830, ctx=0x8261294c0) at signal.c:977
  #5  libthr.so.3 signal trampoline
  #7  <signal handler called>
  #8  0x0000000827846220 in ?? ()          <- heap
  #9  0x000000084dcb9758 in ?? ()          <- the ec
  #10 0x0000000827945d60 in ?? ()          <- heap
  #11 vm_exec_core (ec=0x84dcb9758) at insns.def:864
        864    JIT_EXEC(ec, val);
  #12 rb_vm_exec (ec=0x84dcb9758) at vm.c:2926
  #13 invoke_iseq_block_from_c (...) at vm.c:1934
  #14 invoke_block_from_c_bh (...) at vm.c:1948
  #15 vm_yield_with_cref (...) at vm.c:1991
  #16 vm_yield (...) at vm.c:1999
  #17 rb_ec_yield (ec=0x84dcb9758, val=36) at vm_eval.c:1392
  #18 do_ec_yield (_ec=35664926552) at thread_sync.c:635
  #19 rb_ec_ensure (b_proc=do_ec_yield, e_proc_arg=monitor_sync_ensure)
        at eval.c:1177
  #20 rb_monitor_synchronize (monitor=35684680768) at thread_sync.c:1514

Control frame at the fault:

  cfp = { pc = 0x847848b00, sp = 0x827846470, _iseq = 0x81b7683e0,
          self = 35684811280, ep = 0x827846468, block_code = 0x0,
          jit_return = 0x81d860126 }

Ruby-level frame recovered from the iseq location:

  location = { pathobj = 34995792800, label = 34849573248,
               first_lineno = 52, node_id = 252,
               code_location = { beg_pos = {lineno = 52, column = 32},
                                 end_pos = {lineno = 55, column = 11} } }

which is the body of Turn::Watch#follow in the application:

  def follow(agent_id)
    start!
    queue = Queue.new
    synchronize { watchers(agent_id) << queue }   <- crashed here
    queue
  end

  def synchronize(&b)
    MUTEX.synchronize(&b)
  end

Evidence of memory corruption

  1. The faulting instruction pointer is not in any loaded image. From the
    first core:

    mc_rip = 0x826449d80
    memory region 0x826449d80 -> [0x8263a7000-0x8264f8000) rw-

    A writable, non-executable page: the VM called a value that is not code.

  2. The iseq's path object is corrupt in the core. It reports len = 125 but
    its heap pointer reads back as 0x6f722f656d6f682f, which is the ASCII
    text "/home/ro" interpreted as an address:

    {basic = {flags = 7534522177132955941, klass = 34820395680},
    len = 125,
    as = {heap = {ptr = 0x6f722f656d6f682f , ...}}}

    That run was started with MALLOC_CONF=junk:true, so freed objects are
    filled with junk; this looks like a freed or overwritten object.

  3. Frame #7's return address is 0x00007ffffffff2e3 in the earlier cores - a
    sentinel, not a real return address.

Context: ractors and llm.rb

The process runs llm.rb (gem "llm.rb", github: "r-uby-dev/llm") as its agent
runtime, and the workload that triggers this spawns tool calls in child
ractors. From the application's stream handler:

  def on_tool_call(tool)
    write(tool: tool.name)
    stop_if_cancelled!
    queue << (tool.mcp? ? tool.task(:ractor) :
                          tool.task(:async))
  end

so MCP tool calls run on ractors while an agent turn is streaming, and the
crashing path is the SSE route that follows a turn:

  queue = Raven::Turn::Watch.follow(agent.id) if agent

That is the same process, same threads, same time as the ractor-spawned
tool calls. Ractor machinery is visible in the first core - a scheduler
thread parked in:

  rb_native_cond_wait
  ractor_sched_deq (thread_sched.c:1388)
  nt_shared_loop (thread_sched.c:2074)
  nt_start (thread_sched.c:2021)

Caveat, stated plainly: the faulting thread is a worker thread inside
Monitor#synchronize, not a ractor body, and we have not reproduced this
without llm.rb or without ractors in the process. What we can say is that
the crash happens in a process that is running ractor-spawned tool calls
concurrently, on a 4.1 preview whose scheduler is Ractor-aware.

Other threads

None of the other 12 threads are in the faulting path. They are parked in
syscalls: _umtx_op_err (condvars/mutexes), __sys_kevent, __sys_ppoll, plus
pg_ext.so threads in pgconn_wait_for_notify waiting for a socket. They
inherit the process SIGSEGV, they did not raise it.

Actions

Also available in: PDF Atom