Skip to content

Commit 085e99a

Browse files
jhawthornXrXr
andcommittedMar 6, 2025
Replace tombstone when converting AR to ST hash
[Bug #21170] st_table reserves -1 as a special hash value to indicate that an entry has been deleted. So that that's a valid value to be returned from the hash function, do_hash replaces -1 with 0 so that it is not mistaken for the sentinel. Previously, when upgrading an AR table to an ST table, rb_st_add_direct_with_hash was used which did not perform the same conversion, this could lead to a hash in a broken state where one if its entries which was supposed to exist being marked as a tombstone. The hash could then become further corrupted when the ST table required resizing as the falsely tombstoned entry would be skipped but it would be counted in num entries, leading to an uninitialized entry at index 15. In most cases this will be really rare, unless using a very poorly implemented custom hash function. This also adds two debug assertions, one that st_add_direct_with_hash does not receive the reserved hash value, and a second in rebuild_table_with, which ensures that after we rebuild/compact a table it contains the expected number of elements. Co-authored-by: Alan Wu <alanwu@ruby-lang.org>
1 parent 5d79c52 commit 085e99a

File tree

3 files changed

+31
-6
lines changed

3 files changed

+31
-6
lines changed
 

‎common.mk

+1
Original file line numberDiff line numberDiff line change
@@ -17680,6 +17680,7 @@ st.$(OBJEXT): {$(VPATH)}internal/variable.h
1768017680
st.$(OBJEXT): {$(VPATH)}internal/warning_push.h
1768117681
st.$(OBJEXT): {$(VPATH)}internal/xmalloc.h
1768217682
st.$(OBJEXT): {$(VPATH)}missing.h
17683+
st.$(OBJEXT): {$(VPATH)}ruby_assert.h
1768317684
st.$(OBJEXT): {$(VPATH)}st.c
1768417685
st.$(OBJEXT): {$(VPATH)}st.h
1768517686
st.$(OBJEXT): {$(VPATH)}subst.h

‎st.c

+16-6
Original file line numberDiff line numberDiff line change
@@ -103,20 +103,21 @@
103103
#ifdef NOT_RUBY
104104
#include "regint.h"
105105
#include "st.h"
106+
#include <assert.h>
106107
#elif defined RUBY_EXPORT
107108
#include "internal.h"
108109
#include "internal/bits.h"
109110
#include "internal/hash.h"
110111
#include "internal/sanitizers.h"
111112
#include "internal/st.h"
113+
#include "ruby_assert.h"
112114
#endif
113115

114116
#include <stdio.h>
115117
#ifdef HAVE_STDLIB_H
116118
#include <stdlib.h>
117119
#endif
118120
#include <string.h>
119-
#include <assert.h>
120121

121122
#ifdef __GNUC__
122123
#define PREFETCH(addr, write_p) __builtin_prefetch(addr, write_p)
@@ -314,17 +315,22 @@ static const struct st_features features[] = {
314315
#define RESERVED_HASH_VAL (~(st_hash_t) 0)
315316
#define RESERVED_HASH_SUBSTITUTION_VAL ((st_hash_t) 0)
316317

317-
/* Return hash value of KEY for table TAB. */
318318
static inline st_hash_t
319-
do_hash(st_data_t key, st_table *tab)
319+
normalize_hash_value(st_hash_t hash)
320320
{
321-
st_hash_t hash = (st_hash_t)(tab->type->hash)(key);
322-
323321
/* RESERVED_HASH_VAL is used for a deleted entry. Map it into
324322
another value. Such mapping should be extremely rare. */
325323
return hash == RESERVED_HASH_VAL ? RESERVED_HASH_SUBSTITUTION_VAL : hash;
326324
}
327325

326+
/* Return hash value of KEY for table TAB. */
327+
static inline st_hash_t
328+
do_hash(st_data_t key, st_table *tab)
329+
{
330+
st_hash_t hash = (st_hash_t)(tab->type->hash)(key);
331+
return normalize_hash_value(hash);
332+
}
333+
328334
/* Power of 2 defining the minimal number of allocated entries. */
329335
#define MINIMAL_POWER2 2
330336

@@ -784,6 +790,8 @@ rebuild_table_with(st_table *const new_tab, st_table *const tab)
784790
new_tab->num_entries++;
785791
ni++;
786792
}
793+
794+
assert(new_tab->num_entries == tab->num_entries);
787795
}
788796

789797
static void
@@ -1173,6 +1181,8 @@ st_add_direct_with_hash(st_table *tab,
11731181
st_index_t ind;
11741182
st_index_t bin_ind;
11751183

1184+
assert(hash != RESERVED_HASH_VAL);
1185+
11761186
rebuild_table_if_necessary(tab);
11771187
ind = tab->entries_bound++;
11781188
entry = &tab->entries[ind];
@@ -1190,7 +1200,7 @@ void
11901200
rb_st_add_direct_with_hash(st_table *tab,
11911201
st_data_t key, st_data_t value, st_hash_t hash)
11921202
{
1193-
st_add_direct_with_hash(tab, key, value, hash);
1203+
st_add_direct_with_hash(tab, key, value, normalize_hash_value(hash));
11941204
}
11951205

11961206
/* Insert (KEY, VALUE) into table TAB. The table should not have

‎test/ruby/test_hash.rb

+14
Original file line numberDiff line numberDiff line change
@@ -2389,4 +2389,18 @@ def hash
23892389
end
23902390
end;
23912391
end
2392+
2393+
def test_ar_to_st_reserved_value
2394+
klass = Class.new do
2395+
attr_reader :hash
2396+
def initialize(val) = @hash = val
2397+
end
2398+
2399+
values = 0.downto(-16).to_a
2400+
hash = {}
2401+
values.each do |val|
2402+
hash[klass.new(val)] = val
2403+
end
2404+
assert_equal values, hash.values, "[ruby-core:121239] [Bug #21170]"
2405+
end
23922406
end

0 commit comments

Comments
 (0)
Failed to load comments.