Project

General

Profile

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

Added by AMomchilov (Alexander Momchilov) about 22 hours ago. Updated 14 minutes ago.

Status:
Closed
Assignee:
-
Target version:
-
ruby -v:
ruby 4.0.2 (2026-03-17 revision d3da9fec82) +PRISM [arm64-darwin24]
[ruby-core:125497]

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}"

Updated by AMomchilov (Alexander Momchilov) about 22 hours ago Actions #1 [ruby-core:125499]

I think it's an off-by-one bug. This +1 seems to fix it, though admittedly this code path is pretty complicated and I'm not sure of the exact memory layout.

Updated by jeremyevans0 (Jeremy Evans) about 15 hours ago Actions #2 [ruby-core:125503]

  • Backport changed from 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN to 3.3: DONTNEED, 3.4: DONTNEED, 4.0: REQUIRED

Thank you for the report. I can confirm the issue. Note that it only affects Ruby 4.0. Ruby 3.4 is not affected, and Ruby 3.3 doesn't support arguments to Thread.each_caller_location. I traced the cause of the bug to 10767283dd0277a1d780790ce6bde67cf2c832a2.

Updated by mame (Yusuke Endoh) about 14 hours ago Actions #3 [ruby-core:125504]

  • Backport changed from 3.3: DONTNEED, 3.4: DONTNEED, 4.0: REQUIRED to 3.3: DONTNEED, 3.4: REQUIRED, 4.0: REQUIRED

Confirmed, and the fix looks good to me.

Just one correction: Ruby 3.4 is affected too.

$ RBENV_VERSION=3.4.7 ruby -e '[1].each { Thread.each_caller_location(1, 1) { |loc| loc.label } }'
-e:1: [BUG] Segmentation fault at 0x0000000000000011
ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +PRISM [x86_64-linux]

The off-by-one bug itself was actually introduced in 4c366ec9775eb6acb3fcb3b88038d051512c75a2, not by me, but by you :-)

Updated by Anonymous about 3 hours ago Actions #4

  • Status changed from Open to Closed

Applied in changeset git|a3a2d461aa8cbcc1cb4a7c859acfaa4cbd686e77.


[Bug #22070] Fix segfault in Thread.each_caller_location

Updated by jeremyevans0 (Jeremy Evans) about 2 hours ago Actions #5 [ruby-core:125512]

@mame (Yusuke Endoh) thank you for clarifying. I apologize for implicating you :)

I created backport PRs:

Updated by k0kubun (Takashi Kokubun) 14 minutes ago Actions #6 [ruby-core:125513]

  • Backport changed from 3.3: DONTNEED, 3.4: REQUIRED, 4.0: REQUIRED to 3.3: DONTNEED, 3.4: REQUIRED, 4.0: DONE
Actions

Also available in: PDF Atom