Bug #22524
openCalling a shareable Proc overwrites $~ and $_ of a Thread.new/Fiber.new block
Description
Calling a shareable Proc which sets $~ or $_ directly from the block of Thread.new or Fiber.new overwrites the $~ and $_ of that block:
set = Ractor.shareable_proc { |s| s =~ /(.)/; $_ = s; nil }
p Thread.new { $_ = "line"; "z" =~ /(z)/; set.("a"); [$~[1], $_] }.value # => ["z", "line"]
p Fiber.new { $_ = "line"; "z" =~ /(z)/; set.("b"); [$~[1], $_] }.resume # => ["z", "line"]
def m = ($_ = "line"; "z" =~ /(z)/; SET.("c"); [$~[1], $_])
SET = set
p m # => ["z", "line"]
$ ruby -v svar.rb
ruby 4.1.0dev (2026-11-09T15:47:20Z master 7e5d882817) +PRISM [arm64-darwin25]
["a", "a"]
["b", "b"]
["z", "line"]
Ruby 4.0.7 prints ["z", "line"] three times, so this is a regression.
Cause¶
Since 65424e1791 (GC: per-Ractor local GC), the special variables of a shareable Proc are kept per execution context (so per Fiber) instead of in its environment. That fixed a real problem: the environment of a shareable Proc can be used by several Ractors at once, so its $~ was shared mutable state across Ractors.
But they are kept in ec->root_svar, which also holds the special variables of the root frame of the Fiber or Thread, that is, its block. So calling a shareable Proc from that block overwrites them, as above. Calls from methods or from the main script are not affected, because those frames store their special variables in their own environment.
Fix¶
PR: https://github.com/ruby/ruby/pull/19804
The fix uses a separate per-execution-context slot, ec->isolated_svar, for the special variables of shareable Procs. They are still per Fiber, but no longer alias the root frame. Shareable Procs still share them with each other on the same Fiber. Truly per-call special variables would require saving and restoring them around each call of a shareable Proc, which seems too invasive for this bug fix.
This also matters for https://github.com/ruby/ruby/pull/19801 (making the body of define_method shareable when possible): without this fix, a define_method method using a regexp would overwrite $~ of a Thread.new/Fiber.new block calling it directly.