-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Allow users to replace allocator and handle OOM everywhere. #800
Allow users to replace allocator and handle OOM everywhere. #800
Conversation
* Replace every allocation/free call with defined hiredis variants. This should make it trivial for users of the library to replace the allocator with one of their choosing. * Add logic to handle OOM everywhere in the library to allow for graceful exit in the event of OOM.
One more thing before I forget. I think we may want to impose a sane limit on multi-bulk elements. I needed to do that locally or fuzz testing would immediately break hiredis by sending something like:
To To fix it locally for fuzz testing I just did this: diff --git hiredis.c hiredis.c
index 6ce3049..37795d1 100644
--- hiredis.c
+++ hiredis.c
@@ -44,6 +44,9 @@
#include "async.h"
#include "win32.h"
+/* A sane value for maximum number of array elements */
+#define HIREDIS_MAX_ARRAY_ELEMENTS ((1LL<<32) - 1)
+
static redisContextFuncs redisContextDefaultFuncs = {
.free_privdata = NULL,
.async_read = redisAsyncRead,
@@ -150,6 +153,9 @@ static void *createStringObject(const redisReadTask *task, char *str, size_t len
static void *createArrayObject(const redisReadTask *task, size_t elements) {
redisReply *r, *parent;
+ if (elements > HIREDIS_MAX_ARRAY_ELEMENTS)
+ return NULL;
+
r = createReplyObject(task->type);
if (r == NULL)
return NULL;
@@ -171,6 +177,7 @@ static void *createArrayObject(const redisReadTask *task, size_t elements) {
parent->type == REDIS_REPLY_SET);
parent->element[task->idx] = r;
}
+
return r;
} |
alloc.h
Outdated
#define hi_calloc calloc | ||
#define hi_realloc realloc | ||
#define hi_strdup strdup | ||
#define hi_free free |
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.
@michael-grunder What about pluggable heap functions? Do you prefer to have that in a separate PR?
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.
Sure, that works for me. It would make it much simpler for end-users to plug in their own functions too.
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.
Updated the logic to use a single pass-through struct with function pointers and just access the appropriate member depending on what kind of allocation it is.
I went with static inline functions here because it allowed me to have the allocation functions configurable without paying a performance price. Using non-inline functions defined in alloc.c
cost between 8-15% on my machine for some workloads.
@@ -328,6 +328,22 @@ int redisContextSetTimeout(redisContext *c, const struct timeval tv) { | |||
return REDIS_OK; | |||
} | |||
|
|||
static int _redisContextUpdateTimeout(redisContext *c, const struct timeval *timeout) { | |||
/* Same timeval struct, short circuit */ | |||
if (c->timeout == timeout) |
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.
@michael-grunder You probably meant to compare the actual value and not the pointer here.
@@ -352,20 +368,18 @@ static int _redisContextConnectTcp(redisContext *c, const char *addr, int port, | |||
* This is a bit ugly, but atleast it works and doesn't leak memory. | |||
**/ | |||
if (c->tcp.host != addr) { |
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.
Looks like another case of comparing the pointer rather than the value.
@@ -375,10 +389,10 @@ static int _redisContextConnectTcp(redisContext *c, const char *addr, int port, | |||
} | |||
|
|||
if (source_addr == NULL) { | |||
free(c->tcp.source_addr); | |||
hi_free(c->tcp.source_addr); | |||
c->tcp.source_addr = NULL; | |||
} else if (c->tcp.source_addr != source_addr) { |
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.
Same as above.
@michael-grunder Another thing, regarding I think it's the kind of things that will need to be changed once in a million, but if that happens it's much easier to just do some runtime configuration rather than full library rebuild. |
Thanks for going through the code!
I'm happy to add this as configurable similar to our reader The pointer comparisons were intentional but this is what we're doing now. I think it was initially done this way to handle reconnections. AFAIK it's safe to treat two pointers equal if they point to the same address and are of the same type. Do you mind clarifying what your concern is? It's possible I'm totally missing your point 😄 For reference, this logic has been in place for years (example, example).
|
@michael-grunder Yes I see the reconnect now! So this obviously works for reconnects, but if a user calls this function with the same value (but not same pointer of course) it will get reallocated. Anyway, I guess that's not related to this PR. |
Rework allocator injection logic use non-inline pass-thru functions on Windows, so the feature is at least available.
Just some small changes that makes the new logic conform better to existing hiredis naming and style conventions.
Hi @yossigo, would you like to see any changes in this PR? |
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.
@michael-grunder Looks good to me! Just one small comment to consider above.
This PR replaces every allocation and deallocation with
hi_
variants that are defined in alloc.hAdditionally, it attempts to gracefully handle OOM everywhere.
To test the new logic, I created "fail allocators" which could be configured to fail after
N
allocations or randomly.Then I ran our tests under Valgrind while incrementing this counter until I detected a Valgrind error (leak, invalid read/write, etc) or all the tests passed. We don't have async unit tests so I created some and repeated the process.
A couple points of interest:
redisReadTask->elements
was defined as anint
butredisReply->elements
was defined as asize_t
, which could overflow (3e8835e).Cheers!