My least favourite backtrace is a backtrace that doesn’t include my own code.
Tigerbrew on OS X 10.4 uses Ruby 1.8.2, which was shipped on Christmas Day, 2004, and it has more than its fair share of interesting bugs. In today’s lesson we break Ruby’s stdlib class OpenStruct.
1 2 3
Homebrew uses OpenStruct instances in place of hashes in code which only performs reading and writing of attributes, without using any other hash features. For example, in the
deps command, OpenStruct is used for read-only access to a set of attributes read from ARGV:
1 2 3 4 5 6 7 8 9 10
The first time I ran
brew deps in Tigerbrew, however, I was greeted with this lovely backtrace:
1 2 3 4 5 6 7 8 9 10 11
Given that the backtrace includes only stdlib code and nothing I wrote, I wasn’t sure how to interpret this until I saw “(eval)”. It couldn’t be, could it…? Of course it was.
Accessors for attribute of OpenStruct instances are methods, and they are defined by OpenStruct a) whenever a new attribute is assigned, and b) when OpenStruct is initialized with a hash. This is achieved using the method
OpenStruct#new_ostruct_member1, which was defined like this in Ruby 1.8.2:
1 2 3 4 5 6 7 8
Yes: OpenStruct dynamically defines method names by interpolating the name of the variable into a string and evaluating the string in the context of the object. Unsurprisingly, this is very fragile. In our example, the attributes being defined end with a question mark;
#installed? is a valid method name in Ruby, but
#installed?= is not, and so a SyntaxError exception is raised inside
This was eventually fixed2; in Ruby 2.2.2’s definition, the
#define_singleton_method method is used instead; metaprogramming is not limited to the normal naming restrictions, so the unusual setters are defined properly3.
1 2 3 4 5 6 7 8
Thankfully, the definiton of the method from modern versions of Ruby is fully compatible with Ruby 1.8.2, so Tigerbrew ships with a backported version of