Actions
Bug #22070
closed`Thread.each_caller_location(1, 1)` segfaults when called from a cfunc
Bug #22070:
`Thread.each_caller_location(1, 1)` segfaults when called from a cfunc
Status:
Closed
Assignee:
-
Target version:
-
ruby -v:
ruby 4.0.2 (2026-03-17 revision d3da9fec82) +PRISM [arm64-darwin24]
Description
Reading the label of a Thread::Backtrace::Location (directly or indirectly via e.g. to_s) segfauls if called from within Thread.each_caller_location(1, 1) { it.label }.
Reading the lineno or path seems to not cause any problems, perhaps by coincidence.
Minimal repro¶
ruby -e 'tap { Thread.each_caller_location(1, 1) { it.label } }' # 🧨 Boom
More cases¶
repro.rb
# All four conditions must hold; varying any one makes the crash go away:
#
# 1. The yielded location is inspected via a `cme`-reading method
# (`label`, `base_label`, `to_s`, `inspect`).
# * The `iseq`-based methods (`path`, `absolute_path`, `lineno`) seem to survive by accident.
# 2. `start` is exactly 1.
# 3. `length` is exactly 1.
# * Ranges that compute to length=1 (`1..1`, `1...2`) crash
# * e.g. `1..3` (length=3) does not.
# 4. The caller is inside a C-method frame (`tap`, `Array#each`, `instance_exec`, `eval`, ...).
# * Top-level and plain Ruby methods are safe.
$ruby = begin
require "rbconfig"
RbConfig.ruby
rescue LoadError
ENV["_"] || "ruby"
end
def check(label, code)
out = IO.popen([$ruby, "-e", code, err: [:child, :out]], &:read)
crashed = !$?.success? || out.include?("[BUG]")
printf " %-7s %s\n", (crashed ? "CRASH" : "OK"), label
end
puts "== Which Location method is accessed (start=1, length=1, from tap{}) =="
check("it.path", "tap { Thread.each_caller_location(1, 1) { it.path } }")
check("it.absolute_path", "tap { Thread.each_caller_location(1, 1) { it.absolute_path } }")
check("it.lineno", "tap { Thread.each_caller_location(1, 1) { it.lineno } }")
check("it.label", "tap { Thread.each_caller_location(1, 1) { it.label } }")
check("it.base_label", "tap { Thread.each_caller_location(1, 1) { it.base_label } }")
check("it.to_s", "tap { Thread.each_caller_location(1, 1) { it.to_s } }")
check("it.inspect", "tap { Thread.each_caller_location(1, 1) { it.inspect } }")
puts "== Vary `start` (length=1, .label, from tap{}) =="
check("start=0", "tap { Thread.each_caller_location(0, 1) { it.label } }")
check("start=1", "tap { Thread.each_caller_location(1, 1) { it.label } }")
check("start=2", "tap { Thread.each_caller_location(2, 1) { it.label } }")
check("start=3", "tap { Thread.each_caller_location(3, 1) { it.label } }")
puts "== Vary `length` (start=1, .label, from tap{}) =="
check("length=1", "tap { Thread.each_caller_location(1, 1) { it.label } }")
check("length=2", "tap { Thread.each_caller_location(1, 2) { it.label } }")
check("length=3", "tap { Thread.each_caller_location(1, 3) { it.label } }")
check("length=4", "tap { Thread.each_caller_location(1, 4) { it.label } }")
puts "== Range forms equivalent to (start=1, length=1) =="
check("(1, 1)", "tap { Thread.each_caller_location(1, 1) { it.label } }")
check("(1..1)", "tap { Thread.each_caller_location(1..1) { it.label } }")
check("(1...2)", "tap { Thread.each_caller_location(1...2) { it.label } }")
check("(1..2)", "tap { Thread.each_caller_location(1..2) { it.label } }")
puts "== Caller context (start=1, length=1, .label) =="
check("top-level", 'Thread.each_caller_location(1, 1) { it.label }')
check("tap { ... }", 'tap { Thread.each_caller_location(1, 1) { it.label } }')
check("[1].each { ... }", '[1].each { Thread.each_caller_location(1, 1) { it.label } }')
check("instance_exec", 'instance_exec { Thread.each_caller_location(1, 1) { it.label } }')
check("eval '...'", %{eval 'Thread.each_caller_location(1, 1) { it.label }'})
check("def m; ...; end; m", 'def m; Thread.each_caller_location(1, 1) { it.label }; end; m')
check("tap { m }", 'def m; Thread.each_caller_location(1, 1) { it.label }; end; tap { m }')
puts
puts "Ruby: #{RUBY_DESCRIPTION}"
Actions