diff options
author | John Högberg <john@erlang.org> | 2022-04-22 16:15:21 +0200 |
---|---|---|
committer | John Högberg <john@erlang.org> | 2022-04-22 16:15:21 +0200 |
commit | dfad4ba32184e4e424e0fa8f4e1333199cbd26d1 (patch) | |
tree | 84b4402a50629b3f7b786a22ffea2852f12afbf6 | |
parent | f810d450d3b8eb164ac19158efd6b1e32c9b3110 (diff) | |
parent | 33f51ad23344c077860f5d28252724fd9bbd73d0 (diff) | |
download | erlang-dfad4ba32184e4e424e0fa8f4e1333199cbd26d1.tar.gz |
Merge branch 'john/erts/fix-persistent-term-get-put-race/GH-5908/OTP-18065' into john/erts/fix-persistent-term-get-put-race/maint-23
* john/erts/fix-persistent-term-get-put-race/GH-5908/OTP-18065:
erts: Fix a race in persistent_term:get/1,2
-rw-r--r-- | erts/emulator/beam/erl_bif_persistent.c | 130 | ||||
-rw-r--r-- | erts/emulator/test/persistent_term_SUITE.erl | 40 |
2 files changed, 112 insertions, 58 deletions
diff --git a/erts/emulator/beam/erl_bif_persistent.c b/erts/emulator/beam/erl_bif_persistent.c index 596604cdec..263f7a5e9d 100644 --- a/erts/emulator/beam/erl_bif_persistent.c +++ b/erts/emulator/beam/erl_bif_persistent.c @@ -136,7 +136,7 @@ typedef struct { HashTable* old_table; HashTable* new_table; Uint entry_index; - Eterm old_term; + Eterm old_bucket; HashTable* tmp_table; int must_shrink; ErtsPersistentTermCpyTableCtx cpy_ctx; @@ -147,7 +147,7 @@ typedef struct { */ static HashTable* create_initial_table(void); -static Uint lookup(HashTable* hash_table, Eterm key); +static Uint lookup(HashTable* hash_table, Eterm key, Eterm *bucket); static int is_erasable(HashTable* hash_table, Uint idx); static HashTable* copy_table(ErtsPersistentTermCpyTableCtx* ctx); static int try_seize_update_permission(Process* c_p); @@ -307,6 +307,7 @@ BIF_RETTYPE persistent_term_put_2(BIF_ALIST_2) static const Uint ITERATIONS_PER_RED = 32; ErtsPersistentTermPut2Context* ctx; Eterm state_mref = THE_NON_VALUE; + Eterm old_bucket; long iterations_until_trap; long max_iterations; #define PUT_TRAP_CODE \ @@ -359,14 +360,14 @@ BIF_RETTYPE persistent_term_put_2(BIF_ALIST_2) ctx->key = BIF_ARG_1; ctx->term = BIF_ARG_2; - ctx->entry_index = lookup(ctx->hash_table, ctx->key); + ctx->entry_index = lookup(ctx->hash_table, ctx->key, &old_bucket); ctx->heap[0] = make_arityval(2); ctx->heap[1] = ctx->key; ctx->heap[2] = ctx->term; ctx->tuple = make_tuple(ctx->heap); - if (is_nil(get_bucket(ctx->hash_table, ctx->entry_index))) { + if (is_nil(old_bucket)) { if (MUST_GROW(ctx->hash_table)) { Uint new_size = ctx->hash_table->allocated * 2; TRAPPING_COPY_TABLE_PUT(ctx->hash_table, @@ -374,15 +375,17 @@ BIF_RETTYPE persistent_term_put_2(BIF_ALIST_2) new_size, ERTS_PERSISTENT_TERM_CPY_NO_REHASH, PUT2_TRAP_LOCATION_NEW_KEY); - ctx->entry_index = lookup(ctx->hash_table, ctx->key); + ctx->entry_index = lookup(ctx->hash_table, + ctx->key, + &old_bucket); } ctx->hash_table->num_entries++; } else { - Eterm tuple = get_bucket(ctx->hash_table, ctx->entry_index); Eterm old_term; - ASSERT(is_tuple_arity(tuple, 2)); - old_term = boxed_val(tuple)[2]; + ASSERT(is_tuple_arity(old_bucket, 2)); + old_term = boxed_val(old_bucket)[2]; + if (EQ(ctx->term, old_term)) { /* Same value. No need to update anything. */ release_update_permission(0); @@ -475,15 +478,15 @@ BIF_RETTYPE persistent_term_get_1(BIF_ALIST_1) { Eterm key = BIF_ARG_1; HashTable* hash_table = (HashTable *) erts_atomic_read_nob(&the_hash_table); - Uint entry_index; - Eterm term; + Eterm bucket; - entry_index = lookup(hash_table, key); - term = get_bucket(hash_table, entry_index); - if (is_boxed(term)) { - ASSERT(is_tuple_arity(term, 2)); - BIF_RET(tuple_val(term)[2]); + (void)lookup(hash_table, key, &bucket); + + if (is_boxed(bucket)) { + ASSERT(is_tuple_arity(bucket, 2)); + BIF_RET(tuple_val(bucket)[2]); } + BIF_ERROR(BIF_P, BADARG); } @@ -492,15 +495,15 @@ BIF_RETTYPE persistent_term_get_2(BIF_ALIST_2) Eterm key = BIF_ARG_1; Eterm result = BIF_ARG_2; HashTable* hash_table = (HashTable *) erts_atomic_read_nob(&the_hash_table); - Uint entry_index; - Eterm term; + Eterm bucket; + + (void)lookup(hash_table, key, &bucket); - entry_index = lookup(hash_table, key); - term = get_bucket(hash_table, entry_index); - if (is_boxed(term)) { - ASSERT(is_tuple_arity(term, 2)); - result = tuple_val(term)[2]; + if (is_boxed(bucket)) { + ASSERT(is_tuple_arity(bucket, 2)); + result = tuple_val(bucket)[2]; } + BIF_RET(result); } @@ -578,9 +581,9 @@ BIF_RETTYPE persistent_term_erase_1(BIF_ALIST_1) ctx->key = BIF_ARG_1; ctx->old_table = (HashTable *) erts_atomic_read_nob(&the_hash_table); - ctx->entry_index = lookup(ctx->old_table, ctx->key); - ctx->old_term = get_bucket(ctx->old_table, ctx->entry_index); - if (is_boxed(ctx->old_term)) { + ctx->entry_index = lookup(ctx->old_table, ctx->key, &ctx->old_bucket); + + if (is_boxed(ctx->old_bucket)) { ctx->must_shrink = MUST_SHRINK(ctx->old_table); if (!ctx->must_shrink && is_erasable(ctx->old_table, ctx->entry_index)) { /* @@ -607,7 +610,7 @@ BIF_RETTYPE persistent_term_erase_1(BIF_ALIST_1) * temporary table copy of the same size as the old one. */ - ASSERT(is_tuple_arity(ctx->old_term, 2)); + ASSERT(is_tuple_arity(ctx->old_bucket, 2)); TRAPPING_COPY_TABLE_ERASE(ctx->tmp_table, ctx->old_table, ctx->old_table->allocated, @@ -650,7 +653,7 @@ BIF_RETTYPE persistent_term_erase_1(BIF_ALIST_1) * Key is not present. Nothing to do. */ - ASSERT(is_nil(ctx->old_term)); + ASSERT(is_nil(ctx->old_bucket)); release_update_permission(0); BIF_RET(am_false); } @@ -723,10 +726,10 @@ erts_init_persistent_dumping(void) erts_persistent_areas = (ErtsLiteralArea **) hash_table->term; area_p = erts_persistent_areas; for (i = 0; i < hash_table->allocated; i++) { - Eterm term = get_bucket(hash_table, i); + Eterm bucket = get_bucket(hash_table, i); - if (is_boxed(term)) { - *area_p++ = term_to_area(term); + if (is_boxed(bucket)) { + *area_p++ = term_to_area(bucket); } } erts_num_persistent_areas = area_p - erts_persistent_areas; @@ -812,15 +815,15 @@ do_get_all(Process* c_p, TrapData* trap_data, Eterm res) i = 0; heap_size = (2 + 3) * remaining; while (remaining != 0) { - Eterm term; + Eterm bucket; ASSERT(idx < hash_table->allocated); - term = get_bucket(hash_table, idx); - if (is_tuple(term)) { + bucket = get_bucket(hash_table, idx); + if (is_tuple(bucket)) { Uint key_size; Eterm* tup_val; - ASSERT(is_tuple_arity(term, 2)); - tup_val = tuple_val(term); + ASSERT(is_tuple_arity(bucket, 2)); + tup_val = tuple_val(bucket); key_size = size_object(tup_val[1]); copy_data[i].key_size = key_size; copy_data[i].tuple_ptr = tup_val; @@ -897,13 +900,17 @@ do_info(Process* c_p, TrapData* trap_data) remaining = trap_data->remaining < max_iter ? trap_data->remaining : max_iter; trap_data->remaining -= remaining; while (remaining != 0) { - if (is_boxed(get_bucket(hash_table, idx))) { - ErtsLiteralArea* area; - area = term_to_area(get_bucket(hash_table, idx)); + Eterm bucket = get_bucket(hash_table, idx); + + if (is_boxed(bucket)) { + ErtsLiteralArea* area = term_to_area(bucket); + trap_data->memory += sizeof(ErtsLiteralArea) + sizeof(Eterm) * (area->end - area->start - 1); + remaining--; } + idx++; } trap_data->idx = idx; @@ -953,7 +960,7 @@ cleanup_trap_data(Binary *bp) } static Uint -lookup(HashTable* hash_table, Eterm key) +lookup(HashTable* hash_table, Eterm key, Eterm *bucket) { Uint mask = hash_table->mask; Uint32 idx = make_internal_hash(key, 0); @@ -961,11 +968,15 @@ lookup(HashTable* hash_table, Eterm key) while (1) { term = get_bucket(hash_table, idx & mask); - if (is_nil(term) || EQ(key, (tuple_val(term))[1])) - break; + + if (is_nil(term) || EQ(key, (tuple_val(term))[1])) { + *bucket = term; + + return idx & mask; + } + idx++; } - return idx & mask; } static int @@ -1048,12 +1059,19 @@ copy_table(ErtsPersistentTermCpyTableCtx* ctx) i < MIN(ctx->iterations_done + ctx->max_iterations, old_size); i++) { - Eterm term = get_bucket(ctx->old_table, i); - if (is_tuple(term)) { - Eterm key = tuple_val(term)[1]; - Uint entry_index = lookup(ctx->new_table, key); - ASSERT(is_nil(get_bucket(ctx->new_table, entry_index))); - set_bucket(ctx->new_table, entry_index, term); + Eterm old_bucket = get_bucket(ctx->old_table, i); + + if (is_tuple(old_bucket)) { + Eterm key, assert_empty_bucket; + Uint entry_index; + + key = tuple_val(old_bucket)[1]; + entry_index = lookup(ctx->new_table, key, &assert_empty_bucket); + + ASSERT(is_nil(assert_empty_bucket)); + (void)assert_empty_bucket; + + set_bucket(ctx->new_table, entry_index, old_bucket); } } ctx->total_iterations_done += (i - ctx->iterations_done); @@ -1118,7 +1136,7 @@ table_updater(void* data) old_table = (HashTable *) erts_atomic_read_nob(&the_hash_table); new_table = (HashTable *) data; if (new_table == old_table) { - Eterm old_term = get_bucket(old_table, fast_update_index); + Eterm old_bucket = get_bucket(old_table, fast_update_index); ASSERT(is_value(fast_update_term)); ASSERT(fast_update_index < old_table->allocated); set_bucket(old_table, fast_update_index, fast_update_term); @@ -1126,11 +1144,11 @@ table_updater(void* data) fast_update_term = THE_NON_VALUE; #endif - if (is_not_nil(old_term)) { + if (is_not_nil(old_bucket)) { OldLiteral *olp = alloc_old_literal(); - ASSERT(is_tuple_arity(old_term,2)); - olp->term = old_term; - olp->area = term_to_area(old_term); + ASSERT(is_tuple_arity(old_bucket,2)); + olp->term = old_bucket; + olp->area = term_to_area(old_bucket); olp->delete_op.type = DELETE_OP_TUPLE; olp->delete_op.is_scheduled = 1; append_to_delete_queue(&olp->delete_op); @@ -1411,9 +1429,9 @@ static void debug_table_foreach_off_heap(HashTable *tbl, int i; for (i = 0; i < tbl->allocated; i++) { - Eterm term = get_bucket(tbl, i); - if (is_tuple_arity(term, 2)) { - debug_area_off_heap(term_to_area(term), func, arg); + Eterm bucket = get_bucket(tbl, i); + if (is_tuple_arity(bucket, 2)) { + debug_area_off_heap(term_to_area(bucket), func, arg); } } } diff --git a/erts/emulator/test/persistent_term_SUITE.erl b/erts/emulator/test/persistent_term_SUITE.erl index 0fa8efecc8..cab71f2ae7 100644 --- a/erts/emulator/test/persistent_term_SUITE.erl +++ b/erts/emulator/test/persistent_term_SUITE.erl @@ -30,7 +30,8 @@ off_heap_values/1,keys/1,collisions/1, init_restart/1, put_erase_trapping/1, killed_while_trapping_put/1, - killed_while_trapping_erase/1,whole_message/1,shared_magic_ref/1]). + killed_while_trapping_erase/1,whole_message/1,shared_magic_ref/1, + get_put_colliding_bucket/1]). %% -export([test_init_restart_cmd/1]). @@ -45,7 +46,8 @@ all() -> get_all_race, killed_while_trapping,off_heap_values,keys,collisions, init_restart, put_erase_trapping, killed_while_trapping_put, - killed_while_trapping_erase,whole_message,shared_magic_ref]. + killed_while_trapping_erase,whole_message,shared_magic_ref, + get_put_colliding_bucket]. init_per_suite(Config) -> erts_debug:set_internal_state(available_internal_state, true), @@ -876,3 +878,37 @@ gar_setter(Key) -> persistent_term:erase(Key), persistent_term:put(Key, {complex, term}), gar_setter(Key). + +%% GH-5908: `persistent_term:get/1,2` could race with `persistent_term:put/2` +%% and return an unexpected value if `get/1,2` happened to _abort_ its lookup +%% in the same bucket that `put/2` was writing to. +get_put_colliding_bucket(_Config) -> + [[Key, CollidesWith | _] | _] = colliding_keys(), + false = persistent_term:erase(Key), + + {Pid, MRef} = + spawn_monitor(fun() -> + Schedulers = erlang:system_info(schedulers), + [spawn_link(fun() -> gpcb_reader(Key) end) + || _ <- lists:seq(1, Schedulers - 1)], + gpcb_updater(CollidesWith) + end), + + %% The race usually gets detected within a second or two, so we'll consider + %% the test passed if we can't reproduce it in 10 seconds. + receive + {'DOWN', MRef, process, Pid, Reason} -> + ct:fail(Reason) + after 10000 -> + exit(Pid, kill), + ok + end. + +gpcb_reader(Key) -> + expected = persistent_term:get(Key, expected), + gpcb_reader(Key). + +gpcb_updater(CollidesWith) -> + persistent_term:erase(CollidesWith), + persistent_term:put(CollidesWith, unexpected), + gpcb_updater(CollidesWith). |