Unverified Commit 8e0264cf authored by Michael Grunder's avatar Michael Grunder Committed by GitHub
Browse files

Allow users to replace allocator and handle OOM everywhere. (#800)

* Adds an indirection to every allocation/deallocation to allow users to 
  plug in ones of their choosing (use custom functions, jemalloc, etc).

* Gracefully handle OOM everywhere in hiredis.  This should make it possible
  for users of the library to have more flexibility in how they handle such situations.

* Changes `redisReaderTask->elements` from an `int` to a `long long` to prevent
  a possible overflow when transferring the task elements into a `redisReply`.

* Adds a configurable `max elements` member to `redisReader` that defaults to
  2^32 - 1.  This can be set to "unlimited" by setting the value to zero.
parent 83bba659
......@@ -37,6 +37,8 @@
* the include of your alternate allocator if needed (not needed in order
* to use the default libc allocator). */
#define s_malloc malloc
#define s_realloc realloc
#define s_free free
#include "alloc.h"
#define s_malloc hi_malloc
#define s_realloc hi_realloc
#define s_free hi_free
......@@ -163,18 +163,22 @@ static void opensslDoLock(int mode, int lkid, const char *f, int line) {
(void)line;
}
static void initOpensslLocks(void) {
static int initOpensslLocks(void) {
unsigned ii, nlocks;
if (CRYPTO_get_locking_callback() != NULL) {
/* Someone already set the callback before us. Don't destroy it! */
return;
return REDIS_OK;
}
nlocks = CRYPTO_num_locks();
ossl_locks = hi_malloc(sizeof(*ossl_locks) * nlocks);
if (ossl_locks == NULL)
return REDIS_ERR;
for (ii = 0; ii < nlocks; ii++) {
sslLockInit(ossl_locks + ii);
}
CRYPTO_set_locking_callback(opensslDoLock);
return REDIS_OK;
}
#endif /* HIREDIS_USE_CRYPTO_LOCKS */
......@@ -183,15 +187,20 @@ static void initOpensslLocks(void) {
*/
static int redisSSLConnect(redisContext *c, SSL_CTX *ssl_ctx, SSL *ssl) {
redisSSLContext *rssl;
if (c->privdata) {
__redisSetError(c, REDIS_ERR_OTHER, "redisContext was already associated");
return REDIS_ERR;
}
c->privdata = calloc(1, sizeof(redisSSLContext));
c->funcs = &redisContextSSLFuncs;
redisSSLContext *rssl = c->privdata;
rssl = hi_calloc(1, sizeof(redisSSLContext));
if (rssl == NULL) {
__redisSetError(c, REDIS_ERR_OOM, "Out of memory");
return REDIS_ERR;
}
c->funcs = &redisContextSSLFuncs;
rssl->ssl_ctx = ssl_ctx;
rssl->ssl = ssl;
......@@ -202,12 +211,14 @@ static int redisSSLConnect(redisContext *c, SSL_CTX *ssl_ctx, SSL *ssl) {
ERR_clear_error();
int rv = SSL_connect(rssl->ssl);
if (rv == 1) {
c->privdata = rssl;
return REDIS_OK;
}
rv = SSL_get_error(rssl->ssl, rv);
if (((c->flags & REDIS_BLOCK) == 0) &&
(rv == SSL_ERROR_WANT_READ || rv == SSL_ERROR_WANT_WRITE)) {
c->privdata = rssl;
return REDIS_OK;
}
......@@ -222,6 +233,8 @@ static int redisSSLConnect(redisContext *c, SSL_CTX *ssl_ctx, SSL *ssl) {
}
__redisSetError(c, REDIS_ERR_IO, err);
}
hi_free(rssl);
return REDIS_ERR;
}
......@@ -241,7 +254,10 @@ int redisSecureConnection(redisContext *c, const char *capath,
isInit = 1;
SSL_library_init();
#ifdef HIREDIS_USE_CRYPTO_LOCKS
initOpensslLocks();
if (initOpensslLocks() == REDIS_ERR) {
__redisSetError(c, REDIS_ERR_OOM, "Out of memory");
goto error;
}
#endif
}
......@@ -290,7 +306,8 @@ int redisSecureConnection(redisContext *c, const char *capath,
}
}
return redisSSLConnect(c, ssl_ctx, ssl);
if (redisSSLConnect(c, ssl_ctx, ssl) == REDIS_OK)
return REDIS_OK;
error:
if (ssl) SSL_free(ssl);
......@@ -330,7 +347,7 @@ static void redisSSLFreeContext(void *privdata){
SSL_CTX_free(rsc->ssl_ctx);
rsc->ssl_ctx = NULL;
}
free(rsc);
hi_free(rsc);
}
static int redisSSLRead(redisContext *c, char *buf, size_t bufcap) {
......
......@@ -180,43 +180,43 @@ static void test_format_commands(void) {
len = redisFormatCommand(&cmd,"SET foo bar");
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(3+2)+4+(3+2));
free(cmd);
hi_free(cmd);
test("Format command with %%s string interpolation: ");
len = redisFormatCommand(&cmd,"SET %s %s","foo","bar");
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(3+2)+4+(3+2));
free(cmd);
hi_free(cmd);
test("Format command with %%s and an empty string: ");
len = redisFormatCommand(&cmd,"SET %s %s","foo","");
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$0\r\n\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(3+2)+4+(0+2));
free(cmd);
hi_free(cmd);
test("Format command with an empty string in between proper interpolations: ");
len = redisFormatCommand(&cmd,"SET %s %s","","foo");
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$0\r\n\r\n$3\r\nfoo\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(0+2)+4+(3+2));
free(cmd);
hi_free(cmd);
test("Format command with %%b string interpolation: ");
len = redisFormatCommand(&cmd,"SET %b %b","foo",(size_t)3,"b\0r",(size_t)3);
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nb\0r\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(3+2)+4+(3+2));
free(cmd);
hi_free(cmd);
test("Format command with %%b and an empty string: ");
len = redisFormatCommand(&cmd,"SET %b %b","foo",(size_t)3,"",(size_t)0);
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$0\r\n\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(3+2)+4+(0+2));
free(cmd);
hi_free(cmd);
test("Format command with literal %%: ");
len = redisFormatCommand(&cmd,"SET %% %%");
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$1\r\n%\r\n$1\r\n%\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(1+2)+4+(1+2));
free(cmd);
hi_free(cmd);
/* Vararg width depends on the type. These tests make sure that the
* width is correctly determined using the format and subsequent varargs
......@@ -227,7 +227,7 @@ static void test_format_commands(void) {
len = redisFormatCommand(&cmd,"key:%08" fmt " str:%s", value, "hello"); \
test_cond(strncmp(cmd,"*2\r\n$12\r\nkey:00000123\r\n$9\r\nstr:hello\r\n",len) == 0 && \
len == 4+5+(12+2)+4+(9+2)); \
free(cmd); \
hi_free(cmd); \
} while(0)
#define FLOAT_WIDTH_TEST(type) do { \
......@@ -236,7 +236,7 @@ static void test_format_commands(void) {
len = redisFormatCommand(&cmd,"key:%08.3f str:%s", value, "hello"); \
test_cond(strncmp(cmd,"*2\r\n$12\r\nkey:0123.000\r\n$9\r\nstr:hello\r\n",len) == 0 && \
len == 4+5+(12+2)+4+(9+2)); \
free(cmd); \
hi_free(cmd); \
} while(0)
INTEGER_WIDTH_TEST("d", int);
......@@ -267,13 +267,13 @@ static void test_format_commands(void) {
len = redisFormatCommandArgv(&cmd,argc,argv,NULL);
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(3+2)+4+(3+2));
free(cmd);
hi_free(cmd);
test("Format command by passing argc/argv with lengths: ");
len = redisFormatCommandArgv(&cmd,argc,argv,lens);
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$7\r\nfoo\0xxx\r\n$3\r\nbar\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(7+2)+4+(3+2));
free(cmd);
hi_free(cmd);
sds sds_cmd;
......@@ -308,7 +308,7 @@ static void test_append_formatted_commands(struct config config) {
assert(redisGetReply(c, (void*)&reply) == REDIS_OK);
free(cmd);
hi_free(cmd);
freeReplyObject(reply);
disconnect(c, 0);
......@@ -418,6 +418,16 @@ static void test_reply_reader(void) {
freeReplyObject(reply);
redisReaderFree(reader);
test("Can configure maximum multi-bulk elements: ");
reader = redisReaderCreate();
reader->maxelements = 1024;
redisReaderFeed(reader, "*1025\r\n", 7);
ret = redisReaderGetReply(reader,&reply);
test_cond(ret == REDIS_ERR &&
strcasecmp(reader->errstr, "Multi-bulk length out of range") == 0);
freeReplyObject(reply);
redisReaderFree(reader);
#if LLONG_MAX > SIZE_MAX
test("Set error when array > SIZE_MAX: ");
reader = redisReaderCreate();
......@@ -518,6 +528,46 @@ static void test_free_null(void) {
test_cond(reply == NULL);
}
static void *hi_malloc_fail(size_t size) {
(void)size;
return NULL;
}
static void *hi_calloc_fail(size_t nmemb, size_t size) {
(void)nmemb;
(void)size;
return NULL;
}
static void *hi_realloc_fail(void *ptr, size_t size) {
(void)ptr;
(void)size;
return NULL;
}
static void test_allocator_injection(void) {
hiredisAllocFuncs ha = {
.malloc = hi_malloc_fail,
.calloc = hi_calloc_fail,
.realloc = hi_realloc_fail,
.free = NULL,
};
// Override hiredis allocators
hiredisSetAllocators(&ha);
test("redisContext uses injected allocators: ");
redisContext *c = redisConnect("localhost", 6379);
test_cond(c == NULL);
test("redisReader uses injected allocators: ");
redisReader *reader = redisReaderCreate();
test_cond(reader == NULL);
// Return allocators to default
hiredisResetAllocators();
}
#define HIREDIS_BAD_DOMAIN "idontexist-noreally.com"
static void test_blocking_connection_errors(void) {
redisContext *c;
......@@ -799,6 +849,18 @@ static void test_invalid_timeout_errors(struct config config) {
redisFree(c);
}
/* Wrap malloc to abort on failure so OOM checks don't make the test logic
* harder to follow. */
void *hi_malloc_safe(size_t size) {
void *ptr = hi_malloc(size);
if (ptr == NULL) {
fprintf(stderr, "Error: Out of memory\n");
exit(-1);
}
return ptr;
}
static void test_throughput(struct config config) {
redisContext *c = do_connect(config);
redisReply **replies;
......@@ -810,7 +872,7 @@ static void test_throughput(struct config config) {
freeReplyObject(redisCommand(c,"LPUSH mylist foo"));
num = 1000;
replies = malloc(sizeof(redisReply*)*num);
replies = hi_malloc_safe(sizeof(redisReply*)*num);
t1 = usec();
for (i = 0; i < num; i++) {
replies[i] = redisCommand(c,"PING");
......@@ -818,10 +880,10 @@ static void test_throughput(struct config config) {
}
t2 = usec();
for (i = 0; i < num; i++) freeReplyObject(replies[i]);
free(replies);
hi_free(replies);
printf("\t(%dx PING: %.3fs)\n", num, (t2-t1)/1000000.0);
replies = malloc(sizeof(redisReply*)*num);
replies = hi_malloc_safe(sizeof(redisReply*)*num);
t1 = usec();
for (i = 0; i < num; i++) {
replies[i] = redisCommand(c,"LRANGE mylist 0 499");
......@@ -830,10 +892,10 @@ static void test_throughput(struct config config) {
}
t2 = usec();
for (i = 0; i < num; i++) freeReplyObject(replies[i]);
free(replies);
hi_free(replies);
printf("\t(%dx LRANGE with 500 elements: %.3fs)\n", num, (t2-t1)/1000000.0);
replies = malloc(sizeof(redisReply*)*num);
replies = hi_malloc_safe(sizeof(redisReply*)*num);
t1 = usec();
for (i = 0; i < num; i++) {
replies[i] = redisCommand(c, "INCRBY incrkey %d", 1000000);
......@@ -841,11 +903,11 @@ static void test_throughput(struct config config) {
}
t2 = usec();
for (i = 0; i < num; i++) freeReplyObject(replies[i]);
free(replies);
hi_free(replies);
printf("\t(%dx INCRBY: %.3fs)\n", num, (t2-t1)/1000000.0);
num = 10000;
replies = malloc(sizeof(redisReply*)*num);
replies = hi_malloc_safe(sizeof(redisReply*)*num);
for (i = 0; i < num; i++)
redisAppendCommand(c,"PING");
t1 = usec();
......@@ -855,10 +917,10 @@ static void test_throughput(struct config config) {
}
t2 = usec();
for (i = 0; i < num; i++) freeReplyObject(replies[i]);
free(replies);
hi_free(replies);
printf("\t(%dx PING (pipelined): %.3fs)\n", num, (t2-t1)/1000000.0);
replies = malloc(sizeof(redisReply*)*num);
replies = hi_malloc_safe(sizeof(redisReply*)*num);
for (i = 0; i < num; i++)
redisAppendCommand(c,"LRANGE mylist 0 499");
t1 = usec();
......@@ -869,10 +931,10 @@ static void test_throughput(struct config config) {
}
t2 = usec();
for (i = 0; i < num; i++) freeReplyObject(replies[i]);
free(replies);
hi_free(replies);
printf("\t(%dx LRANGE with 500 elements (pipelined): %.3fs)\n", num, (t2-t1)/1000000.0);
replies = malloc(sizeof(redisReply*)*num);
replies = hi_malloc_safe(sizeof(redisReply*)*num);
for (i = 0; i < num; i++)
redisAppendCommand(c,"INCRBY incrkey %d", 1000000);
t1 = usec();
......@@ -882,7 +944,7 @@ static void test_throughput(struct config config) {
}
t2 = usec();
for (i = 0; i < num; i++) freeReplyObject(replies[i]);
free(replies);
hi_free(replies);
printf("\t(%dx INCRBY (pipelined): %.3fs)\n", num, (t2-t1)/1000000.0);
disconnect(c, 0);
......@@ -1049,11 +1111,14 @@ int main(int argc, char **argv) {
signal(SIGPIPE, SIG_IGN);
test_unix_socket = access(cfg.unix_sock.path, F_OK) == 0;
#else
/* Unix sockets don't exist in Windows */
test_unix_socket = 0;
#endif
test_allocator_injection();
test_format_commands();
test_reply_reader();
test_blocking_connection_errors();
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment