-
Notifications
You must be signed in to change notification settings - Fork 0
Fix HINCRBYFLOAT removes field expiration on replica #8
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: claude_claude_vs_qodo_base_fix_hincrbyfloat_removes_field_expiration_on_replica_pr8
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -706,24 +706,29 @@ GetFieldRes hashTypeGetFromHashTable(robj *o, sds field, sds *value, uint64_t *e | |
| * If *vll is populated *vstr is set to NULL, so the caller can | ||
| * always check the function return by checking the return value | ||
| * for GETF_OK and checking if vll (or vstr) is NULL. | ||
| * expiredAt - if the field has an expiration time, it will be set to the expiration | ||
| * time of the field. Otherwise, will be set to EB_EXPIRE_TIME_INVALID. | ||
| * | ||
| */ | ||
| GetFieldRes hashTypeGetValue(redisDb *db, robj *o, sds field, unsigned char **vstr, | ||
| unsigned int *vlen, long long *vll, int hfeFlags) { | ||
| uint64_t expiredAt; | ||
| unsigned int *vlen, long long *vll, int hfeFlags, uint64_t *expiredAt) | ||
| { | ||
| sds key; | ||
| GetFieldRes res; | ||
| uint64_t dummy; | ||
| if (expiredAt == NULL) expiredAt = &dummy; | ||
|
|
||
| if (o->encoding == OBJ_ENCODING_LISTPACK || | ||
| o->encoding == OBJ_ENCODING_LISTPACK_EX) { | ||
| *vstr = NULL; | ||
| res = hashTypeGetFromListpack(o, field, vstr, vlen, vll, &expiredAt); | ||
| res = hashTypeGetFromListpack(o, field, vstr, vlen, vll, expiredAt); | ||
|
|
||
| if (res == GETF_NOT_FOUND) | ||
| return GETF_NOT_FOUND; | ||
|
|
||
| } else if (o->encoding == OBJ_ENCODING_HT) { | ||
| sds value = NULL; | ||
| res = hashTypeGetFromHashTable(o, field, &value, &expiredAt); | ||
| res = hashTypeGetFromHashTable(o, field, &value, expiredAt); | ||
|
|
||
| if (res == GETF_NOT_FOUND) | ||
| return GETF_NOT_FOUND; | ||
|
|
@@ -734,7 +739,7 @@ GetFieldRes hashTypeGetValue(redisDb *db, robj *o, sds field, unsigned char **vs | |
| serverPanic("Unknown hash encoding"); | ||
| } | ||
|
|
||
| if (expiredAt >= (uint64_t) commandTimeSnapshot()) | ||
| if (*expiredAt > (uint64_t) commandTimeSnapshot()) | ||
| return GETF_OK; | ||
|
|
||
| if (server.masterhost) { | ||
|
|
@@ -794,7 +799,7 @@ robj *hashTypeGetValueObject(redisDb *db, robj *o, sds field, int hfeFlags, int | |
| long long vll; | ||
|
|
||
| if (isHashDeleted) *isHashDeleted = 0; | ||
| GetFieldRes res = hashTypeGetValue(db,o,field,&vstr,&vlen,&vll, hfeFlags); | ||
| GetFieldRes res = hashTypeGetValue(db,o,field,&vstr,&vlen,&vll, hfeFlags, NULL); | ||
|
|
||
| if (res == GETF_OK) { | ||
| if (vstr) return createStringObject((char*)vstr,vlen); | ||
|
|
@@ -823,7 +828,7 @@ int hashTypeExists(redisDb *db, robj *o, sds field, int hfeFlags, int *isHashDel | |
| unsigned int vlen = UINT_MAX; | ||
| long long vll = LLONG_MAX; | ||
|
|
||
| GetFieldRes res = hashTypeGetValue(db, o, field, &vstr, &vlen, &vll, hfeFlags); | ||
| GetFieldRes res = hashTypeGetValue(db, o, field, &vstr, &vlen, &vll, hfeFlags, NULL); | ||
| if (isHashDeleted) | ||
| *isHashDeleted = (res == GETF_EXPIRED_HASH) ? 1 : 0; | ||
| return (res == GETF_OK) ? 1 : 0; | ||
|
|
@@ -2195,7 +2200,7 @@ void hincrbyCommand(client *c) { | |
| if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return; | ||
|
|
||
| GetFieldRes res = hashTypeGetValue(c->db,o,c->argv[2]->ptr,&vstr,&vlen,&value, | ||
| HFE_LAZY_EXPIRE); | ||
| HFE_LAZY_EXPIRE, NULL); | ||
| if (res == GETF_OK) { | ||
| if (vstr) { | ||
| if (string2ll((char*)vstr,vlen,&value) == 0) { | ||
|
|
@@ -2234,6 +2239,9 @@ void hincrbyfloatCommand(client *c) { | |
| sds new; | ||
| unsigned char *vstr; | ||
| unsigned int vlen; | ||
| int has_expiration = 0; | ||
| uint64_t expireat = EB_EXPIRE_TIME_INVALID; | ||
| int unused_flag = 0; | ||
|
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. 🟡 The variable Extended reasoning...What the bug isIn How it was introducedThis PR adds expiration-preservation logic to ImpactThis is a minor code quality issue. The variable has no effect on runtime behavior or correctness. However, compiling with Step-by-step confirmation
FixSimply delete line 2244 ( |
||
|
|
||
| if (getLongDoubleFromObjectOrReply(c,c->argv[3],&incr,NULL) != C_OK) return; | ||
| if (isnan(incr) || isinf(incr)) { | ||
|
|
@@ -2242,7 +2250,7 @@ void hincrbyfloatCommand(client *c) { | |
| } | ||
| if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return; | ||
| GetFieldRes res = hashTypeGetValue(c->db, o,c->argv[2]->ptr,&vstr,&vlen,&ll, | ||
| HFE_LAZY_EXPIRE); | ||
| HFE_LAZY_EXPIRE, &expireat); | ||
| if (res == GETF_OK) { | ||
| if (vstr) { | ||
| if (string2ld((char*)vstr,vlen,&value) == 0) { | ||
|
|
@@ -2252,6 +2260,8 @@ void hincrbyfloatCommand(client *c) { | |
| } else { | ||
| value = (long double)ll; | ||
| } | ||
| /* Field has expiration time. */ | ||
| if (expireat != EB_EXPIRE_TIME_INVALID) has_expiration = 1; | ||
| } else if ((res == GETF_NOT_FOUND) || (res == GETF_EXPIRED)) { | ||
| value = 0; | ||
| } else { | ||
|
|
@@ -2284,6 +2294,24 @@ void hincrbyfloatCommand(client *c) { | |
| rewriteClientCommandArgument(c,0,shared.hset); | ||
| rewriteClientCommandArgument(c,3,newobj); | ||
| decrRefCount(newobj); | ||
|
|
||
| if (has_expiration) { | ||
| /* To make sure that the HSET command is propagated before the HPEXPIREAT, | ||
| * we need to prevent the HSET command from being propagated, and then | ||
| * propagate both commands manually in the correct order. */ | ||
| preventCommandPropagation(c); | ||
| /* Propagate HSET */ | ||
| alsoPropagate(c->db->id, c->argv, c->argc, PROPAGATE_AOF|PROPAGATE_REPL); | ||
| /* Propagate HPEXPIREAT */ | ||
| robj *argv[5]; | ||
| argv[0] = shared.hpexpireat; | ||
| argv[1] = c->argv[1]; | ||
| argv[2] = createStringObjectFromLongLong(expireat); | ||
| argv[3] = shared.fields; | ||
| argv[4] = shared.integers[1]; | ||
| argv[5] = c->argv[2]; | ||
| alsoPropagate(c->db->id, argv, 6, PROPAGATE_AOF|PROPAGATE_REPL); | ||
|
Comment on lines
+2306
to
+2313
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. 🔴 Stack buffer overflow and memory leak in HPEXPIREAT propagation block (lines 2306-2313). Extended reasoning...Buffer overflowAt line 2306, the array is declared as This corrupts the stack on every Memory leakAt line 2309, The correct pattern is demonstrated in Note that even if Step-by-step proof
ImpactThe buffer overflow is critical — it causes undefined behavior on every |
||
| } | ||
| } | ||
|
|
||
| static GetFieldRes addHashFieldToReply(client *c, robj *o, sds field, int hfeFlags) { | ||
|
|
@@ -2296,7 +2324,7 @@ static GetFieldRes addHashFieldToReply(client *c, robj *o, sds field, int hfeFla | |
| unsigned int vlen = UINT_MAX; | ||
| long long vll = LLONG_MAX; | ||
|
|
||
| GetFieldRes res = hashTypeGetValue(c->db, o, field, &vstr, &vlen, &vll, hfeFlags); | ||
| GetFieldRes res = hashTypeGetValue(c->db, o, field, &vstr, &vlen, &vll, hfeFlags, NULL); | ||
| if (res == GETF_OK) { | ||
| if (vstr) { | ||
| addReplyBulkCBuffer(c, vstr, vlen); | ||
|
|
@@ -2408,7 +2436,7 @@ void hstrlenCommand(client *c) { | |
| checkType(c,o,OBJ_HASH)) return; | ||
|
|
||
| GetFieldRes res = hashTypeGetValue(c->db, o, c->argv[2]->ptr, &vstr, &vlen, &vll, | ||
| HFE_LAZY_EXPIRE); | ||
| HFE_LAZY_EXPIRE, NULL); | ||
|
|
||
| if (res == GETF_NOT_FOUND || res == GETF_EXPIRED || res == GETF_EXPIRED_HASH) { | ||
| addReply(c, shared.czero); | ||
|
|
||
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.
🟡 Off-by-one in expiration comparison: the operator was changed from
>=to>at line 742, which meansexpiredAt == commandTimeSnapshot()now falls through as expired instead of returningGETF_OK. Every other expiration check in the codebase (hashTypeIsExpired,keyIsExpired, hash iterator,HTTL,HPERSIST) uses strict less-than for the expired condition, treating the exact-millisecond boundary as NOT expired. The>=should be restored alongside the pointer dereference fix.Extended reasoning...
What the bug is
In
hashTypeGetValueatsrc/t_hash.c:742, the PR changes the expiration check from:to:
The pointer dereference change (
expiredAt→*expiredAt) is correct and necessary because the parameter was changed from a localuint64_tto auint64_t *output parameter. However, the comparison operator was also changed from>=to>, which alters the semantics at the exact-millisecond boundary.How it manifests
With the original
>=: whenexpiredAt == commandTimeSnapshot(), the condition is true,GETF_OKis returned, and the field is considered not expired.With the new
>: when*expiredAt == commandTimeSnapshot(), the condition is false, execution falls through to the lazy-deletion path, and the field is considered expired.Inconsistency with the rest of the codebase
Every other expiration check uses strict less-than (
<) for the "is expired" condition, meaningexpireAt == nowis treated as NOT expired:hashTypeIsExpired(line 574):return (mstime_t) expireAt < commandTimeSnapshot();keyIsExpired(db.c:1941):return now > when;(mstime_t)hi->expire_time < commandTimeSnapshot()(long long) expire < commandTimeSnapshot()(long long) expire < commandTimeSnapshot()All of these consistently say: at the exact millisecond boundary, the field/key is still valid.
Step-by-step proof
f1hasexpiredAt = 1000(milliseconds).commandTimeSnapshot()returns1000.hashTypeGetValue(new code):*expiredAt > 1000→1000 > 1000→ false → field falls through to lazy deletion → field is expired.hashTypeIsExpired:(mstime_t)1000 < 1000→ false → field is not expired.HGETwould delete it, butHSCAN(which uses the iterator callinghashTypeIsExpired) would still return it.Impact
The practical impact is limited to a 1ms race window, making it very unlikely to trigger in production. However, it is a genuine semantic inconsistency introduced by this PR that could cause subtly different behavior depending on which code path handles the field.
Fix
Restore the
>=operator while keeping the pointer dereference: