Bug #22521
open`Ractor.shareable_proc` allows outer variables reassigned by loops, and silently keeps a stale value
Description
Ractor.shareable_proc must reject blocks reading an outer variable which may be reassigned, because the shareable Proc copies the variable's value ([Feature #21557]). The "Loop Cases" section of #21557 lists cases which should be Ractor::IsolationError but are currently allowed, because the variable is assigned only once in the source:
# for loop: one variable, reassigned on each iteration
procs = []
for x in [1, 2]
procs << Ractor.shareable_proc { x }
end
p procs.map(&:call) # Ruby block semantics: [2, 2]
# while loop
procs = []
i = 0
while i < 2
y = i
procs << Ractor.shareable_proc { y }
i += 1
end
p procs.map(&:call) # [1, 1]
# begin/rescue/retry
procs = []
i = 0
begin
z = i
procs << Ractor.shareable_proc { z }
raise if (i += 1) < 2
rescue
retry
end
p procs.map(&:call) # [1, 1]
# redo in a block
procs = []
i = 0
[1].each do
w = i
procs << Ractor.shareable_proc { w }
redo if (i += 1) < 2
end
p procs.map(&:call) # [1, 1]
$ ruby -v loops.rb
ruby 4.1.0dev (2026-11-09T15:47:20Z master 7e5d882817) +PRISM [arm64-darwin25]
[1, 2]
[0, 1]
[0, 1]
[0, 1]
Same result on 4.0.7. With proc { x } instead of Ractor.shareable_proc { x }, the first case gives [2, 2].
Each shareable Proc silently keeps the value the variable had when it was created and ignores the later reassignments. This is exactly what #21557 wanted to prevent by raising Ractor::IsolationError. Since #22444 this also applies to Ractor.new blocks.
Cause¶
The static analysis marks a variable as reassigned only if the source contains more than one assignment to it. It does not consider that a single assignment can be executed several times for the same variable:
- in a
while/untilloop (including its condition), - in a
beginbody whoserescueusesretry, - in a block using
redo, which reruns the body in the same frame, - from a nested block, which may be called many times. This covers
forloops, whose variables belong to the enclosing scope and are assigned from the loop's block.
Fix¶
PR: https://github.com/ruby/ruby/pull/19802
The fix treats such assignments as reassignments, in both prism_compile.c and compile.c. Assignments from rescue/ensure clauses are also level > 0 and are conservatively treated the same way. Blocks still get a new frame per call, so this keeps working:
With the fix, the cases above raise:
cannot make a shareable Proc because the outer variable 'x' may be reassigned. (Ractor::IsolationError)
Code which currently creates such a shareable Proc gets Ractor::IsolationError after this fix, but it is currently silently getting stale values, so I don't think this should be backported.
Updated by Eregon (Benoit Daloze) about 20 hours ago
- Related to Feature #21557: Ractor.shareable_proc to make sharable Proc objects, safely and flexibly added