/* vim: set expandtab ts=4 sw=4: */ /* * You may redistribute this program and/or modify it under the terms of * the GNU General Public License as published by the Free Software Foundation, * either version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #define string_strrchr #include "util/platform/libc/string.h" #include "memory/Allocator.h" #include "memory/MallocAllocator.h" #include "memory/MallocAllocator_pvt.h" #include "util/Bits.h" #include "util/log/Log.h" #include #include #include #include /** This provides the padding for each line based on the depth in the stack. */ struct Unroller; struct Unroller { const char* const content; const struct Unroller* const last; }; static void writeUnroller(const struct Unroller* unroller) { if (unroller) { writeUnroller(unroller->last); fprintf(stderr, "%s", unroller->content); } } static void unroll(struct MallocAllocator_pvt* context, struct Unroller* unroller) { writeUnroller(unroller); const char* ident = (context->identFile) ? strrchr(context->identFile, '/') : "UNKNOWN"; ident = ident ? ident : context->identFile; fprintf(stderr, "%s:%d [%lu] bytes%s\n", ident, context->identLine, context->allocatedHere, (context->freeing) ? " (freeing)" : ""); if (context->firstChild) { unroll(context->firstChild, &(struct Unroller) { .content = ((context->nextSibling) ? "| " : " "), .last = unroller }); } if (context->nextSibling) { unroll(context->nextSibling, unroller); } } Gcc_NORETURN static void failure(struct MallocAllocator_pvt* context, const char* message, const char* identFile, int identLine) { // get the root allocator. struct MallocAllocator_pvt* rootAlloc = context; while (rootAlloc->lastSibling && rootAlloc->lastSibling != rootAlloc) { rootAlloc = rootAlloc->lastSibling; } // can't use this allocator because it failed. unroll(rootAlloc, NULL); Assert_failure("%s:%d Fatal error: [%s] totalBytes [%lu] remaining [%lu]", identFile, identLine, message, (unsigned long)context->rootAlloc->maxSpace, (unsigned long)context->rootAlloc->spaceAvailable); } static inline unsigned long getRealSize(unsigned long requestedSize) { return ((requestedSize + (sizeof(char*) - 1)) & ~(sizeof(char*) - 1)) // align + sizeof(struct MallocAllocator_Allocation) #ifdef MallocAllocator_USE_CANARIES + sizeof(long) #endif ; } #define END_CANARY(alloc) ((long*) alloc)[ (alloc->size / sizeof(long)) - 1 ] static inline void setCanaries(struct MallocAllocator_Allocation* alloc, struct MallocAllocator_pvt* context) { #ifdef MallocAllocator_USE_CANARIES alloc->beginCanary = context->canary; END_CANARY(alloc) = alloc->beginCanary; #endif } static inline void checkCanaries(struct MallocAllocator_Allocation* alloc, struct MallocAllocator_pvt* context) { #ifdef MallocAllocator_USE_CANARIES char* canary; if (alloc->beginCanary != context->canary) { canary = "begin"; } else if (END_CANARY(alloc) != alloc->beginCanary) { canary = "end"; } else { return; } Assert_failure("%s:%d Fatal error: invalid [%s] canary", context->identFile, context->identLine, canary); #endif } static inline void* newAllocation(struct MallocAllocator_pvt* context, unsigned long size, const char* identFile, int identLine) { int64_t realSize = getRealSize(size); if (context->rootAlloc->spaceAvailable <= realSize) { failure(context, "Out of memory, limit exceeded", identFile, identLine); } context->rootAlloc->spaceAvailable -= realSize; context->allocatedHere += realSize; struct MallocAllocator_Allocation* alloc = malloc(realSize); if (alloc == NULL) { failure(context, "Out of memory, malloc() returned NULL", identFile, identLine); } alloc->next = context->allocations; alloc->size = realSize; context->allocations = alloc; setCanaries(alloc, context); return (void*) (alloc + 1); } static int removeJob(struct MallocAllocator_OnFreeJob* job) { struct MallocAllocator_pvt* context = Identity_cast(job->alloc); struct MallocAllocator_OnFreeJob* j = context->onFree; struct MallocAllocator_OnFreeJob** jP = &context->onFree; while (j && j != job) { jP = &j->next; j = j->next; } if (j == job) { *jP = j->next; return 0; } else { return -1; failure(context, "OnFreeJob->complete() called multiple times", job->file, job->line); } } static void releaseMemory(struct MallocAllocator_pvt* context) { // Free all of the allocations including the one which holds the allocator. #ifdef PARANOIA unsigned long allocatedHere = context->allocatedHere; #endif context->rootAlloc->spaceAvailable += context->allocatedHere; struct MallocAllocator_Allocation* loc = context->allocations; while (loc != NULL) { #ifdef PARANOIA allocatedHere -= loc->size; #endif struct MallocAllocator_Allocation* nextLoc = loc->next; checkCanaries(loc, context); // TODO: make this optional. Bits_memset(loc, 0xee, loc->size); free(loc); loc = nextLoc; } #ifdef PARANOIA Assert_always(allocatedHere == 0); #endif } /** * Change the root allocator for a given subtree. * @param alloc an allocator. */ static void changeRoot(struct MallocAllocator_pvt* alloc, struct MallocAllocator_FirstCtx* root, struct MallocAllocator_pvt* first, const char* file, int line) { Assert_true(first != alloc); if (!first) { first = alloc; } if (alloc->rootAlloc != NULL) { alloc->rootAlloc->spaceAvailable += alloc->allocatedHere; } if (root != NULL) { if (root->spaceAvailable < (int64_t)alloc->allocatedHere) { failure(alloc, "Out of memory, limit exceeded", file, line); } root->spaceAvailable -= alloc->allocatedHere; } alloc->rootAlloc = root; struct MallocAllocator_pvt* child = alloc->firstChild; while (child) { struct MallocAllocator_pvt* nextChild = child->nextSibling; changeRoot(child, root, first, file, line); child = nextChild; } } // disconnect an allocator from it's parent. static void disconnect(struct MallocAllocator_pvt* context) { // Remove this allocator from the sibling list. Assert_true(context->lastSibling); if (context->lastSibling->nextSibling == context) { context->lastSibling->nextSibling = context->nextSibling; } else if (context->lastSibling->firstChild == context) { context->lastSibling->firstChild = context->nextSibling; } else if (context->lastSibling == context) { // root alloc Assert_true(!context->nextSibling); } if (context->nextSibling) { context->nextSibling->lastSibling = context->lastSibling; context->nextSibling = NULL; } context->lastSibling = context; } // connect an allocator to a new parent. static void connect(struct MallocAllocator_pvt* parent, struct MallocAllocator_pvt* child, const char* file, int line) { Assert_true(child->lastSibling == child); Assert_true(child->nextSibling == NULL); child->nextSibling = parent->firstChild; parent->firstChild = child; child->lastSibling = parent; changeRoot(child, parent->rootAlloc, NULL, file, line); } static struct MallocAllocator_pvt* getParent(struct MallocAllocator_pvt* child) { struct MallocAllocator_pvt* ls = child->lastSibling; while (ls) { if (ls->firstChild == child) { return ls; } if (ls == ls->lastSibling) { // root alloc return NULL; } child = ls; ls = ls->lastSibling; } Assert_true(0); } static void freeAllocator(struct MallocAllocator_pvt* context, const char* file, int line); static void childFreed(struct MallocAllocator_pvt* child) { struct MallocAllocator_pvt* parent = getParent(child); // disconnect the child and if there are no children left then call freeAllocator() // on the parent a second time. disconnect(child); if (parent && !parent->firstChild && parent->freeing) { freeAllocator(parent, child->identFile, child->identLine); } } static int onFreeComplete(struct Allocator_OnFreeJob* onFreeJob) { struct MallocAllocator_OnFreeJob* job = (struct MallocAllocator_OnFreeJob*) onFreeJob; struct MallocAllocator_pvt* context = Identity_cast(job->alloc); if (removeJob(job)) { failure(context, "OnFreeJob->complete() called multiple times", job->file, job->line); } if (!context->onFree) { //childFreed(context); // There are no more jobs, release the memory. freeAllocator(context, context->identFile, context->identLine); } return 0; } static void disconnectAdopted(struct MallocAllocator_pvt* parent, struct MallocAllocator_pvt* child) { Assert_true(parent->adoptions); Assert_true(parent->adoptions->children); struct MallocAllocator_List** cpp = &parent->adoptions->children; struct MallocAllocator_List* cp; int found = 0; while ((cp = *cpp)) { if (cp->alloc == child) { *cpp = cp->next; found = 1; break; } cpp = &cp->next; } Assert_true(found); Assert_true(child->adoptions); Assert_true(child->adoptions->parents); cpp = &child->adoptions->parents; found = 0; while ((cp = *cpp)) { if (cp->alloc == parent) { *cpp = cp->next; found = 1; break; } cpp = &cp->next; } Assert_true(found); } /** * Triggered when freeAllocator() is called and the allocator nolonger * has any remaining links to the allocator tree. */ static void freeAllocator(struct MallocAllocator_pvt* context, const char* file, int line) { // When the last child calls us back via childFreed() we will be called the last time and // if this is not set, the child will be disconnected from us and we will be left. context->freeing = 1; if (context->adoptions && context->adoptions->parents) { disconnect(context); connect(context->adoptions->parents->alloc, context, file, line); disconnectAdopted(context->adoptions->parents->alloc, context); return; } // from now on, identFile/line will point to the place of freeing. // this allows childFreed() to tell the truth when calling us back. context->identFile = file; context->identLine = line; // Disconnect adopted children. struct MallocAllocator_List* childL = context->adoptions ? context->adoptions->children : NULL; while (childL) { disconnectAdopted(context, childL->alloc); childL = childL->next; } // Do the onFree jobs. struct MallocAllocator_OnFreeJob** jobP = &context->onFree; while (*jobP != NULL) { struct MallocAllocator_OnFreeJob* job = *jobP; if (!job->generic.callback) { // no callback, remove the job Assert_true(!removeJob(job)); continue; } else if (!job->done) { if (job->generic.callback(&job->generic) != Allocator_ONFREE_ASYNC) { Assert_true(!removeJob(job)); continue; } // asynchronously completing, don't bother it again. job->done = true; } jobP = &job->next; } if (context->onFree) { // onFreeComplete() will call us back. return; } // Free children struct MallocAllocator_pvt* child = context->firstChild; if (child) { while (child) { struct MallocAllocator_pvt* nextChild = child->nextSibling; freeAllocator(child, file, line); child = nextChild; } // childFreed() will call us back. return; } childFreed(context); releaseMemory(context); } /** * Disconnect an allocator from it's parent and free it if there are no more links to the tree. */ static void disconnectAllocator(struct Allocator* allocator, const char* file, int line) { struct MallocAllocator_pvt* context = Identity_cast((struct MallocAllocator_pvt*) allocator); freeAllocator(context, file, line); } /** @see Allocator->malloc() */ static void* allocatorMalloc(unsigned long length, struct Allocator* allocator, const char* identFile, int identLine) { struct MallocAllocator_pvt* ctx = Identity_cast((struct MallocAllocator_pvt*) allocator); return newAllocation(ctx, length, identFile, identLine); } /** @see Allocator->calloc() */ static void* allocatorCalloc(unsigned long length, unsigned long count, struct Allocator* allocator, const char* identFile, int identLine) { void* pointer = allocatorMalloc(length * count, allocator, identFile, identLine); Bits_memset(pointer, 0, length * count); return pointer; } /** @see Allocator->clone() */ static void* allocatorClone(unsigned long length, struct Allocator* allocator, const void* toClone, const char* identFile, int identLine) { void* pointer = allocatorMalloc(length, allocator, identFile, identLine); Bits_memcpy(pointer, toClone, length); return pointer; } /** @see Allocator->realloc() */ static void* allocatorRealloc(const void* original, unsigned long size, struct Allocator* allocator, const char* identFile, int identLine) { if (original == NULL) { return allocatorMalloc(size, allocator, identFile, identLine); } struct MallocAllocator_pvt* context = Identity_cast((struct MallocAllocator_pvt*) allocator); struct MallocAllocator_Allocation** locPtr = &context->allocations; struct MallocAllocator_Allocation* origLoc = ((struct MallocAllocator_Allocation*) original) - 1; for (;;) { struct MallocAllocator_Allocation* loc = *locPtr; if (loc == NULL) { failure(context, "Reallocation of memory which was not allocated using this allocator.", identFile, identLine); } checkCanaries(loc, context); if (loc == origLoc) { break; } locPtr = &loc->next; } struct MallocAllocator_Allocation* nextLoc = origLoc->next; size_t realSize = getRealSize(size); if (context->rootAlloc->spaceAvailable + origLoc->size < realSize) { failure(context, "Out of memory, limit exceeded.", identFile, identLine); } context->rootAlloc->spaceAvailable += origLoc->size; context->rootAlloc->spaceAvailable -= realSize; context->allocatedHere -= origLoc->size; context->allocatedHere += realSize; struct MallocAllocator_Allocation* alloc = realloc(origLoc, realSize); if (alloc == NULL) { failure(context, "Out of memory, realloc() returned NULL.", identFile, identLine); } alloc->next = nextLoc; alloc->size = realSize; *locPtr = alloc; setCanaries(alloc, context); return (void*) (alloc + 1); } /** @see Allocator_child() */ static struct Allocator* childAllocator(struct Allocator* allocator, const char* file, int line) { struct MallocAllocator_pvt* parent = Identity_cast((struct MallocAllocator_pvt*) allocator); struct MallocAllocator_pvt* child = newAllocation(parent, sizeof(struct MallocAllocator_pvt), file, line); Bits_memset(child, 0, sizeof(struct MallocAllocator_pvt)); Bits_memcpyConst(&child->pub, &parent->pub, sizeof(struct Allocator)); Identity_set(child); // Add the child to it's own allocation list. child->allocations = parent->allocations; // Remove the child from the parent's allocation list. parent->allocations = parent->allocations->next; parent->allocatedHere -= getRealSize(sizeof(struct MallocAllocator_pvt)); child->allocatedHere += getRealSize(sizeof(struct MallocAllocator_pvt)); // Drop the rest of the linked list from the child's allocation child->allocations->next = NULL; // Link the child into the parent's allocator list child->lastSibling = parent; child->nextSibling = parent->firstChild; if (parent->firstChild != NULL) { parent->firstChild->lastSibling = child; } child->rootAlloc = parent->rootAlloc; child->identFile = file; child->identLine = line; child->nextCanary = child->canary = parent->nextCanary; parent->firstChild = child; child->rootAlloc = parent->rootAlloc; // Now set the child to use it's own canaries so it will free properly. setCanaries(child->allocations, child); return &child->pub; } static int removeOnFreeJob(struct Allocator_OnFreeJob* toRemove) { struct MallocAllocator_OnFreeJob* job = (struct MallocAllocator_OnFreeJob*) toRemove; struct MallocAllocator_pvt* context = Identity_cast(job->alloc); struct MallocAllocator_OnFreeJob** jobPtr = &(context->onFree); while (*jobPtr != NULL) { if (*jobPtr == job) { *jobPtr = (*jobPtr)->next; return 0; } jobPtr = &(*jobPtr)->next; } return -1; } static bool isAncestorOf(struct MallocAllocator_pvt* maybeParent, struct MallocAllocator_pvt* maybeChild) { if (maybeParent == maybeChild) { return true; } if (maybeParent == NULL || maybeChild == NULL) { return false; } if (isAncestorOf(maybeParent, getParent(maybeChild))) { return true; } if (maybeChild->adoptions) { struct MallocAllocator_List* al = maybeChild->adoptions->parents; while (al) { if (isAncestorOf(maybeParent, al->alloc)) { return true; } } } return false; } static void adopt(struct Allocator* adoptedParent, struct Allocator* childToAdopt, const char* file, int line) { struct MallocAllocator_pvt* parent = Identity_cast((struct MallocAllocator_pvt*) adoptedParent); struct MallocAllocator_pvt* child = Identity_cast((struct MallocAllocator_pvt*) childToAdopt); if (isAncestorOf(child, parent)) { // The child is a parent of the parent, this means an adoption would be meaningless // because if the child is otherwise freed, it will take the parent along with it. return; } if (!parent->adoptions) { parent->adoptions = allocatorCalloc(sizeof(struct MallocAllocator_Adoptions), 1, adoptedParent, file, line); } if (!child->adoptions) { child->adoptions = allocatorCalloc(sizeof(struct MallocAllocator_Adoptions), 1, childToAdopt, file, line); } struct MallocAllocator_List* pl = allocatorCalloc(sizeof(struct MallocAllocator_List), 1, adoptedParent, file, line); pl->alloc = child; pl->next = parent->adoptions->children; parent->adoptions->children = pl; struct MallocAllocator_List* cl = allocatorCalloc(sizeof(struct MallocAllocator_List), 1, childToAdopt, file, line); cl->alloc = parent; cl->next = child->adoptions->parents; child->adoptions->parents = cl; } /** @see Allocator_onFree() */ static struct Allocator_OnFreeJob* addOnFreeJob(struct Allocator* allocator, const char* file, int line) { struct MallocAllocator_pvt* context = Identity_cast((struct MallocAllocator_pvt*) allocator); struct MallocAllocator_OnFreeJob* newJob = Allocator_clone(allocator, (&(struct MallocAllocator_OnFreeJob) { .generic = { .cancel = removeOnFreeJob, .complete = onFreeComplete }, .alloc = context, .file = file, .line = line })); Identity_set(&newJob->generic); struct MallocAllocator_OnFreeJob* job = context->onFree; if (job == NULL) { context->onFree = newJob; } else { while (job->next != NULL) { job = job->next; } job->next = newJob; } return &newJob->generic; } /** @see MallocAllocator.h */ struct Allocator* MallocAllocator_newWithIdentity(unsigned long sizeLimit, const char* identFile, int identLine) { // Add in the size of the allocator so that a very small sizeLimit is sane. sizeLimit += getRealSize(sizeof(struct MallocAllocator_FirstCtx)); struct MallocAllocator_FirstCtx stackContext = { .spaceAvailable = (sizeLimit == 0) ? SIZE_MAX : sizeLimit, .context = { .identFile = identFile, .identLine = identLine, .canary = (long) 0x09F911029D74E35Bll, .nextCanary = (long) 0xD84156C5635688C0ll, } }; stackContext.maxSpace = stackContext.spaceAvailable; stackContext.context.rootAlloc = &stackContext; Identity_set(&stackContext.context); struct MallocAllocator_FirstCtx* firstContext = allocatorClone(sizeof(struct MallocAllocator_FirstCtx), &stackContext.context.pub, &stackContext, identFile, identLine); struct MallocAllocator_pvt* context = &firstContext->context; context->rootAlloc = firstContext; struct Allocator allocator = { .free = disconnectAllocator, .malloc = allocatorMalloc, .calloc = allocatorCalloc, .clone = allocatorClone, .realloc = allocatorRealloc, .child = childAllocator, .onFree = addOnFreeJob, .adopt = adopt }; Bits_memcpyConst(&context->pub, &allocator, sizeof(struct Allocator)); context->lastSibling = context; Identity_set(context); return &context->pub; } unsigned long MallocAllocator_bytesAllocated(struct Allocator* allocator) { struct MallocAllocator_pvt* context = Identity_cast((struct MallocAllocator_pvt*) allocator); return context->rootAlloc->maxSpace - context->rootAlloc->spaceAvailable; } void MallocAllocator_setCanary(struct Allocator* alloc, long value) { #ifdef MallocAllocator_USE_CANARIES struct MallocAllocator_pvt* context = Identity_cast((struct MallocAllocator_pvt*) alloc); context->nextCanary ^= value; #endif }