From: "Eregon (Benoit Daloze) via ruby-core" Date: 2026-11-09T15:51:27+00:00 Subject: [ruby-core:127381] [Ruby Bug#22521] `Ractor.shareable_proc` allows outer variables reassigned by loops, and silently keeps a stale value Issue #22521 has been reported by Eregon (Benoit Daloze). ---------------------------------------- Bug #22521: `Ractor.shareable_proc` allows outer variables reassigned by loops, and silently keeps a stale value https://bugs.ruby-lang.org/issues/22521 * Author: Eregon (Benoit Daloze) * Status: Open * Target version: 4.1 * ruby -v: ruby 4.1.0dev (2026-11-09T15:47:20Z master 7e5d882817) +PRISM [arm64-darwin25] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- `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: ```ruby # 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`/`until` loop (including its condition), * in a `begin` body whose `rescue` uses `retry`, * 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 `for` loops, 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: ```ruby [1, 2].each do |e| f = e * 2 Ractor.shareable_proc { [e, f] } # OK end ``` 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. -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/