-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Fix/ready key blocked client uaf 4198 #4212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: unstable
Are you sure you want to change the base?
Changes from 4 commits
44f9480
c5854f7
a33b5e1
c0a42ad
ad9a89b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -618,6 +618,30 @@ void signalDeletedKeyAsReady(serverDb *db, robj *key, int type) { | |
| signalKeyAsReadyLogic(db, key, type, 1); | ||
| } | ||
|
|
||
| /* Return 1 if client c is still present in the live blocked-clients list for | ||
| * rl->key. Safe to call with a possibly stale pointer: unlink happens before | ||
| * free, so a torn-down client cannot appear in the live list. */ | ||
| static int clientStillInBlockingKeysList(readyList *rl, client *c) { | ||
| dictEntry *de = dictFind(rl->db->blocking_keys, rl->key); | ||
| listNode *ln; | ||
| listIter li; | ||
|
|
||
| if (de == NULL) return 0; | ||
| listRewind(dictGetVal(de), &li); | ||
| while ((ln = listNext(&li)) != NULL) { | ||
| if (listNodeValue(ln) == c) return 1; | ||
| } | ||
| return 0; | ||
| } | ||
|
|
||
| /* Return 1 if a live client is still blocked waiting on rl->key. */ | ||
| static int clientStillBlockedOnReadyKey(client *c, readyList *rl) { | ||
| if (c == NULL || !c->flag.blocked) return 0; | ||
| if (c->bstate == NULL || c->bstate->keys == NULL) return 0; | ||
| if (c->db != rl->db) return 0; | ||
| return dictFind(c->bstate->keys, rl->key) != NULL; | ||
| } | ||
|
|
||
| /* Helper function for handleClientsBlockedOnKeys(). This function is called | ||
| * whenever a key is ready. we iterate over all the clients blocked on this key | ||
| * and try to re-execute the command (in case the key is still available). */ | ||
|
|
@@ -630,14 +654,43 @@ static void handleClientsBlockedOnKey(readyList *rl) { | |
| list *clients = dictGetVal(de); | ||
| listNode *ln; | ||
| listIter li; | ||
| long count = listLength(clients); | ||
| long snapshot_len = 0; | ||
| struct { | ||
| client *c; | ||
| uint64_t id; | ||
| } *snapshot; | ||
| long i; | ||
|
|
||
| /* Snapshot client* + id (not listNode*): serving one client may re-enter | ||
| * and free another still queued on this key (CLIENT KILL / evictClients), | ||
| * invalidating a listIter's cached successor. Prefer O(1) re-check via | ||
| * lookupClientByID(); fall back to a live-list scan for RM_Call fake | ||
| * clients that are absent from clients_index. */ | ||
| snapshot = zmalloc(sizeof(*snapshot) * count); | ||
| listRewind(clients, &li); | ||
| while ((ln = listNext(&li)) != NULL && snapshot_len < count) { | ||
| client *c = listNodeValue(ln); | ||
| snapshot[snapshot_len].c = c; | ||
| snapshot[snapshot_len].id = c->id; | ||
| snapshot_len++; | ||
| } | ||
|
|
||
| /* Avoid processing more than the initial count so that we're not stuck | ||
| * in an endless loop in case the reprocessing of the command blocks again. */ | ||
| long count = listLength(clients); | ||
| while ((ln = listNext(&li)) && count--) { | ||
| client *receiver = listNodeValue(ln); | ||
| robj *o = lookupKeyReadWithFlags(rl->db, rl->key, LOOKUP_NOEFFECTS); | ||
| for (i = 0; i < snapshot_len; i++) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't actually specific to the module reply path. The regular flow — |
||
| client *receiver = snapshot[i].c; | ||
| robj *o; | ||
|
|
||
| /* The client may have been killed/freed or may no longer be blocked | ||
| * on this key by the time we reach it. */ | ||
| if (lookupClientByID(snapshot[i].id) == receiver) { | ||
| if (!clientStillBlockedOnReadyKey(receiver, rl)) continue; | ||
| } else if (!clientStillInBlockingKeysList(rl, receiver)) { | ||
| continue; | ||
| } | ||
|
|
||
| o = lookupKeyReadWithFlags(rl->db, rl->key, LOOKUP_NOEFFECTS); | ||
| /* 1. In case new key was added/touched we need to verify it satisfy the | ||
| * blocked type, since we might process the wrong key type. | ||
| * 2. We want to serve clients blocked on module keys | ||
|
|
@@ -653,6 +706,7 @@ static void handleClientsBlockedOnKey(readyList *rl) { | |
| moduleUnblockClientOnKey(receiver, rl->key); | ||
| } | ||
| } | ||
| zfree(snapshot); | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dereferencing a pointer that was never assigned an initial value is UB
sizeof(*snapshot) * countshould be(sizeof(client*) + sizeof(uint64_t)) * countUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taking a look.
sizeof(*snapshot)does not evaluate/dereference
snapshot— sizeof on a non-VLA operand is unevaluatedand this is the usual C idiom for
malloc(sizeof(*p)).Using
sizeof(client*) + sizeof(uint64_t)would also be incorrectwhere the struct has padding (e.g. 32-bit), and could under-allocate.
In C23 N3220: §6.5.4.4 The sizeof and alignof operators,paragraph 2
And here is an example:

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you're right, thanks for the useful info