Bug #22436
openArray#zip with a method_missing-based argument leaks a stale block handler (segfault when the next C-called method yields)
Description
The following code:
o = Object.new
def o.method_missing(*) = nil # Array#zip calls o.each via method_missing
out = Object.new
def out.write(*) = nil
def out.puts(*) = yield # receives a block it was never given
$stdout = out
[1].zip(o)
puts 1 # Kernel#puts -> $stdout.puts gets a stale ifunc block
Resulted in this output (release build, also ruby 3.2.3):
min.rb:6: [BUG] Segmentation fault at 0x0000000000000018
-- Control frame information -----------------------------------------------
c:0005 p:---- s:0021 e:000020 l:n b:---- r:(nil) IFUNC :puts
c:0004 p:0002 s:0018 e:000017 l:y b:0001 r:(nil) METHOD min.rb:6 [FINISH]
c:0003 p:---- s:0013 e:000012 l:y b:0001 r:(nil) CFUNC :puts
c:0002 p:0062 s:0008 E:001df0 l:n b:---- r:(nil) EVAL min.rb:10 [FINISH]
-- C level backtrace information -------------------------------------------
ruby(vm_yield_with_cfunc+0x329) ../vm_insnhelper.c:5273
ruby(vm_invoke_ifunc_block+0x1a9) ../vm_insnhelper.c:5537
ruby(vm_sendish+0xea) ../vm_insnhelper.c:6290
ruby(rb_vm_exec) vm.c:2884
ruby(vm_call0_body+0xeb2) ../vm_eval.c:225
ruby(rb_call0) ../vm_eval.c:577
ruby(rb_funcallv_kw+0x8e) ../vm_eval.c:896
ruby(vm_call_cfunc_with_frame_) ../vm_insnhelper.c:3906
With -DRUBY_DEBUG=1 an even smaller program trips the VM assertion:
../vm_core.h:1849: Assertion Failed: VM_BH_IFUNC_P:imemo_type_p(captured->code.val, imemo_ifunc)
ruby 4.1.0dev (2026-10-11T06:59:58Z master 46033eff6c) +PRISM [x86_64-linux]
ruby(rb_assert_failure)
ruby(vm_block_handler_type+0x15b) ../vm_core.h:1849
ruby(vm_block_handler_verify+0xd) ../vm_core.h:1903
ruby(rb_call0) vm.c:327
ruby(rb_funcallv_kw+0xdd) ../vm_eval.c:896
ruby(vm_call_cfunc_with_frame_+0x27b) ../vm_insnhelper.c:3906
The same thing also silently drops the block. method_missing(:each, &b) called from Array#zip gets b == nil.
Analysis: take_items (array.c) calls rb_check_block_call, and rb_iterate0 sets ec->passed_block_handler to an ifunc block captured in the zip frame. When each is missing, check_funcall_missing -> check_funcall_exec -> call_method_entry (vm_method.c) takes the passed block handler, calls rb_vm_call_kw (so method_missing runs with no block), and then restores the handler. Nothing consumes it after that. After zip returns, ec->passed_block_handler still points at the popped frame's captured block. The next C-level rb_funcall* (here Kernel#puts -> $stdout.puts) picks it up in vm_passed_block_handler() and passes it as the callee's block. call_method_entry's save/restore suits respond_to_missing?, but on the final method_missing call the block should be passed to that method, not restored.
To reproduce:
Commit:
Build configuration:
../configure --prefix=... --disable-install-doc cflags="-fsanitize=address -fno-omit-frame-pointer -DUSE_MN_THREADS=0" cppflags="-DRUBY_DEBUG=1" optflags="-O1" debugflags="-g"
(segfault also reproduces with a non-debug build and with Ubuntu's packaged ruby 3.2.3)
Operating System:
Updated by peterzhu2118 (Peter Zhu) about 4 hours ago
- Backport changed from 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN to 3.3: WONTFIX, 3.4: REQUIRED, 4.0: REQUIRED
Thank you for the bug report, I have a fix here: https://github.com/ruby/ruby/pull/19337