[ruby-core:125497] [Ruby Bug#22070] `Thread.each_caller_location(1, 1)` segfaults when called from a cfunc
Issue #22070 has been reported by AMomchilov (Alexander Momchilov). ---------------------------------------- Bug #22070: `Thread.each_caller_location(1, 1)` segfaults when called from a cfunc https://bugs.ruby-lang.org/issues/22070 * Author: AMomchilov (Alexander Momchilov) * Status: Open * ruby -v: ruby 4.0.2 (2026-03-17 revision d3da9fec82) +PRISM [arm64-darwin24] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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 ```sh ruby -e 'tap { Thread.each_caller_location(1, 1) { it.label } }' # 🧨 Boom ``` ## More cases <details><summary><code>repro.rb</code></summary> ```ruby # 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}" ``` </details> -- https://bugs.ruby-lang.org/
Issue #22070 has been updated by AMomchilov (Alexander Momchilov). I think it's an off-by-one bug. [This +1 seems](https://github.com/ruby/ruby/pull/16951) to fix it, though admittedly this code path is pretty complicated and I'm not sure of the exact memory layout. ---------------------------------------- Bug #22070: `Thread.each_caller_location(1, 1)` segfaults when called from a cfunc https://bugs.ruby-lang.org/issues/22070#change-117314 * Author: AMomchilov (Alexander Momchilov) * Status: Open * ruby -v: ruby 4.0.2 (2026-03-17 revision d3da9fec82) +PRISM [arm64-darwin24] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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 ```sh ruby -e 'tap { Thread.each_caller_location(1, 1) { it.label } }' # 🧨 Boom ``` ## More cases <details><summary><code>repro.rb</code></summary> ```ruby # 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}" ``` </details> -- https://bugs.ruby-lang.org/
Issue #22070 has been updated by jeremyevans0 (Jeremy Evans). 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 commit:10767283dd0277a1d780790ce6bde67cf2c832a2. ---------------------------------------- Bug #22070: `Thread.each_caller_location(1, 1)` segfaults when called from a cfunc https://bugs.ruby-lang.org/issues/22070#change-117319 * Author: AMomchilov (Alexander Momchilov) * Status: Open * ruby -v: ruby 4.0.2 (2026-03-17 revision d3da9fec82) +PRISM [arm64-darwin24] * Backport: 3.3: DONTNEED, 3.4: DONTNEED, 4.0: REQUIRED ---------------------------------------- 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 ```sh ruby -e 'tap { Thread.each_caller_location(1, 1) { it.label } }' # 🧨 Boom ``` ## More cases <details><summary><code>repro.rb</code></summary> ```ruby # 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}" ``` </details> -- https://bugs.ruby-lang.org/
Issue #22070 has been updated by mame (Yusuke Endoh). 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 commit:4c366ec9775eb6acb3fcb3b88038d051512c75a2, not by me, but by you :-) ---------------------------------------- Bug #22070: `Thread.each_caller_location(1, 1)` segfaults when called from a cfunc https://bugs.ruby-lang.org/issues/22070#change-117320 * Author: AMomchilov (Alexander Momchilov) * Status: Open * ruby -v: ruby 4.0.2 (2026-03-17 revision d3da9fec82) +PRISM [arm64-darwin24] * Backport: 3.3: DONTNEED, 3.4: REQUIRED, 4.0: REQUIRED ---------------------------------------- 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 ```sh ruby -e 'tap { Thread.each_caller_location(1, 1) { it.label } }' # 🧨 Boom ``` ## More cases <details><summary><code>repro.rb</code></summary> ```ruby # 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}" ``` </details> -- https://bugs.ruby-lang.org/
Issue #22070 has been updated by jeremyevans0 (Jeremy Evans). @mame thank you for clarifying. I apologize for implicating you :) I created backport PRs: * 4.0: https://github.com/ruby/ruby/pull/16976 * 3.4: https://github.com/ruby/ruby/pull/16977 ---------------------------------------- Bug #22070: `Thread.each_caller_location(1, 1)` segfaults when called from a cfunc https://bugs.ruby-lang.org/issues/22070#change-117328 * Author: AMomchilov (Alexander Momchilov) * Status: Closed * ruby -v: ruby 4.0.2 (2026-03-17 revision d3da9fec82) +PRISM [arm64-darwin24] * Backport: 3.3: DONTNEED, 3.4: REQUIRED, 4.0: REQUIRED ---------------------------------------- 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 ```sh ruby -e 'tap { Thread.each_caller_location(1, 1) { it.label } }' # 🧨 Boom ``` ## More cases <details><summary><code>repro.rb</code></summary> ```ruby # 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}" ``` </details> -- https://bugs.ruby-lang.org/
Issue #22070 has been updated by k0kubun (Takashi Kokubun). Backport changed from 3.3: DONTNEED, 3.4: REQUIRED, 4.0: REQUIRED to 3.3: DONTNEED, 3.4: REQUIRED, 4.0: DONE ruby_4_0 commit:977e7fd3927a3c5da2bacce03798fcc0b44ab4bf. ---------------------------------------- Bug #22070: `Thread.each_caller_location(1, 1)` segfaults when called from a cfunc https://bugs.ruby-lang.org/issues/22070#change-117329 * Author: AMomchilov (Alexander Momchilov) * Status: Closed * ruby -v: ruby 4.0.2 (2026-03-17 revision d3da9fec82) +PRISM [arm64-darwin24] * Backport: 3.3: DONTNEED, 3.4: REQUIRED, 4.0: DONE ---------------------------------------- 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 ```sh ruby -e 'tap { Thread.each_caller_location(1, 1) { it.label } }' # 🧨 Boom ``` ## More cases <details><summary><code>repro.rb</code></summary> ```ruby # 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}" ``` </details> -- https://bugs.ruby-lang.org/
Issue #22070 has been updated by nagachika (Tomoyuki Chikanaga). 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 ---------------------------------------- Bug #22070: `Thread.each_caller_location(1, 1)` segfaults when called from a cfunc https://bugs.ruby-lang.org/issues/22070#change-117345 * Author: AMomchilov (Alexander Momchilov) * Status: Closed * ruby -v: ruby 4.0.2 (2026-03-17 revision d3da9fec82) +PRISM [arm64-darwin24] * Backport: 3.3: DONTNEED, 3.4: DONE, 4.0: DONE ---------------------------------------- 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 ```sh ruby -e 'tap { Thread.each_caller_location(1, 1) { it.label } }' # 🧨 Boom ``` ## More cases <details><summary><code>repro.rb</code></summary> ```ruby # 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}" ``` </details> -- https://bugs.ruby-lang.org/
participants (5)
-
AMomchilov (Alexander Momchilov) -
jeremyevans0 (Jeremy Evans) -
k0kubun (Takashi Kokubun) -
mame (Yusuke Endoh) -
nagachika (Tomoyuki Chikanaga)