Project

General

Profile

Bug #11091

Updated by nobu (Nobuyoshi Nakada) over 9 years ago

See https://makandracards.com/makandra/32333-bugreport-symbolized-strings-break-keyword-arguments-in-ruby-2-2. 

 --- 
 **TL;DR** Under certain circumstances, dynamically defined symbols may break keyword arguments in Ruby 2.2. 

 Specifically, **when** … 

 
 - there is a method with several keyword arguments **and** a double-splat argument (e.g. `def m(foo: 'bar, option: 'will be lost', **further_options)`) 
 - there is a dynamically created `Symbol` (e.g. `'culprit'.to_sym`) that is _created_ **before** the method is _parsed_ 
 - the method gets called with both the `option` and a `culprit` keyword argument 

 … **then** the `option` keyword argument will be `nil` inside of `#m`. 

 ## Affected Ruby Versions 

 - Affected version: 2.2.1, 2.2.2 
 - Unaffected versions: 1.x, 2.0, 2.1 


 ## How to Expose it 

 The following code exposes the bug. Save it, make sure you have Ruby 2.2 and run it. 

 ```ruby ``` 
 test_symbol = '__test' 

 # The bug only occurs when a symbol used in a keyword argument is dynamically 
 # added to the Ruby symbols table *before* Ruby first sees the keyword argument. 
 existing = Symbol.all_symbols.map(&:to_s).grep('__test') 
 raise "Symbol #{test_symbol} already exists in symbol table!" if existing.any? 

 '__test'.to_sym # breaks it 
 # :__test # does not break it 

 # GC.start # fixes it 

 # Why #eval? 
 # Without, Ruby would parse the symbols in this code into its symbol table 
 # before running the file, which prevents the bug. 
 eval <<-RUBY 
   $hash = { __test: '__test', lost: 'lost', q: 'q' } 

   def _report(name, value) 
     puts name.to_s << ': ' << (value ? 'ok' : 'broken') 
   end 

   # Confirmed broken when: 
   # - `lost` is the second keyword argument Oo 
   # - there is a double-splat argument 
   def vulnerable_method_1(p: 'p', lost: 'lost', **options) 
     _report(__method__, lost) 
   end 

   def vulnerable_method_2(p: 'p', lost: 'lost', q: 'q', **options) 
     _report(__method__, lost) 
   end 

   def immune_method_1(lost: 'lost', p: 'p', **options) 
     _report(__method__, lost) 
   end 

   def immune_method_2(q: 'q', lost: 'lost', __test: '__test') 
     _report(__method__, lost) 
   end 

   def immune_method_3(lost: 'lost', **options) 
     _report(__method__, lost) 
   end 
 RUBY 

 # Exposure ##################################################################### 

 puts '', 'Broken when calling with a hash' 
 vulnerable_method_1($hash) 
 vulnerable_method_2($hash) 
 immune_method_1($hash) 
 immune_method_2($hash) 
 immune_method_3($hash) 

 puts '', 'Double splat (**) has no influence:' 
 vulnerable_method_1(**$hash) 
 vulnerable_method_2(**$hash) 
 immune_method_1(**$hash) 
 immune_method_2(**$hash) 
 immune_method_3(**$hash) 

 puts '', 'Hash order does not matter:' 
 inversed_hash = Hash[$hash.to_a.reverse] 
 vulnerable_method_1(inversed_hash) 
 vulnerable_method_2(inversed_hash) 
 immune_method_1(inversed_hash) 
 immune_method_2(inversed_hash) 
 immune_method_3(inversed_hash) 
 ``` 

 ## References 

 - Related (but does not fix it): [https://bugs.ruby-lang.org/issues/11027](https://bugs.ruby-lang.org/issues/11027)

Back