Project

General

Profile

Actions

Bug #21368

closed

Moving objects with finalizer between Ractors crashes

Bug #21368: Moving objects with finalizer between Ractors crashes

Added by peterzhu2118 (Peter Zhu) over 1 year ago. Updated 21 days ago.

Status:
Closed
Assignee:
Target version:
-
[ruby-core:122261]

Description

When an object is moved to a different Ractor, the finalizers are not copied to the new object, so it will have the FL_FINALIZE flag set but no entry in the finalizer table.

The following script crashes:

r = Ractor.new do
  loop { Ractor.receive }
end

1_000.times do
  o = Object.new
  ObjectSpace.define_finalizer(o, proc { |id| })
  r.send(o, move: true)
end

Files

Updated by peterzhu2118 (Peter Zhu) over 1 year ago Actions #1 [ruby-core:122262]

  • Assignee set to ractor

Updated by zzak (zzak _) over 1 year ago Actions #2 [ruby-core:122312]

It seemed fun to patch, but I'm not sure this is correct:
https://github.com/ruby/ruby/pull/13452

Updated by hsbt (Hiroshi SHIBATA) over 1 year ago Actions #3

  • Status changed from Open to Assigned

Updated by osyoyu (Daisuke Aritomo) over 1 year ago Actions #4 [ruby-core:122328]

The script in the description didn't crash in my environments (the following). Explicitly calling GC.start did.

  • Linux ruby 3.5.0dev (2025-05-28T04:34:40Z master d064fd067b) +PRISM [x86_64-linux]
  • macOS ruby 3.5.0dev (2025-05-28T04:34:40Z master d064fd067b) +PRISM [arm64-darwin24]
r = Ractor.new do
  Ractor.receive # don't bind to a variable (let it be garbage collected)
  GC.start
end

o = Object.new
ObjectSpace.define_finalizer(o, proc { |id| })
r.send(o, move: true)

r.take

I have attached an updated version of @zzak (zzak _)_'s patch which resolves this crash, but I suppose this is inappropiate since this will leak outer variables in the following case:

r = Ractor.new do
  Ractor.receive
  GC.start # `unshareable` happens to get read in this Ractor
end

o = Object.new
unshareable = +"hello"
ObjectSpace.define_finalizer(o, proc { |id| p unshareable })
r.send(o, move: true)

r.take

Maybe simply marking objects with a finalizer ineligible for moving is more appropiate.

Updated by zzak (zzak _) over 1 year ago Actions #5 [ruby-core:122494]

Maybe simply marking objects with a finalizer ineligible for moving is more appropiate.

Thanks for checking, I've updated the PR to raise if the object has a finalizer. I'm not sure if we should do the check in like make_shareable_check_shareable instead, for example.

Updated by osyoyu (Daisuke Aritomo) over 1 year ago Actions #6 [ruby-core:122514]

Maybe this ticket should be merged with https://bugs.ruby-lang.org/issues/21315 ?

Updated by ko1 (Koichi Sasada) 21 days ago Actions #7

  • Status changed from Assigned to Closed

Applied in changeset git|11a54ae0f9e1ac7d64fbf64c411e00b37e4dbe49.


Keep FL_FINALIZE on the shell an object leaves behind when it is moved

move_neutralize_source() rewrites the source's flags to
T_OBJECT | FL_FREEZE | (flags & FL_PROMOTED), which drops FL_FINALIZE
while the finalizer table entry keyed on that slot stays. The two then
disagree, and a RUBY_DEBUG build aborts at shutdown:

gc/default/default.c:4044: Assertion Failed:
rb_gc_impl_shutdown_call_finalizer_i:RB_FL_TEST(obj, FL_FINALIZE)

r = Ractor.new { Ractor.receive }
o = Object.new
ObjectSpace.define_finalizer(o, proc { |id| })
r.send(o, move: true)

Carry FL_FINALIZE over to the shell. The finalizer then runs when the
shell is collected, in the Ractor that defined it; the object rebuilt on
the other side gets fresh flags and does not inherit it, so it still runs
exactly once.

[Bug #21368]

Co-Authored-By: Claude Opus 5 (1M context)

Actions

Also available in: PDF Atom