dmendel / bindata

BinData - Reading and Writing Binary Data in Ruby
BSD 2-Clause "Simplified" License
577 stars 55 forks source link

Race condition when creating int primitives #108

Closed jbpeirce closed 6 years ago

jbpeirce commented 6 years ago

Because the byte- and bit-based integer primitives use const_missing to create new classes, there is a race condition when two threads create the same class in quick succession. The first thread to use a class will trigger define_class on the Int or BitField module. However, a second thread can use the class before all of its methods have been defined by the call to module_eval, causing a NotImplementedException to be thrown from the BasePrimitive class.

I am not a Ruby programmer, so am not certain if there is a solution to this race condition that preserves flexible potential sizes for integers. (The float primitive appears to avoid the race condition by initializing classes in the module rather than relying on const_missing.)

A concrete example using the Uint64beclass:

Given the following Ruby script, which creates BinData::Uint64be in two threads in quick succession and then calls to_binary_s on each:

require 'bindata'

module Foo
 class << self
   def int_thing(arg1)
     puts "#{arg1} says #{BinData::Uint64be.new(0).to_binary_s.inspect}"
   end
 end
end

t1 = Thread.new { Foo.int_thing(1) }; t2 = Thread.new { Foo.int_thing(2) }
t1.join; t2.join

running the script in an infinite loop on ruby 2.4.4 produces:

> while ((i++)); echo $i; ruby foo.rb; done
0
1 says "\x00\x00\x00\x00\x00\x00\x00\x00"
2 says "\x00\x00\x00\x00\x00\x00\x00\x00"

...

1012
2 says "\x00\x00\x00\x00\x00\x00\x00\x00"
1 says "\x00\x00\x00\x00\x00\x00\x00\x00"

1013
.../.rvm/gems/ruby-2.4.4/gems/bindata-2.4.3/lib/bindata/base_primitive.rb:232:in `value_to_binary_string': NotImplementedError (NotImplementedError)
    from .../.rvm/gems/ruby-2.4.4/gems/bindata-2.4.3/lib/bindata/base_primitive.rb:133:in `do_write'
    from .../.rvm/gems/ruby-2.4.4/gems/bindata-2.4.3/lib/bindata/base.rb:158:in `write'
    from .../.rvm/gems/ruby-2.4.4/gems/bindata-2.4.3/lib/bindata/base.rb:174:in `to_binary_s'
    from foo.rb:6:in `int_thing'
    from foo.rb:11:in `block in <main>'
1 says "\x00\x00\x00\x00\x00\x00\x00\x00"
...

with identical errors thrown at iteration 1088 and 1251.

cc @wtmcc who also investigated the bug and constructed the example above

dmendel commented 6 years ago

Thanks for the detailed report.

Fixed in latest release (2.4.4)

cc @wtmcc