Project

General

Profile

Actions

Bug #22436

open

Array#zip with a method_missing-based argument leaks a stale block handler (segfault when the next C-called method yields)

Bug #22436: Array#zip with a method_missing-based argument leaks a stale block handler (segfault when the next C-called method yields)

Added by 0599jiangyc@gmail.com (Yuancheng Jiang) about 6 hours ago. Updated about 3 hours ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:127100]

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:

o = Object.new
def o.method_missing(*) = nil
[1].zip(o)
puts 1
../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:

ruby ./min.rb

Commit:

46033eff6c01f05afa2505452fdbf4bea7ca9c6d (also reproduced on 3296d5c99c and ruby 3.2.3)

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:

Ubuntu 24.04.4 LTS (x86_64), Docker image fusion-fuzz-ruby:latest
Actions

Also available in: PDF Atom