Skip to content
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

Merged
merged 22 commits into from
May 22, 2020

Conversation

michael-grunder
Copy link
Collaborator

@michael-grunder michael-grunder commented May 7, 2020

This PR replaces every allocation and deallocation with hi_ variants that are defined in alloc.h

Additionally, 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:

  • When fuzz testing hiredis I found a bug where redisReadTask->elements was defined as an int but redisReply->elements was defined as a size_t, which could overflow (3e8835e).
  • I fixed a Unix socket reconnection leak
  • I made a small change to ssl.c to avoid a potential memory leak (8fe3dd7)

Cheers!

* 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.
@michael-grunder
Copy link
Collaborator Author

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:

*35184372088832\r\n

To redisReader. This isn't invalid and fits in a 64 bit integer, but it's an absurd count that ends up blocking hiredis for minutes. It seems unlikely we'd ever need to actually process that many elements.

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
Copy link
Member

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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)
Copy link
Member

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) {
Copy link
Member

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

@yossigo
Copy link
Member

yossigo commented May 11, 2020

@michael-grunder Another thing, regarding HIREDIS_MAX_ARRAY_ELEMENTS do you see any reason not to make this runtime configurable rather than a compile time define?

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.

@michael-grunder
Copy link
Collaborator Author

Thanks for going through the code!

@michael-grunder Another thing, regarding HIREDIS_MAX_ARRAY_ELEMENTS do you see any reason not to make this runtime configurable rather than a compile time define?

I'm happy to add this as configurable similar to our reader maxbuf.


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).


  1. I'll add HIREDIS_MAX_ARRAY_ELEMENTS as a configurable runtime setting, with a sane default.
  2. I'll switch to pluggable heap allocation functions that will default to malloc, calloc, free, etc.

@yossigo
Copy link
Member

yossigo commented May 12, 2020

@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.

@michael-grunder michael-grunder mentioned this pull request May 15, 2020
7 tasks
@michael-grunder
Copy link
Collaborator Author

Hi @yossigo, would you like to see any changes in this PR?

Copy link
Member

@yossigo yossigo left a 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants