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) 3 days ago. Updated 1 day 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) 3 days 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) 3 days 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) 3 days 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 2 days 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) 2 days 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) 2 days 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

Updated by nagachika (Tomoyuki Chikanaga) 1 day ago Actions #7 [ruby-core:125527]

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

Thank you for creating PRs. Merged into ruby_3_4 at https://github.com/ruby/ruby/commit/0dcf36db7b22b0eac26281cb7b9b0f1f1b85f374

Actions

Also available in: PDF Atom