Skip to content
Permalink
Browse files Browse the repository at this point in the history
Fix ziplist and listpack overflows and truncations (CVE-2021-32627, C…
…VE-2021-32628)

- fix possible heap corruption in ziplist and listpack resulting by trying to
  allocate more than the maximum size of 4GB.
- prevent ziplist (hash and zset) from reaching size of above 1GB, will be
  converted to HT encoding, that's not a useful size.
- prevent listpack (stream) from reaching size of above 1GB.
- XADD will start a new listpack if the new record may cause the previous
  listpack to grow over 1GB.
- XADD will respond with an error if a single stream record is over 1GB
- List type (ziplist in quicklist) was truncating strings that were over 4GB,
  now it'll respond with an error.
  • Loading branch information
oranagra committed Oct 4, 2021
1 parent 666ed7f commit f6a4057
Show file tree
Hide file tree
Showing 13 changed files with 339 additions and 49 deletions.
5 changes: 3 additions & 2 deletions src/geo.c
Expand Up @@ -635,7 +635,7 @@ void georadiusGeneric(client *c, int flags) {
robj *zobj;
zset *zs;
int i;
size_t maxelelen = 0;
size_t maxelelen = 0, totelelen = 0;

if (returned_items) {
zobj = createZsetObject();
Expand All @@ -650,13 +650,14 @@ void georadiusGeneric(client *c, int flags) {
size_t elelen = sdslen(gp->member);

if (maxelelen < elelen) maxelelen = elelen;
totelelen += elelen;
znode = zslInsert(zs->zsl,score,gp->member);
serverAssert(dictAdd(zs->dict,gp->member,&znode->score) == DICT_OK);
gp->member = NULL;
}

if (returned_items) {
zsetConvertToZiplistIfNeeded(zobj,maxelelen);
zsetConvertToZiplistIfNeeded(zobj,maxelelen,totelelen);
setKey(c,c->db,storekey,zobj);
decrRefCount(zobj);
notifyKeyspaceEvent(NOTIFY_ZSET,"georadiusstore",storekey,
Expand Down
2 changes: 1 addition & 1 deletion src/listpack.c
Expand Up @@ -283,7 +283,7 @@ int lpEncodeGetType(unsigned char *ele, uint32_t size, unsigned char *intenc, ui
} else {
if (size < 64) *enclen = 1+size;
else if (size < 4096) *enclen = 2+size;
else *enclen = 5+size;
else *enclen = 5+(uint64_t)size;
return LP_ENCODING_STRING;
}
}
Expand Down
17 changes: 15 additions & 2 deletions src/quicklist.c
Expand Up @@ -29,6 +29,7 @@
*/

#include <string.h> /* for memcpy */
#include "redisassert.h"
#include "quicklist.h"
#include "zmalloc.h"
#include "ziplist.h"
Expand All @@ -43,11 +44,16 @@
#define REDIS_STATIC static
#endif

/* Optimization levels for size-based filling */
/* Optimization levels for size-based filling.
* Note that the largest possible limit is 16k, so even if each record takes
* just one byte, it still won't overflow the 16 bit count field. */
static const size_t optimization_level[] = {4096, 8192, 16384, 32768, 65536};

/* Maximum size in bytes of any multi-element ziplist.
* Larger values will live in their own isolated ziplists. */
* Larger values will live in their own isolated ziplists.
* This is used only if we're limited by record count. when we're limited by
* size, the maximum limit is bigger, but still safe.
* 8k is a recommended / default size limit */
#define SIZE_SAFETY_LIMIT 8192

/* Minimum ziplist size in bytes for attempting compression. */
Expand Down Expand Up @@ -449,6 +455,8 @@ REDIS_STATIC int _quicklistNodeAllowInsert(const quicklistNode *node,
unsigned int new_sz = node->sz + sz + ziplist_overhead;
if (likely(_quicklistNodeSizeMeetsOptimizationRequirement(new_sz, fill)))
return 1;
/* when we return 1 above we know that the limit is a size limit (which is
* safe, see comments next to optimization_level and SIZE_SAFETY_LIMIT) */
else if (!sizeMeetsSafetyLimit(new_sz))
return 0;
else if ((int)node->count < fill)
Expand All @@ -468,6 +476,8 @@ REDIS_STATIC int _quicklistNodeAllowMerge(const quicklistNode *a,
unsigned int merge_sz = a->sz + b->sz - 11;
if (likely(_quicklistNodeSizeMeetsOptimizationRequirement(merge_sz, fill)))
return 1;
/* when we return 1 above we know that the limit is a size limit (which is
* safe, see comments next to optimization_level and SIZE_SAFETY_LIMIT) */
else if (!sizeMeetsSafetyLimit(merge_sz))
return 0;
else if ((int)(a->count + b->count) <= fill)
Expand All @@ -487,6 +497,7 @@ REDIS_STATIC int _quicklistNodeAllowMerge(const quicklistNode *a,
* Returns 1 if new head created. */
int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
quicklistNode *orig_head = quicklist->head;
assert(sz < UINT32_MAX); /* TODO: add support for quicklist nodes that are sds encoded (not zipped) */
if (likely(
_quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
quicklist->head->zl =
Expand All @@ -510,6 +521,7 @@ int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
* Returns 1 if new tail created. */
int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
quicklistNode *orig_tail = quicklist->tail;
assert(sz < UINT32_MAX); /* TODO: add support for quicklist nodes that are sds encoded (not zipped) */
if (likely(
_quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
quicklist->tail->zl =
Expand Down Expand Up @@ -852,6 +864,7 @@ REDIS_STATIC void _quicklistInsert(quicklist *quicklist, quicklistEntry *entry,
int fill = quicklist->fill;
quicklistNode *node = entry->node;
quicklistNode *new_node = NULL;
assert(sz < UINT32_MAX); /* TODO: add support for quicklist nodes that are sds encoded (not zipped) */

if (!node) {
/* we have no reference node, so let's create only node in the list */
Expand Down
36 changes: 24 additions & 12 deletions src/rdb.c
Expand Up @@ -1561,7 +1561,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key) {
} else if (rdbtype == RDB_TYPE_ZSET_2 || rdbtype == RDB_TYPE_ZSET) {
/* Read list/set value. */
uint64_t zsetlen;
size_t maxelelen = 0;
size_t maxelelen = 0, totelelen = 0;
zset *zs;

if ((zsetlen = rdbLoadLen(rdb,NULL)) == RDB_LENERR) return NULL;
Expand Down Expand Up @@ -1598,15 +1598,19 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key) {

/* Don't care about integer-encoded strings. */
if (sdslen(sdsele) > maxelelen) maxelelen = sdslen(sdsele);
totelelen += sdslen(sdsele);

znode = zslInsert(zs->zsl,score,sdsele);
dictAdd(zs->dict,sdsele,&znode->score);
}

/* Convert *after* loading, since sorted sets are not stored ordered. */
if (zsetLength(o) <= server.zset_max_ziplist_entries &&
maxelelen <= server.zset_max_ziplist_value)
zsetConvert(o,OBJ_ENCODING_ZIPLIST);
maxelelen <= server.zset_max_ziplist_value &&
ziplistSafeToAdd(NULL, totelelen))
{
zsetConvert(o,OBJ_ENCODING_ZIPLIST);
}
} else if (rdbtype == RDB_TYPE_HASH) {
uint64_t len;
int ret;
Expand Down Expand Up @@ -1635,21 +1639,25 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key) {
return NULL;
}

/* Add pair to ziplist */
o->ptr = ziplistPush(o->ptr, (unsigned char*)field,
sdslen(field), ZIPLIST_TAIL);
o->ptr = ziplistPush(o->ptr, (unsigned char*)value,
sdslen(value), ZIPLIST_TAIL);

/* Convert to hash table if size threshold is exceeded */
if (sdslen(field) > server.hash_max_ziplist_value ||
sdslen(value) > server.hash_max_ziplist_value)
sdslen(value) > server.hash_max_ziplist_value ||
!ziplistSafeToAdd(o->ptr, sdslen(field)+sdslen(value)))
{
sdsfree(field);
sdsfree(value);
hashTypeConvert(o, OBJ_ENCODING_HT);
ret = dictAdd((dict*)o->ptr, field, value);
if (ret == DICT_ERR) {
rdbExitReportCorruptRDB("Duplicate hash fields detected");
}
break;
}

/* Add pair to ziplist */
o->ptr = ziplistPush(o->ptr, (unsigned char*)field,
sdslen(field), ZIPLIST_TAIL);
o->ptr = ziplistPush(o->ptr, (unsigned char*)value,
sdslen(value), ZIPLIST_TAIL);

sdsfree(field);
sdsfree(value);
}
Expand Down Expand Up @@ -1726,6 +1734,10 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key) {
while ((zi = zipmapNext(zi, &fstr, &flen, &vstr, &vlen)) != NULL) {
if (flen > maxlen) maxlen = flen;
if (vlen > maxlen) maxlen = vlen;
if (!ziplistSafeToAdd(zl, (size_t)flen + vlen)) {
rdbExitReportCorruptRDB("Hash zipmap too big (%u)", flen);
}

zl = ziplistPush(zl, fstr, flen, ZIPLIST_TAIL);
zl = ziplistPush(zl, vstr, vlen, ZIPLIST_TAIL);
}
Expand Down
2 changes: 1 addition & 1 deletion src/server.h
Expand Up @@ -1999,7 +1999,7 @@ unsigned char *zzlFirstInRange(unsigned char *zl, zrangespec *range);
unsigned char *zzlLastInRange(unsigned char *zl, zrangespec *range);
unsigned long zsetLength(const robj *zobj);
void zsetConvert(robj *zobj, int encoding);
void zsetConvertToZiplistIfNeeded(robj *zobj, size_t maxelelen);
void zsetConvertToZiplistIfNeeded(robj *zobj, size_t maxelelen, size_t totelelen);
int zsetScore(robj *zobj, sds member, double *score);
unsigned long zslGetRank(zskiplist *zsl, double score, sds o);
int zsetAdd(robj *zobj, double score, sds ele, int *flags, double *newscore);
Expand Down
13 changes: 9 additions & 4 deletions src/t_hash.c
Expand Up @@ -39,17 +39,22 @@
* as their string length can be queried in constant time. */
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
int i;
size_t sum = 0;

if (o->encoding != OBJ_ENCODING_ZIPLIST) return;

for (i = start; i <= end; i++) {
if (sdsEncodedObject(argv[i]) &&
sdslen(argv[i]->ptr) > server.hash_max_ziplist_value)
{
if (!sdsEncodedObject(argv[i]))
continue;
size_t len = sdslen(argv[i]->ptr);
if (len > server.hash_max_ziplist_value) {
hashTypeConvert(o, OBJ_ENCODING_HT);
break;
return;
}
sum += len;
}
if (!ziplistSafeToAdd(o->ptr, sum))
hashTypeConvert(o, OBJ_ENCODING_HT);
}

/* Get the value from a ziplist encoded hash, identified by field.
Expand Down
30 changes: 30 additions & 0 deletions src/t_list.c
Expand Up @@ -29,6 +29,8 @@

#include "server.h"

#define LIST_MAX_ITEM_SIZE ((1ull<<32)-1024)

/*-----------------------------------------------------------------------------
* List API
*----------------------------------------------------------------------------*/
Expand Down Expand Up @@ -196,6 +198,14 @@ void listTypeConvert(robj *subject, int enc) {

void pushGenericCommand(client *c, int where) {
int j, pushed = 0;

for (j = 2; j < c->argc; j++) {
if (sdslen(c->argv[j]->ptr) > LIST_MAX_ITEM_SIZE) {
addReplyError(c, "Element too large");
return;
}
}

robj *lobj = lookupKeyWrite(c->db,c->argv[1]);

if (lobj && lobj->type != OBJ_LIST) {
Expand Down Expand Up @@ -277,6 +287,11 @@ void linsertCommand(client *c) {
return;
}

if (sdslen(c->argv[4]->ptr) > LIST_MAX_ITEM_SIZE) {
addReplyError(c, "Element too large");
return;
}

if ((subject = lookupKeyWriteOrReply(c,c->argv[1],shared.czero)) == NULL ||
checkType(c,subject,OBJ_LIST)) return;

Expand Down Expand Up @@ -344,6 +359,11 @@ void lsetCommand(client *c) {
long index;
robj *value = c->argv[3];

if (sdslen(value->ptr) > LIST_MAX_ITEM_SIZE) {
addReplyError(c, "Element too large");
return;
}

if ((getLongFromObjectOrReply(c, c->argv[2], &index, NULL) != C_OK))
return;

Expand Down Expand Up @@ -510,6 +530,11 @@ void lposCommand(client *c) {
int direction = LIST_TAIL;
long rank = 1, count = -1, maxlen = 0; /* Count -1: option not given. */

if (sdslen(ele->ptr) > LIST_MAX_ITEM_SIZE) {
addReplyError(c, "Element too large");
return;
}

/* Parse the optional arguments. */
for (int j = 3; j < c->argc; j++) {
char *opt = c->argv[j]->ptr;
Expand Down Expand Up @@ -610,6 +635,11 @@ void lremCommand(client *c) {
long toremove;
long removed = 0;

if (sdslen(obj->ptr) > LIST_MAX_ITEM_SIZE) {
addReplyError(c, "Element too large");
return;
}

if ((getLongFromObjectOrReply(c, c->argv[2], &toremove, NULL) != C_OK))
return;

Expand Down
48 changes: 38 additions & 10 deletions src/t_stream.c
Expand Up @@ -40,6 +40,12 @@
#define STREAM_ITEM_FLAG_DELETED (1<<0) /* Entry is deleted. Skip it. */
#define STREAM_ITEM_FLAG_SAMEFIELDS (1<<1) /* Same fields as master entry. */

/* Don't let listpacks grow too big, even if the user config allows it.
* doing so can lead to an overflow (trying to store more than 32bit length
* into the listpack header), or actually an assertion since lpInsert
* will return NULL. */
#define STREAM_LISTPACK_MAX_SIZE (1<<30)

void streamFreeCG(streamCG *cg);
void streamFreeNACK(streamNACK *na);
size_t streamReplyWithRangeFromConsumerPEL(client *c, stream *s, streamID *start, streamID *end, size_t count, streamConsumer *consumer);
Expand Down Expand Up @@ -191,8 +197,11 @@ int streamCompareID(streamID *a, streamID *b) {
*
* The function returns C_OK if the item was added, this is always true
* if the ID was generated by the function. However the function may return
* C_ERR if an ID was given via 'use_id', but adding it failed since the
* current top ID is greater or equal. */
* C_ERR in several cases:
* 1. If an ID was given via 'use_id', but adding it failed since the
* current top ID is greater or equal. errno will be set to EDOM.
* 2. If a size of a single element or the sum of the elements is too big to
* be stored into the stream. errno will be set to ERANGE. */
int streamAppendItem(stream *s, robj **argv, int64_t numfields, streamID *added_id, streamID *use_id) {

/* Generate the new entry ID. */
Expand All @@ -206,7 +215,23 @@ int streamAppendItem(stream *s, robj **argv, int64_t numfields, streamID *added_
* or return an error. Automatically generated IDs might
* overflow (and wrap-around) when incrementing the sequence
part. */
if (streamCompareID(&id,&s->last_id) <= 0) return C_ERR;
if (streamCompareID(&id,&s->last_id) <= 0) {
errno = EDOM;
return C_ERR;
}

/* Avoid overflow when trying to add an element to the stream (listpack
* can only host up to 32bit length sttrings, and also a total listpack size
* can't be bigger than 32bit length. */
size_t totelelen = 0;
for (int64_t i = 0; i < numfields*2; i++) {
sds ele = argv[i]->ptr;
totelelen += sdslen(ele);
}
if (totelelen > STREAM_LISTPACK_MAX_SIZE) {
errno = ERANGE;
return C_ERR;
}

/* Add the new entry. */
raxIterator ri;
Expand Down Expand Up @@ -265,9 +290,10 @@ int streamAppendItem(stream *s, robj **argv, int64_t numfields, streamID *added_
* if we need to switch to the next one. 'lp' will be set to NULL if
* the current node is full. */
if (lp != NULL) {
if (server.stream_node_max_bytes &&
lp_bytes >= server.stream_node_max_bytes)
{
size_t node_max_bytes = server.stream_node_max_bytes;
if (node_max_bytes == 0 || node_max_bytes > STREAM_LISTPACK_MAX_SIZE)
node_max_bytes = STREAM_LISTPACK_MAX_SIZE;
if (lp_bytes + totelelen >= node_max_bytes) {
lp = NULL;
} else if (server.stream_node_max_entries) {
int64_t count = lpGetInteger(lpFirst(lp));
Expand Down Expand Up @@ -1267,11 +1293,13 @@ void xaddCommand(client *c) {

/* Append using the low level function and return the ID. */
if (streamAppendItem(s,c->argv+field_pos,(c->argc-field_pos)/2,
&id, id_given ? &id : NULL)
== C_ERR)
&id, id_given ? &id : NULL) == C_ERR)
{
addReplyError(c,"The ID specified in XADD is equal or smaller than the "
"target stream top item");
if (errno == EDOM)
addReplyError(c,"The ID specified in XADD is equal or smaller than "
"the target stream top item");
else
addReplyError(c,"Elements are too large to be stored");
return;
}
addReplyStreamID(c,&id);
Expand Down

0 comments on commit f6a4057

Please sign in to comment.