- Ruby's range syntax has lower precedence than method invocation, so
1..16.sizereturns theRange1..8(probably1..4on 32-bit systems), while(1..16).sizereturns theInteger16. Because of this, we need to wrap a range in parentheses or store it in a variable when we want to call a method on the range. - I missed the parentheses around a range during a late night coding session.
- Ruby parsed this as a flip-flop operator but my program appeared to work normally.
- My test suite uses SimpleCov to gather code coverage metrics.
- Ruby 3.4 switched to the PRISM parser.
- The PRISM compiler was generating a line number of zero for flip-flop operators.
- The Ruby code coverage system stores line counts in an
Array. - A line number of zero would reference memory before the start of the
Array. - Memory allocators use tags before and/or after allocated blocks to record information about the allocation.
- Writing the line count for line zero corrupted the heap by overwriting the allocator's tags.
- The program would crash some time later when the garbage collector deallocated memory and libc found the heap corruption.
- mb-sound issue 36
- Ruby issue 21220
- Ruby issue 21259
Back in March of 2025 I started what should have been a straightforward Ruby version upgrade. Little did I know I was about to fall down a multi-day rabbit hole that would ultimately reveal a memory corruption bug (now fixed) deep within core Ruby systems.
Often when you make a typo or miss an operator in code the program simply doesn’t work. But there are those rare cases where you get a subtle, insidious change that appears to work correctly. This is the latter case, with missing parentheses leading to a crash due to memory corruption.
Here’s a bit of foreshadowing (did you know that Ruby has an operator called the flip-flop operator?):
1 2 3 | |
How will these two lines behave?
Keep reading to see how code coverage, automated testing, uncommon operators, and out-of-bounds array writes all converge in a very unexpected way.





