Skip to content

Commit 70196ee

Browse files
Add safe iterator tracking in hashtable to prevents invalid access on hashtable delete (#2807)
This makes it safe to delete hashtable while a safe iterator is iterating it. This currently isn't possible, but this improvement is required for fork-less replication #1754 which is being actively worked on. We discussed these issues in #2611 which guards against a different but related issue: calling hashtableNext again after it has already returned false. I implemented a singly linked list that hashtable uses to track its current safe iterators. It is used to invalidate all associated safe iterators when the hashtable is released. A singly linked list is acceptable because the list length is always very small - typically zero and no more than a handful. Also, renames the internal functions: hashtableReinitIterator -> hashtableRetargetIterator hashtableResetIterator -> hashtableCleanupIterator --------- Signed-off-by: Rain Valentine <[email protected]> Signed-off-by: Viktor Söderqvist <[email protected]> Co-authored-by: Viktor Söderqvist <[email protected]>
1 parent 8ec9381 commit 70196ee

23 files changed

+447
-76
lines changed

src/acl.c

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,7 @@ static void ACLChangeSelectorPerm(aclSelector *selector, struct serverCommand *c
651651
struct serverCommand *sub = next;
652652
ACLSetSelectorCommandBit(selector, sub->id, allow);
653653
}
654-
hashtableResetIterator(&iter);
654+
hashtableCleanupIterator(&iter);
655655
}
656656
}
657657

@@ -674,7 +674,7 @@ static void ACLSetSelectorCommandBitsForCategory(hashtable *commands, aclSelecto
674674
ACLSetSelectorCommandBitsForCategory(cmd->subcommands_ht, selector, cflag, value);
675675
}
676676
}
677-
hashtableResetIterator(&iter);
677+
hashtableCleanupIterator(&iter);
678678
}
679679

680680
/* This function is responsible for recomputing the command bits for all selectors of the existing users.
@@ -1964,7 +1964,7 @@ static int ACLShouldKillPubsubClient(client *c, list *upcoming) {
19641964
int res = ACLCheckChannelAgainstList(upcoming, o->ptr, sdslen(o->ptr), 1);
19651965
kill = (res == ACL_DENIED_CHANNEL);
19661966
}
1967-
hashtableResetIterator(&iter);
1967+
hashtableCleanupIterator(&iter);
19681968

19691969
/* Check for channel violations. */
19701970
if (!kill) {
@@ -1977,7 +1977,7 @@ static int ACLShouldKillPubsubClient(client *c, list *upcoming) {
19771977
int res = ACLCheckChannelAgainstList(upcoming, o->ptr, sdslen(o->ptr), 0);
19781978
kill = (res == ACL_DENIED_CHANNEL);
19791979
}
1980-
hashtableResetIterator(&iter);
1980+
hashtableCleanupIterator(&iter);
19811981
}
19821982
if (!kill) {
19831983
/* Check for shard channels violation. */
@@ -1989,7 +1989,7 @@ static int ACLShouldKillPubsubClient(client *c, list *upcoming) {
19891989
int res = ACLCheckChannelAgainstList(upcoming, o->ptr, sdslen(o->ptr), 0);
19901990
kill = (res == ACL_DENIED_CHANNEL);
19911991
}
1992-
hashtableResetIterator(&iter);
1992+
hashtableCleanupIterator(&iter);
19931993
}
19941994

19951995
if (kill) {
@@ -2792,7 +2792,7 @@ static void aclCatWithFlags(client *c, hashtable *commands, uint64_t cflag, int
27922792
aclCatWithFlags(c, cmd->subcommands_ht, cflag, arraylen);
27932793
}
27942794
}
2795-
hashtableResetIterator(&iter);
2795+
hashtableCleanupIterator(&iter);
27962796
}
27972797

27982798
/* Add the formatted response from a single selector to the ACL GETUSER

src/aof.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1918,19 +1918,19 @@ int rewriteSortedSetObject(rio *r, robj *key, robj *o) {
19181918

19191919
if (!rioWriteBulkCount(r, '*', 2 + cmd_items * 2) || !rioWriteBulkString(r, "ZADD", 4) ||
19201920
!rioWriteBulkObject(r, key)) {
1921-
hashtableResetIterator(&iter);
1921+
hashtableCleanupIterator(&iter);
19221922
return 0;
19231923
}
19241924
}
19251925
sds ele = zslGetNodeElement(node);
19261926
if (!rioWriteBulkDouble(r, node->score) || !rioWriteBulkString(r, ele, sdslen(ele))) {
1927-
hashtableResetIterator(&iter);
1927+
hashtableCleanupIterator(&iter);
19281928
return 0;
19291929
}
19301930
if (++count == AOF_REWRITE_ITEMS_PER_CMD) count = 0;
19311931
items--;
19321932
}
1933-
hashtableResetIterator(&iter);
1933+
hashtableCleanupIterator(&iter);
19341934
} else {
19351935
serverPanic("Unknown sorted zset encoding");
19361936
}

src/debug.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ void xorObjectDigest(serverDb *db, robj *keyobj, unsigned char *digest, robj *o)
220220
mixDigest(eledigest, buf, strlen(buf));
221221
xorDigest(digest, eledigest, 20);
222222
}
223-
hashtableResetIterator(&iter);
223+
hashtableCleanupIterator(&iter);
224224
} else {
225225
serverPanic("Unknown sorted set encoding");
226226
}

src/defrag.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,7 @@ static void defragPubsubScanCallback(void *privdata, void *elemref) {
771771
bool replaced = hashtableReplaceReallocatedEntry(client_channels, channel, newchannel);
772772
serverAssert(replaced);
773773
}
774-
hashtableResetIterator(&iter);
774+
hashtableCleanupIterator(&iter);
775775
}
776776

777777
/* Try to defrag the dictionary of clients that is stored as the value part. */

src/hashtable.c

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,9 @@ typedef struct hashtableBucket {
284284
/* A key property is that the bucket size is one cache line. */
285285
static_assert(sizeof(bucket) == HASHTABLE_BUCKET_SIZE, "Bucket size mismatch");
286286

287+
/* Forward declaration for iter type */
288+
typedef struct iter iter;
289+
287290
struct hashtable {
288291
hashtableType *type;
289292
ssize_t rehash_idx; /* -1 = rehashing not in progress. */
@@ -293,10 +296,11 @@ struct hashtable {
293296
int16_t pause_rehash; /* Non-zero = rehashing is paused */
294297
int16_t pause_auto_shrink; /* Non-zero = automatic resizing disallowed. */
295298
size_t child_buckets[2]; /* Number of allocated child buckets. */
299+
iter *safe_iterators; /* Head of linked list of safe iterators */
296300
void *metadata[];
297301
};
298302

299-
typedef struct {
303+
struct iter {
300304
hashtable *hashtable;
301305
bucket *bucket;
302306
long index;
@@ -309,7 +313,8 @@ typedef struct {
309313
/* Safe iterator temporary storage for bucket chain compaction. */
310314
uint64_t last_seen_size;
311315
};
312-
} iter;
316+
iter *next_safe_iter; /* Next safe iterator in hashtable's list */
317+
};
313318

314319
/* The opaque hashtableIterator is defined as a blob of bytes. */
315320
static_assert(sizeof(hashtableIterator) >= sizeof(iter),
@@ -1084,6 +1089,37 @@ static inline int shouldPrefetchValues(iter *iter) {
10841089
return (iter->flags & HASHTABLE_ITER_PREFETCH_VALUES);
10851090
}
10861091

1092+
/* Add a safe iterator to the hashtable's tracking list */
1093+
static void trackSafeIterator(iter *it) {
1094+
assert(it->next_safe_iter == NULL);
1095+
hashtable *ht = it->hashtable;
1096+
it->next_safe_iter = ht->safe_iterators;
1097+
ht->safe_iterators = it;
1098+
}
1099+
1100+
/* Remove a safe iterator from the hashtable's tracking list */
1101+
static void untrackSafeIterator(iter *it) {
1102+
hashtable *ht = it->hashtable;
1103+
if (ht->safe_iterators == it) {
1104+
ht->safe_iterators = it->next_safe_iter;
1105+
} else {
1106+
iter *current = ht->safe_iterators;
1107+
assert(current != NULL);
1108+
while (current->next_safe_iter != it) {
1109+
current = current->next_safe_iter;
1110+
assert(current != NULL);
1111+
}
1112+
current->next_safe_iter = it->next_safe_iter;
1113+
}
1114+
it->next_safe_iter = NULL;
1115+
it->hashtable = NULL; /* Mark as invalid */
1116+
}
1117+
1118+
/* Invalidate all safe iterators by setting hashtable = NULL */
1119+
static void invalidateAllSafeIterators(hashtable *ht) {
1120+
while (ht->safe_iterators) untrackSafeIterator(ht->safe_iterators);
1121+
}
1122+
10871123
/* --- API functions --- */
10881124

10891125
/* Allocates and initializes a new hashtable specified by the given type. */
@@ -1098,6 +1134,7 @@ hashtable *hashtableCreate(hashtableType *type) {
10981134
ht->rehash_idx = -1;
10991135
ht->pause_rehash = 0;
11001136
ht->pause_auto_shrink = 0;
1137+
ht->safe_iterators = NULL;
11011138
resetTable(ht, 0);
11021139
resetTable(ht, 1);
11031140
if (type->trackMemUsage) type->trackMemUsage(ht, alloc_size);
@@ -1153,6 +1190,7 @@ void hashtableEmpty(hashtable *ht, void(callback)(hashtable *)) {
11531190

11541191
/* Deletes all the entries and frees the table. */
11551192
void hashtableRelease(hashtable *ht) {
1193+
invalidateAllSafeIterators(ht);
11561194
hashtableEmpty(ht, NULL);
11571195
/* Call trackMemUsage before zfree, so trackMemUsage can access ht. */
11581196
if (ht->type->trackMemUsage) {
@@ -1242,6 +1280,7 @@ static void hashtablePauseRehashing(hashtable *ht) {
12421280
/* Resumes incremental rehashing, after pausing it. */
12431281
static void hashtableResumeRehashing(hashtable *ht) {
12441282
ht->pause_rehash--;
1283+
assert(ht->pause_rehash >= 0);
12451284
hashtableResumeAutoShrink(ht);
12461285
}
12471286

@@ -1962,7 +2001,9 @@ size_t hashtableScanDefrag(hashtable *ht, size_t cursor, hashtableScanFunction f
19622001
* rehashing to prevent entries from moving around. It's allowed to insert and
19632002
* replace entries. Deleting entries is only allowed for the entry that was just
19642003
* returned by hashtableNext. Deleting other entries is possible, but doing so
1965-
* can cause internal fragmentation, so don't.
2004+
* can cause internal fragmentation, so don't. The hash table itself can be
2005+
* safely deleted while safe iterators exist - they will be invalidated and
2006+
* subsequent calls to hashtableNext will return false.
19662007
*
19672008
* Guarantees for safe iterators:
19682009
*
@@ -1978,7 +2019,7 @@ size_t hashtableScanDefrag(hashtable *ht, size_t cursor, hashtableScanFunction f
19782019
* - Entries that are inserted during the iteration may or may not be returned
19792020
* by the iterator.
19802021
*
1981-
* Call hashtableNext to fetch each entry. You must call hashtableResetIterator
2022+
* Call hashtableNext to fetch each entry. You must call hashtableCleanupIterator
19822023
* when you are done with the iterator.
19832024
*/
19842025
void hashtableInitIterator(hashtableIterator *iterator, hashtable *ht, uint8_t flags) {
@@ -1988,22 +2029,31 @@ void hashtableInitIterator(hashtableIterator *iterator, hashtable *ht, uint8_t f
19882029
iter->table = 0;
19892030
iter->index = -1;
19902031
iter->flags = flags;
2032+
iter->next_safe_iter = NULL;
2033+
if (isSafe(iter) && ht != NULL) {
2034+
trackSafeIterator(iter);
2035+
}
19912036
}
19922037

1993-
/* Reinitializes the iterator for the provided hashtable while
1994-
* preserving the flags from its previous initialization. */
1995-
void hashtableReinitIterator(hashtableIterator *iterator, hashtable *ht) {
2038+
/* Reinitializes the iterator to begin a new iteration of the provided hashtable
2039+
* while preserving the flags from its previous initialization. */
2040+
void hashtableRetargetIterator(hashtableIterator *iterator, hashtable *ht) {
19962041
iter *iter = iteratorFromOpaque(iterator);
1997-
hashtableInitIterator(iterator, ht, iter->flags);
2042+
uint8_t flags = iter->flags;
2043+
2044+
hashtableCleanupIterator(iterator);
2045+
hashtableInitIterator(iterator, ht, flags);
19982046
}
19992047

2000-
/* Resets a stack-allocated iterator. */
2001-
void hashtableResetIterator(hashtableIterator *iterator) {
2048+
/* Performs required cleanup for a stack-allocated iterator. */
2049+
void hashtableCleanupIterator(hashtableIterator *iterator) {
20022050
iter *iter = iteratorFromOpaque(iterator);
2051+
if (iter->hashtable == NULL) return;
2052+
20032053
if (!(iter->index == -1 && iter->table == 0)) {
20042054
if (isSafe(iter)) {
20052055
hashtableResumeRehashing(iter->hashtable);
2006-
assert(iter->hashtable->pause_rehash >= 0);
2056+
untrackSafeIterator(iter);
20072057
} else {
20082058
assert(iter->fingerprint == hashtableFingerprint(iter->hashtable));
20092059
}
@@ -2021,7 +2071,7 @@ hashtableIterator *hashtableCreateIterator(hashtable *ht, uint8_t flags) {
20212071
/* Resets and frees the memory of an allocated iterator, i.e. one created using
20222072
* hashtableCreate(Safe)Iterator. */
20232073
void hashtableReleaseIterator(hashtableIterator *iterator) {
2024-
hashtableResetIterator(iterator);
2074+
hashtableCleanupIterator(iterator);
20252075
iter *iter = iteratorFromOpaque(iterator);
20262076
zfree(iter);
20272077
}
@@ -2030,6 +2080,9 @@ void hashtableReleaseIterator(hashtableIterator *iterator) {
20302080
* Returns false if there are no more entries. */
20312081
bool hashtableNext(hashtableIterator *iterator, void **elemptr) {
20322082
iter *iter = iteratorFromOpaque(iterator);
2083+
/* Check if iterator has been invalidated */
2084+
if (iter->hashtable == NULL) return false;
2085+
20332086
while (1) {
20342087
if (iter->index == -1 && iter->table == 0) {
20352088
/* It's the first call to next. */

src/hashtable.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ typedef struct hashtable hashtable;
3939
typedef struct hashtableStats hashtableStats;
4040

4141
/* Can types that can be stack allocated. */
42-
typedef uint64_t hashtableIterator[5];
42+
typedef uint64_t hashtableIterator[6];
4343
typedef uint64_t hashtablePosition[2];
4444
typedef uint64_t hashtableIncrementalFindState[5];
4545

@@ -160,8 +160,8 @@ bool hashtableIncrementalFindGetResult(hashtableIncrementalFindState *state, voi
160160
size_t hashtableScan(hashtable *ht, size_t cursor, hashtableScanFunction fn, void *privdata);
161161
size_t hashtableScanDefrag(hashtable *ht, size_t cursor, hashtableScanFunction fn, void *privdata, void *(*defragfn)(void *), int flags);
162162
void hashtableInitIterator(hashtableIterator *iter, hashtable *ht, uint8_t flags);
163-
void hashtableReinitIterator(hashtableIterator *iterator, hashtable *ht);
164-
void hashtableResetIterator(hashtableIterator *iter);
163+
void hashtableRetargetIterator(hashtableIterator *iterator, hashtable *ht);
164+
void hashtableCleanupIterator(hashtableIterator *iter);
165165
hashtableIterator *hashtableCreateIterator(hashtable *ht, uint8_t flags);
166166
void hashtableReleaseIterator(hashtableIterator *iter);
167167
bool hashtableNext(hashtableIterator *iter, void **elemptr);

src/kvstore.c

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,7 @@ kvstoreIterator *kvstoreIteratorInit(kvstore *kvs, uint8_t flags) {
628628
/* Free the kvs_it returned by kvstoreIteratorInit. */
629629
void kvstoreIteratorRelease(kvstoreIterator *kvs_it) {
630630
hashtableIterator *iter = &kvs_it->di;
631-
hashtableResetIterator(iter);
631+
hashtableCleanupIterator(iter);
632632
/* In the safe iterator context, we may delete entries. */
633633
if (kvs_it->didx != KVSTORE_INDEX_NOT_FOUND) {
634634
freeHashtableIfNeeded(kvs_it->kvs, kvs_it->didx);
@@ -672,7 +672,7 @@ static hashtable *kvstoreIteratorNextHashtable(kvstoreIterator *kvs_it) {
672672
if (kvs_it->didx != KVSTORE_INDEX_NOT_FOUND && kvstoreGetHashtable(kvs_it->kvs, kvs_it->didx)) {
673673
/* Before we move to the next hashtable, reset the iter of the previous hashtable. */
674674
hashtableIterator *iter = &kvs_it->di;
675-
hashtableResetIterator(iter);
675+
hashtableCleanupIterator(iter);
676676
/* In the safe iterator context, we may delete entries. */
677677
freeHashtableIfNeeded(kvs_it->kvs, kvs_it->didx);
678678
}
@@ -697,7 +697,7 @@ bool kvstoreIteratorNext(kvstoreIterator *kvs_it, void **next) {
697697
/* No current hashtable or reached the end of the hash table. */
698698
hashtable *ht = kvstoreIteratorNextHashtable(kvs_it);
699699
if (!ht) return false;
700-
hashtableReinitIterator(&kvs_it->di, ht);
700+
hashtableRetargetIterator(&kvs_it->di, ht);
701701
return hashtableNext(&kvs_it->di, next);
702702
}
703703
}
@@ -779,7 +779,7 @@ kvstoreHashtableIterator *kvstoreGetHashtableIterator(kvstore *kvs, int didx, ui
779779
void kvstoreReleaseHashtableIterator(kvstoreHashtableIterator *kvs_di) {
780780
/* The hashtable may be deleted during the iteration process, so here need to check for NULL. */
781781
if (kvstoreGetHashtable(kvs_di->kvs, kvs_di->didx)) {
782-
hashtableResetIterator(&kvs_di->di);
782+
hashtableCleanupIterator(&kvs_di->di);
783783
/* In the safe iterator context, we may delete entries. */
784784
freeHashtableIfNeeded(kvs_di->kvs, kvs_di->didx);
785785
}

src/latency.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ void latencyAllCommandsFillCDF(client *c, hashtable *commands, int *command_with
547547
latencyAllCommandsFillCDF(c, cmd->subcommands_ht, command_with_data);
548548
}
549549
}
550-
hashtableResetIterator(&iter);
550+
hashtableCleanupIterator(&iter);
551551
}
552552

553553
/* latencyCommand() helper to produce for a specific command set,
@@ -580,7 +580,7 @@ void latencySpecificCommandsFillCDF(client *c) {
580580
command_with_data++;
581581
}
582582
}
583-
hashtableResetIterator(&iter);
583+
hashtableCleanupIterator(&iter);
584584
}
585585
}
586586
setDeferredMapLen(c, replylen, command_with_data);

src/module.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12547,7 +12547,7 @@ int moduleFreeCommand(struct ValkeyModule *module, struct serverCommand *cmd) {
1254712547
sdsfree(sub->fullname);
1254812548
zfree(sub);
1254912549
}
12550-
hashtableResetIterator(&iter);
12550+
hashtableCleanupIterator(&iter);
1255112551
hashtableRelease(cmd->subcommands_ht);
1255212552
}
1255312553

@@ -12571,7 +12571,7 @@ void moduleUnregisterCommands(struct ValkeyModule *module) {
1257112571
sdsfree(cmd->fullname);
1257212572
zfree(cmd);
1257312573
}
12574-
hashtableResetIterator(&iter);
12574+
hashtableCleanupIterator(&iter);
1257512575
}
1257612576

1257712577
/* We parse argv to add sds "NAME VALUE" pairs to the server.module_configs_queue list of configs.

src/object.c

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,7 @@ void dismissSetObject(robj *o, size_t size_hint) {
636636
sds item = next;
637637
dismissSds(item);
638638
}
639-
hashtableResetIterator(&iter);
639+
hashtableCleanupIterator(&iter);
640640
}
641641

642642
dismissHashtable(ht);
@@ -688,7 +688,7 @@ void dismissHashObject(robj *o, size_t size_hint) {
688688
while (hashtableNext(&iter, &next)) {
689689
entryDismissMemory(next);
690690
}
691-
hashtableResetIterator(&iter);
691+
hashtableCleanupIterator(&iter);
692692
}
693693

694694
dismissHashtable(ht);
@@ -1165,7 +1165,7 @@ size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) {
11651165
elesize += sdsAllocSize(element);
11661166
samples++;
11671167
}
1168-
hashtableResetIterator(&iter);
1168+
hashtableCleanupIterator(&iter);
11691169
if (samples) asize += (double)elesize / samples * hashtableSize(ht);
11701170
} else if (o->encoding == OBJ_ENCODING_INTSET) {
11711171
asize += zmalloc_size(o->ptr);
@@ -1207,7 +1207,7 @@ size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) {
12071207
elesize += entryMemUsage(next);
12081208
samples++;
12091209
}
1210-
hashtableResetIterator(&iter);
1210+
hashtableCleanupIterator(&iter);
12111211
if (samples) asize += (double)elesize / samples * hashtableSize(ht);
12121212
if (vsetIsValid(volatile_fields)) asize += vsetMemUsage(volatile_fields);
12131213
} else {

0 commit comments

Comments
 (0)