Bug #22499
openSIGSEGV in vm_exec_core inside Monitor#synchronize (4.1.0preview1)
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¶
-
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.
-
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. -
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:
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.