The Future Is Now

Reevaluate

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.

OpenStruct is a simple data structure that provides a JavaScript object-like interface to Ruby hashes. It’s essentially a hash that provides getter and setter methods for each defined attribute. For example:

1
2
3
os = OpenStruct.new
os.key = 'value'
os.key #=> 'value'

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
mode = OpenStruct.new(
  :installed?  => ARGV.include?('--installed'),
  :tree?       => ARGV.include?('--tree'),
  :all?        => ARGV.include?('--all'),
  :topo_order? => ARGV.include?('-n'),
  :union?      => ARGV.include?('--union')
)

if mode.installed? && mode.tree?
  # ...

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
SyntaxError: (eval):3:in `instance_eval': compile error
(eval):3: syntax error
        def topo_order?=(x); @table[:topo_order?] = x; end
                       ^
(eval):3: syntax error
    from /usr/lib/ruby/1.8/ostruct.rb:72:in `instance_eval'
    from /usr/lib/ruby/1.8/ostruct.rb:72:in `instance_eval'
    from /usr/lib/ruby/1.8/ostruct.rb:72:in `new_ostruct_member'
    from /usr/lib/ruby/1.8/ostruct.rb:51:in `initialize'
    from /usr/lib/ruby/1.8/ostruct.rb:49:in `each'
    from /usr/lib/ruby/1.8/ostruct.rb:49:in `initialize'

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
def new_ostruct_member(name)
  unless self.respond_to?(name)
    self.instance_eval %{
      def #{name}; @table[:#{name}]; end
      def #{name}=(x); @table[:#{name}] = x; end
    }
  end
end

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 eval.

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
def new_ostruct_member(name)
  name = name.to_sym
  unless respond_to?(name)
    define_singleton_method(name) { @table[name] }
    define_singleton_method("#{name}=") { |x| modifiable[name] = x }
  end
  name
end

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 OpenStruct#new_ostruct_member.


  1. This sounds like it should be a private method, and is documented as being “used internally”, but for some reason this was a public instance method right up until Ruby 2.0.

  2. Close to a year after Ruby 1.8.2 was released.

  3. These illegal method names can’t be called using the normal syntax, but they can be called via metaprogramming using the #send instance method, e.g. os.send "foo?=", "baz"