|
# Exit status: 0 clean, 1 corruption, 2 unlucky hash salt (rerun).
|
|
|
|
module Ref
|
|
# Even though the refinement is lexically scoped to the module, it globally disables the interpreter's ability to do
|
|
# fast-path integer comparisons.
|
|
refine Integer do
|
|
def ==(other) = super
|
|
end
|
|
end
|
|
|
|
N = 128 # must exceed 31; higher makes reproducing easier
|
|
|
|
# Figure out two argument indices:
|
|
# - above index 31 so they use the hash path
|
|
# - which collide in the 8-bit ar_hint that small hashes use for the linear scan
|
|
hint = ->(i) { i.hash & 0xff }
|
|
a, b = (0...N).to_a.combination(2).find { |x, y| hint[x] == hint[y] && y >= 31 }
|
|
if b.nil?
|
|
warn "no two keyword indices collide under this process's hash salt; rerun"
|
|
exit 2
|
|
end
|
|
|
|
# Only the two colliding indices get a call-time-evaluated default, so the Hash holds
|
|
# exactly those two keys; every other keyword gets a literal default, which is
|
|
# written into the locals once and never recomputed.
|
|
params = (0...N).map { |i| [a, b].include?(i) ? "k#{i}: {}" : "k#{i}: #{i}" }
|
|
eval "def victim(#{params.join(', ')}) = [#{(0...N).map { |i| "k#{i}" }.join(', ')}]"
|
|
|
|
# The victim method just returns its arguments, which *should* all be consecutive integers
|
|
# except for the two collision candidates which should be {}
|
|
got = victim
|
|
bad = (0...N).reject { |i| [a, b].include?(i) || got[i] == i }
|
|
|
|
puts RUBY_DESCRIPTION
|
|
puts "colliding keyword indices #{a}/#{b}"
|
|
if bad.empty?
|
|
puts " clean"
|
|
else
|
|
bad.each { |i| puts " k#{i}: expected #{i}, got #{got[i].inspect[0, 48]}" }
|
|
end
|
|
exit(bad.empty? ? 0 : 1)
|