Project

General

Profile

Actions

Bug #15262

closed

WeakRef::RefError for object that is still in use

Added by larskanis (Lars Kanis) almost 4 years ago. Updated over 2 years ago.

Status:
Closed
Priority:
Normal
Assignee:
-
Target version:
-
ruby -v:
ruby 2.6.0dev (2018-10-27 trunk 65390) [x86_64-linux]
[ruby-core:89579]
Tags:

Description

Given the following program:

require "weakref"

Thread.abort_on_exception = true

class Adder
  def self.start_adder(obj, oid)
    obj.add
  end

  def initialize
    @qu = Queue.new
    count = 10
    count.times do
      Thread.new(WeakRef.new(self), object_id, &self.class.method(:start_adder))
    end
    count.times do
      @qu.pop
    end
  end

  def add
    @qu.push true
  end
end

def test_adder
  10.times.map do
    Thread.new do
      Adder.new
    end
  end.each(&:join)
end

1000.times do
  test_adder
end

Expected behaviour:

The program should simply execute without error. This is the case on JRuby but not on MRI.

Actual behavior:

The program stops with a probability of approximately 50% with the following error:

$ ruby -W2 adder-test.rb 
#<Thread:0x0000556e9e06f3f8@adder-test2.rb:6 run> terminated with exception (report_on_exception is true):
Traceback (most recent call last):
adder-test2.rb:7:in `start_adder': Invalid Reference - probably recycled (WeakRef::RefError)

Although start_adder works with a WeakRef, the Adder object should still be GC marked and therefore kept usable per WeakRef until Adder.new exited. This is because Adder.new waits for completion of all Threads to have called start_adder.

If Adder.start_adder is changed to catch the WeakRef error like so:

  def self.start_adder(obj, oid)
    obj.add
  rescue WeakRef::RefError
  end

... then the ruby VM detects a deadlock, which shows that @qu.pop is still executed within initialize. Therefore WeakRef#add should not raise a WeakRef::RefError to that point in time, but allow access to the object:

Traceback (most recent call last):
        5: from adder-test2.rb:35:in `<main>'
        4: from adder-test2.rb:35:in `times'
        3: from adder-test2.rb:36:in `block in <main>'
        2: from adder-test2.rb:32:in `test_adder'
        1: from adder-test2.rb:32:in `each'
adder-test2.rb:32:in `join': No live threads left. Deadlock? (fatal)
2 threads, 2 sleeps current:0x000056520aa3cee0 main thread:0x000056520a5f2ff0
* #<Thread:0x000056520a644b48 sleep_forever>
   rb_thread_t:0x000056520a5f2ff0 native:0x00007f7ccf535740 int:0
   adder-test2.rb:32:in `join'
   adder-test2.rb:32:in `each'
   adder-test2.rb:32:in `test_adder'
   adder-test2.rb:36:in `block in <main>'
   adder-test2.rb:35:in `times'
   adder-test2.rb:35:in `<main>'
* #<Thread:0x000056520a913040@adder-test2.rb:29 sleep_forever>
   rb_thread_t:0x000056520a609fc0 native:0x00007f7ca54d4700 int:0
    depended by: tb_thread_id:0x000056520a5f2ff0
   adder-test2.rb:18:in `pop'
   adder-test2.rb:18:in `block in initialize'
   adder-test2.rb:17:in `times'
   adder-test2.rb:17:in `initialize'
   adder-test2.rb:30:in `new'
   adder-test2.rb:30:in `block (2 levels) in test_adder'

I verified this on ruby-trunk, but get the same behavior on all older MRI versions.


Files

adder-test2.rb (476 Bytes) adder-test2.rb Simplified version of the initial exploit larskanis (Lars Kanis), 10/29/2018 08:34 AM
Actions

Also available in: Atom PDF