Fix SIGABRT in HSETEX when a field appears twice in the FIELDS list (#14956)

HSETEX crashed on assert() with a SIGABRT when the same field appeared
more than once in the FIELDS list and an expiry time was given
(EX/PX/EXAT/PXAT).

Root cause: hfieldPersist() and the KEEP_TTL path in hashTypeSet() both
asserted that dictExpireMeta->expireMeta.trash == 0, meaning the hash
must be globally registered in the HFE DS. This is incorrect during
HSETEX execution because hashTypeSetExDone(), which registers the hash
globally and clears trash, called only at the end of flow. The private
per-field ebuckets are fully valid regardless of the global registration state.

Fix: Remove both incorrect assertions. The operations on the private
ebuckets (ebRemove in hfieldPersist, ebAdd in the KEEP_TTL path) are
correct and do not require the hash to be globally registered.

Tests: Added two regression tests covering the crash scenarios:
- HSETEX EX with a duplicate field (existing field, expiry given)
- HSETEX FNX EX with a duplicate field (no prior field, FNX condition
passes)
This commit is contained in:
Moti Cohen 2026-04-16 13:16:52 +03:00 committed by GitHub
parent eb74450fca
commit fa6d4c3d63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 18 additions and 4 deletions

View file

@ -1022,7 +1022,6 @@ int hashTypeSet(redisDb *db, kvobj *o, sds field, sds value, int flags) {
if (newExpireAt != EB_EXPIRE_TIME_INVALID) {
dict *d = o->ptr;
htMetadataEx *dictExpireMeta = htGetMetadataEx(d);
serverAssert(dictExpireMeta->expireMeta.trash == 0);
ebAdd(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, newEntry, newExpireAt);
}
@ -3478,9 +3477,6 @@ static void hfieldPersist(robj *hashObj, Entry *entry) {
dict *d = hashObj->ptr;
htMetadataEx *dictExpireMeta = htGetMetadataEx(d);
/* If field has valid expiry then dict must have valid metadata as well */
serverAssert(dictExpireMeta->expireMeta.trash == 0);
/* Remove field from private HFE DS */
ebRemove(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, entry);

View file

@ -1277,6 +1277,24 @@ start_server {tags {"external:skip needs:debug"}} {
assert_range [r hpttl myhash FIELDS 1 f3] 4500 5000
}
test "HSETEX EX - field appears twice in FIELDS list with EX is allowed ($type)" {
# The EX condition passes, so all fields must be set, and the last value wins.
r del myhash
r hset myhash f1 v1
r hsetex myhash EX 100 FIELDS 2 f1 new1 f1 new2
# Last value wins (same as plain HSET behavior with duplicate fields)
assert_equal "new2" [r hget myhash f1]
assert_range [r httl myhash FIELDS 1 f1] 80 100
}
test "HSETEX FNX - field appears twice in FIELDS list with EX is allowed ($type)" {
# The FNX condition passes, so all fields must be set, and the last value wins.
r del myhash
r hsetex myhash FNX EX 100 FIELDS 2 f1 new1 f1 new2
assert_equal "new2" [r hget myhash f1]
assert_range [r httl myhash FIELDS 1 f1] 80 100
}
test "HSETEX - Test 'EX' flag ($type)" {
r del myhash
r hset myhash f1 v1 f2 v2