mirror of
https://github.com/ruby/ruby.git
synced 2026-01-29 05:24:23 +00:00
[Bug #20688] We cannot free the weakmap_entry before the ST_DELETE because it could hash the key which would read the weakmap_entry and would cause a use-after-free. Instead, we store the entry and free it on the next iteration. For example, the following script triggers a use-after-free in Valgrind: weakmap = ObjectSpace::WeakMap.new 10_000.times { weakmap[Object.new] = Object.new } ==25795== Invalid read of size 8 ==25795== at 0x462297: wmap_cmp (weakmap.c:165) ==25795== by 0x3A2B1C: find_table_bin_ind (st.c:930) ==25795== by 0x3A5EAA: st_general_foreach (st.c:1599) ==25795== by 0x3A5EAA: rb_st_foreach (st.c:1640) ==25795== by 0x25C991: gc_mark_children (default.c:4870) ==25795== by 0x25C991: gc_marks_wb_unprotected_objects_plane (default.c:5565) ==25795== by 0x25C991: rgengc_rememberset_mark_plane (default.c:5557) ==25795== by 0x25C991: rgengc_rememberset_mark (default.c:6233) ==25795== by 0x25C991: gc_marks_start (default.c:6057) ==25795== by 0x25C991: gc_marks (default.c:6077) ==25795== by 0x25C991: gc_start (default.c:6723) ==25795== by 0x260F96: heap_prepare (default.c:2282) ==25795== by 0x260F96: heap_next_free_page (default.c:2489) ==25795== by 0x260F96: newobj_cache_miss (default.c:2598) ==25795== by 0x26197F: newobj_alloc (default.c:2622) ==25795== by 0x26197F: rb_gc_impl_new_obj (default.c:2701) ==25795== by 0x26197F: newobj_of (gc.c:890) ==25795== by 0x26197F: rb_wb_protected_newobj_of (gc.c:917) ==25795== by 0x2DEA88: rb_class_allocate_instance (object.c:131) ==25795== by 0x2E3B18: class_call_alloc_func (object.c:2141) ==25795== by 0x2E3B18: rb_class_alloc (object.c:2113) ==25795== by 0x2E3B18: rb_class_new_instance_pass_kw (object.c:2172) ==25795== by 0x429DDC: vm_call_cfunc_with_frame_ (vm_insnhelper.c:3786) ==25795== by 0x44B08D: vm_sendish (vm_insnhelper.c:5953) ==25795== by 0x44B08D: vm_exec_core (insns.def:898) ==25795== by 0x43A7A4: rb_vm_exec (vm.c:2564) ==25795== by 0x234914: rb_ec_exec_node (eval.c:281) ==25795== Address 0x21603710 is 0 bytes inside a block of size 16 free'd ==25795== at 0x4849B2C: free (vg_replace_malloc.c:989) ==25795== by 0x249651: rb_gc_impl_free (default.c:8527) ==25795== by 0x249651: rb_gc_impl_free (default.c:8508) ==25795== by 0x249651: ruby_sized_xfree.constprop.0 (gc.c:4178) ==25795== by 0x4626EC: ruby_sized_xfree_inlined (gc.h:277) ==25795== by 0x4626EC: wmap_free_entry (weakmap.c:45) ==25795== by 0x4626EC: wmap_mark_weak_table_i (weakmap.c:61) ==25795== by 0x3A5CEF: apply_functor (st.c:1633) ==25795== by 0x3A5CEF: st_general_foreach (st.c:1543) ==25795== by 0x3A5CEF: rb_st_foreach (st.c:1640) ==25795== by 0x25C991: gc_mark_children (default.c:4870) ==25795== by 0x25C991: gc_marks_wb_unprotected_objects_plane (default.c:5565) ==25795== by 0x25C991: rgengc_rememberset_mark_plane (default.c:5557) ==25795== by 0x25C991: rgengc_rememberset_mark (default.c:6233) ==25795== by 0x25C991: gc_marks_start (default.c:6057) ==25795== by 0x25C991: gc_marks (default.c:6077) ==25795== by 0x25C991: gc_start (default.c:6723) ==25795== by 0x260F96: heap_prepare (default.c:2282) ==25795== by 0x260F96: heap_next_free_page (default.c:2489) ==25795== by 0x260F96: newobj_cache_miss (default.c:2598) ==25795== by 0x26197F: newobj_alloc (default.c:2622) ==25795== by 0x26197F: rb_gc_impl_new_obj (default.c:2701) ==25795== by 0x26197F: newobj_of (gc.c:890) ==25795== by 0x26197F: rb_wb_protected_newobj_of (gc.c:917) ==25795== by 0x2DEA88: rb_class_allocate_instance (object.c:131) ==25795== by 0x2E3B18: class_call_alloc_func (object.c:2141) ==25795== by 0x2E3B18: rb_class_alloc (object.c:2113) ==25795== by 0x2E3B18: rb_class_new_instance_pass_kw (object.c:2172) ==25795== by 0x429DDC: vm_call_cfunc_with_frame_ (vm_insnhelper.c:3786) ==25795== by 0x44B08D: vm_sendish (vm_insnhelper.c:5953) ==25795== by 0x44B08D: vm_exec_core (insns.def:898) ==25795== by 0x43A7A4: rb_vm_exec (vm.c:2564) ==25795== Block was alloc'd at ==25795== at 0x484680F: malloc (vg_replace_malloc.c:446) ==25795== by 0x25CE9E: rb_gc_impl_malloc (default.c:8542) ==25795== by 0x462A39: wmap_aset_replace (weakmap.c:423) ==25795== by 0x3A5542: rb_st_update (st.c:1487) ==25795== by 0x462B8E: wmap_aset (weakmap.c:452) ==25795== by 0x429DDC: vm_call_cfunc_with_frame_ (vm_insnhelper.c:3786) ==25795== by 0x44B08D: vm_sendish (vm_insnhelper.c:5953) ==25795== by 0x44B08D: vm_exec_core (insns.def:898) ==25795== by 0x43A7A4: rb_vm_exec (vm.c:2564) ==25795== by 0x234914: rb_ec_exec_node (eval.c:281) ==25795== by 0x2369B8: ruby_run_node (eval.c:319) ==25795== by 0x15D675: rb_main (main.c:43) ==25795== by 0x15D675: main (main.c:62)
269 lines
5.5 KiB
Ruby
269 lines
5.5 KiB
Ruby
# frozen_string_literal: false
|
|
require 'test/unit'
|
|
|
|
class TestWeakMap < Test::Unit::TestCase
|
|
def setup
|
|
@wm = ObjectSpace::WeakMap.new
|
|
end
|
|
|
|
def test_map
|
|
x = Object.new
|
|
k = "foo"
|
|
@wm[k] = x
|
|
assert_same(x, @wm[k])
|
|
assert_not_same(x, @wm["FOO".downcase])
|
|
end
|
|
|
|
def test_aset_const
|
|
x = Object.new
|
|
@wm[true] = x
|
|
assert_same(x, @wm[true])
|
|
@wm[false] = x
|
|
assert_same(x, @wm[false])
|
|
@wm[nil] = x
|
|
assert_same(x, @wm[nil])
|
|
@wm[42] = x
|
|
assert_same(x, @wm[42])
|
|
@wm[:foo] = x
|
|
assert_same(x, @wm[:foo])
|
|
|
|
@wm[x] = true
|
|
assert_same(true, @wm[x])
|
|
@wm[x] = false
|
|
assert_same(false, @wm[x])
|
|
@wm[x] = nil
|
|
assert_same(nil, @wm[x])
|
|
@wm[x] = 42
|
|
assert_same(42, @wm[x])
|
|
@wm[x] = :foo
|
|
assert_same(:foo, @wm[x])
|
|
end
|
|
|
|
def assert_weak_include(m, k, n = 100)
|
|
if n > 0
|
|
return assert_weak_include(m, k, n-1)
|
|
end
|
|
1.times do
|
|
x = Object.new
|
|
@wm[k] = x
|
|
assert_send([@wm, m, k])
|
|
assert_not_send([@wm, m, "FOO".downcase])
|
|
x = Object.new
|
|
end
|
|
end
|
|
|
|
def test_include?
|
|
m = __callee__[/test_(.*)/, 1]
|
|
k = "foo"
|
|
1.times do
|
|
assert_weak_include(m, k)
|
|
end
|
|
GC.start
|
|
pend('TODO: failure introduced from 837fd5e494731d7d44786f29e7d6e8c27029806f')
|
|
assert_not_send([@wm, m, k])
|
|
end
|
|
alias test_member? test_include?
|
|
alias test_key? test_include?
|
|
|
|
def test_inspect
|
|
x = Object.new
|
|
k = BasicObject.new
|
|
@wm[k] = x
|
|
assert_match(/\A\#<#{@wm.class.name}:[^:]+:\s\#<BasicObject:[^:]*>\s=>\s\#<Object:[^:]*>>\z/,
|
|
@wm.inspect)
|
|
end
|
|
|
|
def test_inspect_garbage
|
|
1000.times do |i|
|
|
@wm[i] = Object.new
|
|
@wm.inspect
|
|
end
|
|
assert_match(/\A\#<#{@wm.class.name}:0x[\da-f]+(?::(?: \d+ => \#<(?:Object|collected):0x[\da-f]+>,?)+)?>\z/,
|
|
@wm.inspect)
|
|
end
|
|
|
|
def test_delete
|
|
k1 = "foo"
|
|
x1 = Object.new
|
|
@wm[k1] = x1
|
|
assert_equal x1, @wm[k1]
|
|
assert_equal x1, @wm.delete(k1)
|
|
assert_nil @wm[k1]
|
|
assert_nil @wm.delete(k1)
|
|
|
|
fallback = @wm.delete(k1) do |key|
|
|
assert_equal k1, key
|
|
42
|
|
end
|
|
assert_equal 42, fallback
|
|
end
|
|
|
|
def test_each
|
|
m = __callee__[/test_(.*)/, 1]
|
|
x1 = Object.new
|
|
k1 = "foo"
|
|
@wm[k1] = x1
|
|
x2 = Object.new
|
|
k2 = "bar"
|
|
@wm[k2] = x2
|
|
n = 0
|
|
@wm.__send__(m) do |k, v|
|
|
assert_match(/\A(?:foo|bar)\z/, k)
|
|
case k
|
|
when /foo/
|
|
assert_same(k1, k)
|
|
assert_same(x1, v)
|
|
when /bar/
|
|
assert_same(k2, k)
|
|
assert_same(x2, v)
|
|
end
|
|
n += 1
|
|
end
|
|
assert_equal(2, n)
|
|
end
|
|
|
|
def test_each_key
|
|
x1 = Object.new
|
|
k1 = "foo"
|
|
@wm[k1] = x1
|
|
x2 = Object.new
|
|
k2 = "bar"
|
|
@wm[k2] = x2
|
|
n = 0
|
|
@wm.each_key do |k|
|
|
assert_match(/\A(?:foo|bar)\z/, k)
|
|
case k
|
|
when /foo/
|
|
assert_same(k1, k)
|
|
when /bar/
|
|
assert_same(k2, k)
|
|
end
|
|
n += 1
|
|
end
|
|
assert_equal(2, n)
|
|
end
|
|
|
|
def test_each_value
|
|
x1 = "foo"
|
|
k1 = Object.new
|
|
@wm[k1] = x1
|
|
x2 = "bar"
|
|
k2 = Object.new
|
|
@wm[k2] = x2
|
|
n = 0
|
|
@wm.each_value do |v|
|
|
assert_match(/\A(?:foo|bar)\z/, v)
|
|
case v
|
|
when /foo/
|
|
assert_same(x1, v)
|
|
when /bar/
|
|
assert_same(x2, v)
|
|
end
|
|
n += 1
|
|
end
|
|
assert_equal(2, n)
|
|
end
|
|
|
|
def test_size
|
|
m = __callee__[/test_(.*)/, 1]
|
|
assert_equal(0, @wm.__send__(m))
|
|
x1 = "foo"
|
|
k1 = Object.new
|
|
@wm[k1] = x1
|
|
assert_equal(1, @wm.__send__(m))
|
|
x2 = "bar"
|
|
k2 = Object.new
|
|
@wm[k2] = x2
|
|
assert_equal(2, @wm.__send__(m))
|
|
end
|
|
alias test_length test_size
|
|
|
|
def test_frozen_object
|
|
o = Object.new.freeze
|
|
assert_nothing_raised(FrozenError) {@wm[o] = 'foo'}
|
|
assert_nothing_raised(FrozenError) {@wm['foo'] = o}
|
|
end
|
|
|
|
def test_no_memory_leak
|
|
assert_no_memory_leak([], '', "#{<<~"begin;"}\n#{<<~'end;'}", "[Bug #19398]", rss: true, limit: 1.5, timeout: 60)
|
|
begin;
|
|
1_000_000.times do
|
|
ObjectSpace::WeakMap.new
|
|
end
|
|
end;
|
|
end
|
|
|
|
def test_compaction
|
|
omit "compaction is not supported on this platform" unless GC.respond_to?(:compact)
|
|
|
|
# [Bug #19529]
|
|
obj = Object.new
|
|
100.times do |i|
|
|
GC.compact
|
|
@wm[i] = obj
|
|
end
|
|
|
|
assert_separately([], <<-'end;')
|
|
wm = ObjectSpace::WeakMap.new
|
|
obj = Object.new
|
|
100.times do
|
|
wm[Object.new] = obj
|
|
GC.start
|
|
end
|
|
GC.compact
|
|
end;
|
|
|
|
assert_separately(%w(-robjspace), <<-'end;')
|
|
wm = ObjectSpace::WeakMap.new
|
|
key = Object.new
|
|
val = Object.new
|
|
wm[key] = val
|
|
|
|
GC.verify_compaction_references(expand_heap: true, toward: :empty)
|
|
|
|
assert_equal(val, wm[key])
|
|
end;
|
|
|
|
assert_separately(["-W0"], <<-'end;')
|
|
wm = ObjectSpace::WeakMap.new
|
|
|
|
ary = 10_000.times.map do
|
|
o = Object.new
|
|
wm[o] = 1
|
|
o
|
|
end
|
|
|
|
GC.verify_compaction_references(expand_heap: true, toward: :empty)
|
|
end;
|
|
end
|
|
|
|
def test_gc_compact_stress
|
|
omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077
|
|
EnvUtil.under_gc_compact_stress { ObjectSpace::WeakMap.new }
|
|
end
|
|
|
|
def test_replaced_values_bug_19531
|
|
a = "A".dup
|
|
b = "B".dup
|
|
|
|
@wm[1] = a
|
|
@wm[1] = a
|
|
@wm[1] = a
|
|
|
|
@wm[1] = b
|
|
assert_equal b, @wm[1]
|
|
|
|
a = nil
|
|
GC.start
|
|
|
|
assert_equal b, @wm[1]
|
|
end
|
|
|
|
def test_use_after_free_bug_20688
|
|
assert_normal_exit(<<~RUBY)
|
|
weakmap = ObjectSpace::WeakMap.new
|
|
10_000.times { weakmap[Object.new] = Object.new }
|
|
RUBY
|
|
end
|
|
end
|