Hash#rehash corrupts the internal insertion-order doubly-linked list when it deduplicates keys (i.e., when mutated keys now compare as equal). After the corrupted rehash, inserting new entries disconnects existing entries from iteration, and calling h.keys can trigger a NullPointerException.
Root Cause
In RubyHash#rehash, when a duplicate key is found and its entry is unlinked from the insertion-order list:
RubyHashEntry tmpNext = entry.nextAdded;
RubyHashEntry tmpPrev = entry.prevAdded;
tmpPrev.nextAdded = tmpNext; // correct: prev.next skips over entry
tmpPrev.prevAdded = tmpPrev; // BUG: should be tmpNext.prevAdded = tmpPrev
Reproducer
a = [1]; b = [2]
h = { a => "a", b => "b" }
a[0] = 2
h.rehash
h[[3]] = "c"
expected = [[2], [3]]
actual = []
h.each { |k, v| actual << k }
if actual == expected
puts "PASS: each yields #{actual.inspect}"
else
puts "FAIL: each yields #{actual.inspect}, expected #{expected.inspect}"
end
# h.keys.inspect triggers NullPointerException on JRuby
# puts h.keys.inspect
CRuby 3.4.8: PASS: each yields [[2], [3]]
JRuby (master): FAIL: each yields [[3]], expected [[2], [3]]
Uncommenting h.keys.inspect produces:
Unhandled Java exception: java.lang.NullPointerException: Cannot read field "metaClass" because "arg" is null
Hash#rehashcorrupts the internal insertion-order doubly-linked list when it deduplicates keys (i.e., when mutated keys now compare as equal). After the corrupted rehash, inserting new entries disconnects existing entries from iteration, and callingh.keyscan trigger aNullPointerException.Root Cause
In
RubyHash#rehash, when a duplicate key is found and its entry is unlinked from the insertion-order list:Reproducer
CRuby 3.4.8:
PASS: each yields [[2], [3]]JRuby (master):
FAIL: each yields [[3]], expected [[2], [3]]Uncommenting
h.keys.inspectproduces: