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
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 triggerdefine_class
on theInt
orBitField
module. However, a second thread can use the class before all of its methods have been defined by the call tomodule_eval
, causing aNotImplementedException
to be thrown from theBasePrimitive
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
Uint64be
class:Given the following Ruby script, which creates
BinData::Uint64be
in two threads in quick succession and then callsto_binary_s
on each:running the script in an infinite loop on
ruby 2.4.4
produces:with identical errors thrown at iteration 1088 and 1251.
cc @wtmcc who also investigated the bug and constructed the example above