From: "Eregon (Benoit Daloze) via ruby-core" Date: 2026-11-09T19:22:01+00:00 Subject: [ruby-core:127387] [Ruby Bug#22525] Ractor.shareable_proc sees nil for a block parameter of the method which was not read yet Issue #22525 has been reported by Eregon (Benoit Daloze). ---------------------------------------- Bug #22525: Ractor.shareable_proc sees nil for a block parameter of the method which was not read yet https://bugs.ruby-lang.org/issues/22525 * 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 ---------------------------------------- When a shareable Proc reads a block parameter (`&b`) of the enclosing method, it gets `nil` if the method did not read `b` before. It does not raise `Ractor::IsolationError` even when the block is not shareable, and it loses the block even when the block is shareable: ```ruby def read_block(&b) = Ractor.shareable_proc { b } p read_block { 1 }.call # expected: Ractor::IsolationError, as the block is not shareable shareable = Ractor.shareable_proc { 2 } p read_block(&shareable).call # expected: the shareable Proc ``` ``` $ ruby -v blockparam.rb ruby 4.1.0dev (2026-11-09T15:47:20Z master 7e5d882817) +PRISM [arm64-darwin25] nil nil ``` Same result on 4.0.7. Since #22444 this also applies to `Ractor.new` blocks. ## Cause The block parameter of a method is only set when it is first read: `getblockparam` (and `getblockparamproxy` for `b.call`) creates the Proc from the block handler of the method's frame and stores it in the local variable slot. Until then, the slot holds `nil`. When making a Proc shareable, `env_copy` copies the slot, which is still `nil`. The copied environment has no block handler, so reading `b` in the shareable Proc gives `nil`. `Binding#local_variable_get` already handles this case (`get_local_variable_ptr` in `proc.c`) by setting the block parameter first. Additionally, `b.call`, `b[]`, `b.yield` and `b === x` are compiled to `getblockparamproxy` without recording `b` as an outer variable of the block, so for these `b` is not checked nor copied at all: ```ruby def call_block(&b) = Ractor.shareable_proc { b.call } call_block { 1 } # no error, the shareable Proc raises NoMethodError when called ``` ## Fix PR: https://github.com/ruby/ruby/pull/19808 The fix does the same in `env_copy`: it sets the block parameter before checking whether its value is shareable and copying it. It also marks the block parameter as set in the copied environment, so that the shareable Proc reads the copied value. It also records `b` as an outer variable for `b.call` and the like, in both `prism_compile.c` and `compile.c`. This also matters for https://github.com/ruby/ruby/pull/19801 (making the body of `define_method` shareable when possible). With that change, this pattern from `rspec-support` (and similar code in ActiveRecord) got `nil` instead of the block: ```ruby def self.define_optimized_require_for_rspec(lib, &require_relative) define_singleton_method("require_rspec_#{lib}") do |f| require_relative.call("#{lib}/#{f}") end end ``` With the fix, such a body is not made shareable, because the block is not shareable, and the method works as before. -- 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/