Bug #22242
closedSEGV in method dispatch (`vm_call_iseq_setup_kwparm_nokwarg` / `def_iseq_ptr`) on Ruby 4.0.6 — Rails CI
Description
Recently Rails CI sometimes got SEGV.
Steps to reproduce¶
Run the Rails (rails/rails main branch) ActiveRecord test suite on Ruby 4.0.6, as Rails CI
does. For example:
$ git clone https://github.com/rails/rails.git
$ cd rails && bundle install
$ cd activerecord
$ bundle exec rake sqlite3_mem:test # also reproduced with trilogy:test and mysql2:test
The crash is intermittent (a small percentage of Rails CI runs) and the crashing test is
different every time, so it may take many runs to reproduce.
Rails CI log files for the five occurrences observed so far are attached, with Buildkite
permalinks to the segfault in each job:
rails_build_132123_activerecord-sqlite3-mem-4-dot-0.log— https://buildkite.com/rails/rails/builds/132123#019fd767-de5d-4a26-8bd0-aa3778729237/L10156rails_build_132137_activerecord-trilogy-4-dot-0-mysql-5-7.log— https://buildkite.com/rails/rails/builds/132137#019fd855-0650-42f5-8771-a683619fc603/L5849rails_build_132289_activerecord-mysql2-4-dot-0-mariadb.log— https://buildkite.com/rails/rails/builds/132289#019fe7b7-0c81-43bf-8661-7a3f9fe01cc1/L4496rails_build_132357_activerecord-sqlite3-mem-4-dot-0.log— https://buildkite.com/rails/rails/builds/132357#019fefa9-5187-425b-9967-c10cabd5f4f2/L7898rails_build_132357_activerecord-trilogy-4-dot-0-mysql-5-7.log— https://buildkite.com/rails/rails/builds/132357#019fefa9-5188-4728-b712-b226999e30e6/L4778
Expected behavior¶
It should always pass.
Actual behavior¶
The test process intermittently dies with [BUG] Segmentation fault. Every crash has the
same signature: SEGV inside the interpreter's method dispatch while reading the iseq out of
the callable method entry taken from an inline call cache — def_iseq_ptr(vm_cc_cme(cc)->def)
— in either vm_call_iseq_setup_kwparm_nokwarg (vm_insnhelper.c:3116-3117) or
vm_call_iseq_setup_normal (vm_insnhelper.c:3496):
| Rails CI build | Job | Crashing test | Fault address | Crash frame |
|---|---|---|---|---|
| 132123 | activerecord sqlite3_mem (4.0) | NestedRelationScopingTest#test_nested_scoped_create | 0x17 | vm_call_iseq_setup_kwparm_nokwarg vm_insnhelper.c:3116 |
| 132137 | activerecord trilogy (4.0) [mysql_5_7] | CompatibilityTest5_1#test_datetime_doesnt_set_precision_on_change_column | 0x08 | vm_call_iseq_setup_normal vm_insnhelper.c:3496 |
| 132289 | activerecord mysql2 (4.0) [mariadb] | OptimisticLockingTest#test_polymorphic_destroy_with_dependencies_and_lock_version | 0x17b9 | vm_call_iseq_setup_kwparm_nokwarg vm_insnhelper.c:3117 |
| 132357 | activerecord sqlite3_mem (4.0) | IntegrationTest#test_cache_version_format_is_not_too_precise | 0x1109 | vm_call_iseq_setup_kwparm_nokwarg vm_insnhelper.c:3117 |
| 132357 | activerecord trilogy (4.0) [mysql_5_7] | ActiveRecord::ConnectionAdapters::ConnectionHandlersShardingDbTest#test_establish_connection_using_3_levels_config_with_shards_and_replica | 0x18 | vm_call_iseq_setup_kwparm_nokwarg vm_insnhelper.c:3116 |
The crash-site frames of the C-level backtraces, showing that all five crashes are
duplicates of the same bug (full crash dumps are in the attached log files):
Build 132123, activerecord sqlite3_mem (4.0):
/usr/local/lib/libruby.so.4.0(def_iseq_ptr+0x0) [0x7fa976246636] /usr/src/ruby/vm_insnhelper.c:3116
/usr/local/lib/libruby.so.4.0(vm_call_iseq_setup_kwparm_nokwarg) /usr/src/ruby/vm_insnhelper.c:3116
/usr/local/lib/libruby.so.4.0(vm_sendish+0xb0) [0x7fa97625ecb5] /usr/src/ruby/vm_insnhelper.c:6134
/usr/local/lib/libruby.so.4.0(vm_exec_core) /usr/src/ruby/insns.def:904
Build 132137, activerecord trilogy (4.0) [mysql_5_7]:
/usr/local/lib/libruby.so.4.0(def_iseq_ptr+0xd) [0x7ff68de8a350] /usr/src/ruby/vm_core.h:626
/usr/local/lib/libruby.so.4.0(vm_call_iseq_setup_normal) /usr/src/ruby/vm_insnhelper.c:3496
/usr/local/lib/libruby.so.4.0(vm_call_iseq_setup_normal_0start_0params_0locals) /usr/src/ruby/vm_call_iseq_optimized.inc:14
/usr/local/lib/libruby.so.4.0(vm_sendish+0xce) [0x7ff68de9692e] /usr/src/ruby/vm_insnhelper.c:6134
Build 132289, activerecord mysql2 (4.0) [mariadb]:
/usr/local/lib/libruby.so.4.0(vm_call_iseq_setup_kwparm_nokwarg+0x2e) [0x7fb4fd53763e] /usr/src/ruby/vm_insnhelper.c:3117
/usr/local/lib/libruby.so.4.0(vm_sendish+0xb0) [0x7fb4fd54fcb5] /usr/src/ruby/vm_insnhelper.c:6134
/usr/local/lib/libruby.so.4.0(vm_exec_core) /usr/src/ruby/insns.def:904
/usr/local/lib/libruby.so.4.0(vm_exec_loop+0xa) [0x7fb4fd5565ca] /usr/src/ruby/vm.c:2825
Build 132357, activerecord sqlite3_mem (4.0):
/usr/local/lib/libruby.so.4.0(vm_call_iseq_setup_kwparm_nokwarg+0x2e) [0x7ff26cc2163e] /usr/src/ruby/vm_insnhelper.c:3117
/usr/local/lib/libruby.so.4.0(vm_sendish+0xb0) [0x7ff26cc39cb5] /usr/src/ruby/vm_insnhelper.c:6134
/usr/local/lib/libruby.so.4.0(vm_exec_core) /usr/src/ruby/insns.def:904
/usr/local/lib/libruby.so.4.0(vm_exec_loop+0xa) [0x7ff26cc405ca] /usr/src/ruby/vm.c:2825
Build 132357, activerecord trilogy (4.0) [mysql_5_7]:
/usr/local/lib/libruby.so.4.0(def_iseq_ptr+0x0) [0x7fa242c65636] /usr/src/ruby/vm_insnhelper.c:3116
/usr/local/lib/libruby.so.4.0(vm_call_iseq_setup_kwparm_nokwarg) /usr/src/ruby/vm_insnhelper.c:3116
/usr/local/lib/libruby.so.4.0(vm_sendish+0xb0) [0x7fa242c7dcb5] /usr/src/ruby/vm_insnhelper.c:6134
/usr/local/lib/libruby.so.4.0(vm_exec_core) /usr/src/ruby/insns.def:904
Files
Updated by luke-gru (Luke Gruber) 24 days ago
- Subject changed from laky SEGV in method dispatch (`vm_call_iseq_setup_kwparm_nokwarg` / `def_iseq_ptr`) on Ruby 4.0.6 — Rails CI to SEGV in method dispatch (`vm_call_iseq_setup_kwparm_nokwarg` / `def_iseq_ptr`) on Ruby 4.0.6 — Rails CI
Updated by luke-gru (Luke Gruber) 23 days ago
Thank you for the report. Has rails ran CI with other 4.0 releases? If so, did you notice any similar crashes for 4.0 releases preceding 4.0.6?
Updated by yahonda (Yasuo Honda) 17 days ago
- File rails_build_132611_activerecord-mysql2-4-dot-0.log.zip rails_build_132611_activerecord-mysql2-4-dot-0.log.zip added
Yes. Rails CI builds its test images FROM ruby:3.3 / ruby:3.4 / ruby:4.0, so each
lane automatically follows the latest patch release.
As far as I remember, this signature had not appeared on any 4.0 release before 4.0.6 — that is a recollection rather than a statistical claim, but I do not think I would have missed anything like the six SEGVs (2026-08-06 to 2026-08-16) that followed the switch to 4.0.6.
One more SEGV with the same signature occurred after the five I reported: build 132611
(activerecord mysql2, 2026-08-16) —
https://buildkite.com/rails/rails/builds/132611#01a00964-d6c3-4664-a380-d0c89f15eecc/L8913
Updated by kamipo (Ryuta Kamizono) 11 days ago
A VM_CHECK_MODE build turns this from a rare SEGV in random places into an
assertion at the moment the corrupted call cache is used, and the assertion
says the cc is holding a reclaimed cme.
RUBY_CONFIGURE_OPTS="cppflags=-DVM_CHECK_MODE=1" ruby-build 4.0.6 ~/.rbenv/versions/4.0.6-vmcheck
cd activerecord && bundle exec rake sqlite3_mem:test TESTOPTS="--seed=$RANDOM"
One hit in 53 full-suite runs (arm64-darwin25; lower rate than on CI here):
/vm_insnhelper.c:131: Assertion Failed: callable_method_entry_p:IMEMO_TYPE_P((VALUE)cme, imemo_ment): imemo_type:svar
ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [arm64-darwin25]
c:0082 METHOD .../active_support/notifications/fanout.rb:176 # Time.now
So vm_cc_cme(cc) points at an object whose imemo type is svar: the method
entry was collected and its slot reused while a live cc still refers to it.
The assertion fires from vm_sendish, i.e. at the
METHOD_ENTRY_INVALIDATED(vm_cc_cme(cc)) check in vm_search_method_fastpath,
which is already reading freed memory. That is the invariant
rb_imemo_mark_and_move() documents for imemo_callcache, so something frees
a cme that a live, non-invalidated cc still refers to — which also explains why
the fault addresses in this ticket vary so much.
The crashes I have looked at are all on a plain Time.now call. Time.now is
def self.now(in: nil) in <internal:timev>, i.e. keyword-only parameters, so
it always takes the kwparm_nokwarg fast path, and it is also constantly
redefined by ActiveSupport::Testing::TimeHelpers, which is presumably why
this mid is the one that surfaces it.
Updated by yahonda (Yasuo Honda) 11 days ago
The crash does not reproduce on master, so bisecting from the 4.0 branch point (d375bcc965d4) gives the first fixed commit:
1da540aeaa1507af4c050bf8a6ee2802e65f9aac "Invalidate CC on cme free as well" (gc.c, +3/-1)
The boundary is sharp between 1da540aeaa and its parent commit 3c251f79a3d9, each built and run through the reproducer up to 1,200 times (the per-commit budget for the bisect): the parent crashes with the same signature (at run 101), while 1da540aeaa completes all 1,200 runs cleanly.
Your Time.now observation also explains the reproducer's two files. A per-file bisect of the Active Record suite narrows CI to the smallest reliable reproducer (one process per run, official ruby:4.0.6 image, recent rails main, in-memory SQLite; the crash is not commit-specific):
| test files | crashes / runs |
|---|---|
primary_keys_test.rb + timestamp_test.rb, --seed 57420 |
162 / 400 (≈40%) |
| 8-file control set without primary_keys_test.rb | 0 / 37,239 |
primary_keys_test.rb is the file that enters multi-Ractor mode (its test_primary_key_can_be_read_from_a_ractor_* tests). timestamp_test.rb is the file that drives ActiveSupport::Testing::TimeHelpers, i.e. the constant Time.now redefinition you point to. Dropping either file gives zero crashes. The rate is very seed-sensitive: the favorable order from --seed 57420 crashes about 40% of runs, whereas a random seed is around 0.78% (~1 in 130). Consistently, in all six Rails CI failures inspected so far (builds 132123, 132137, 132655, 132673, 132698, 132752), the failure occurred between 1.4 s and about 130 s after the Ractor tests had run in the same process. Since the initial report, this signature keeps recurring — ten and counting.
Reproducer. The bug is not specific to this commit; the commit is pinned only so that --seed 57420 reliably reproduces the same test order — a seed fixes the order only relative to a given set of tests — and that order crashes fast (about 40% of runs here, usually within a few iterations). On any other recent checkout, drop the git checkout and --seed and loop with a random seed instead (~0.78%, ~10 minutes).
git clone https://github.com/rails/rails && cd rails
git checkout 1f0c247be3da # not commit-specific; pins so --seed 57420 gives the same test order
bundle install
cd activerecord
export ARCONN=sqlite3_mem
while :; do
bin/test test/cases/primary_keys_test.rb test/cases/timestamp_test.rb --seed 57420 >/tmp/run.log 2>&1
grep -qE "\[BUG\]|Segmentation fault" /tmp/run.log && break
done
tail -60 /tmp/run.log
Updated by jhawthorn (John Hawthorn) 10 days ago
· Edited
I've managed to reproduce this under rr locally through the Active Record test suite with VM_CHECK_MODE enabled. I don't have a more reliable reproduction yet or a fix.
What the recording seems to show is that the ccs entries become orphaned in multi-ractor mode via a vm_cc_table_dup_i since that skips invalidated CMEs (a change I made in https://github.com/ruby/ruby/pull/15652, though that was to avoid a different crash) however that breaks our invariant that all "valid" CC's have their CME marked via the ccs table. So my change there is wrong (but also we probably shouldn't revert it because of what it fixed).
This is also made more complicated because there's a latent bug in cc_is_active, where it always returns false, which . This should be "harmless" (bad for performance and CoW memory usage) because it simply clears the iseq's inline callcaches more often than it should, but is likely part of why this is hard to reproduce. I was planning to merge a fix for this into HEAD https://github.com/ruby/ruby/pull/18360
I do think 1da540aeaa1507af4c050bf8a6ee2802e65f9aac may fix this, but that's a tough one to backport. The weakref mechanism on Ruby HEAD makes that pretty cheap, but it's twice as expensive on 4.0 (which records the edges rather than the parent objects which have a weakref). Also, though it seems safe it was not the intended invalidation mechanism. I'd like to investigate more, a simpler reproduction would go a long way because it's very tricky to get call caches working correctly.
Updated by jhawthorn (John Hawthorn) 9 days ago
- Status changed from Open to Closed
Applied in changeset git|f95e71cc1e2a082393d58db1eb6f57b09694f8cb.
[Bug #22242] Invalidate CCs on cc_table_dup
When a Ractor is started, rather than modifying the cc table in place we
make a copy and CAS the table. When making the copy we previously
skipped cc_entries with an invalidated CME. However, that left the CCs
associated with that entry orphaned. CCs do not mark their CME, and the
CAS removed the table from the live set, so nothing invalidated the CC
and it ended up pointing at a garbage object.
We believe this bug is dormant in HEAD because handle_weak_references
for imemo_callcache will invalidate when the cme is collected.
Co-authored-by: Luke Gruber luke.gru@gmail.com