{-# LANGUAGE OverloadedStrings #-} module Tokstyle.Linter.MemoryLifetimeSpec (spec) where import Test.Hspec (Spec, it) import Tokstyle.LinterSpec (shouldAccept, shouldWarn, testC) spec :: Spec spec = do it "warns about a struct freed while a field it was given memory for still holds it" $ do shouldWarn ["memory-lifetime"] (testC [ "Obj *obj_new(void) {" , " Err rc = ERR_OK;" , " Obj *obj = (Obj *)calloc(1, sizeof(Obj));" , " if (obj == nullptr) {" , " return nullptr;" , " }" , " obj->lock = (Lock *)malloc(sizeof(Lock));" , " if (obj->lock == nullptr) {" , " rc = ERR_MALLOC;" , " goto RETURN;" , " }" , " obj->log = logger_new();" , " if (obj->log == nullptr) {" , " rc = ERR_MALLOC;" , " goto RETURN;" , " }" , "RETURN:" , " if (rc != ERR_OK) {" , " free(obj);" , " return nullptr;" , " }" , " return obj;" , "}" ]) [[ "error: `obj` is freed while `obj->lock` may still hold memory [-Wmemory-lifetime]" , " --> test.c:19:5" , " |" , " 7| obj->lock = (Lock *)malloc(sizeof(Lock));" , " | ^^^^^^" , " | |" , " | memory stored here" , " 19| free(obj);" , " | ^^^^" , " | |" , " | freed here" ]] it "accepts a struct freed by a function that frees its fields too" $ do shouldAccept ["memory-lifetime"] (testC [ "static void obj_free(Obj *obj) {" , " free(obj->lock);" , " free(obj);" , "}" , "Obj *obj_new(void) {" , " Obj *obj = (Obj *)calloc(1, sizeof(Obj));" , " if (obj == nullptr) {" , " return nullptr;" , " }" , " obj->lock = (Lock *)malloc(sizeof(Lock));" , " if (!init(obj)) {" , " obj_free(obj);" , " return nullptr;" , " }" , " return obj;" , "}" ]) it "accepts a field's memory handed on before its struct is freed" $ do shouldAccept ["memory-lifetime"] (testC [ "void hand_over(Obj *obj, Holder *holder) {" , " obj->buf = (uint8_t *)malloc(4);" , " holder->buf = obj->buf;" , " free(obj);" , "}" ]) it "warns about a return that skips the free the other paths do" $ do shouldWarn ["memory-lifetime"] (testC [ "int handle(Chat *chat, Conn *gconn) {" , " uint8_t *payload = nullptr;" , " const uint16_t len = reassemble(gconn, &payload);" , " if (len == 0) {" , " free(payload);" , " return -1;" , " }" , " if (!handle_helper(chat, payload + 1, len - 1)) {" , " free(payload);" , " return -1;" , " }" , " gconn = get_conn(chat);" , " if (gconn == nullptr) {" , " return 0;" , " }" , " set_id(gconn);" , " free(payload);" , " return 0;" , "}" ]) [[ "error: `payload` may still hold memory when the function returns [-Wmemory-lifetime]" , " --> test.c:3:24" , " |" , " 3| const uint16_t len = reassemble(gconn, &payload);" , " | ^^^^^^^^^^" , " | |" , " | memory from here" , " 14| return 0;" , " | ^" , " | |" , " | returns here without freeing it" ]] it "warns about an early return after an allocation" $ do shouldWarn ["memory-lifetime"] (testC [ "int send(Conn *c, uint16_t size) {" , " uint8_t *buf = (uint8_t *)malloc(size);" , " if (buf == nullptr) {" , " return -1;" , " }" , " if (!c->ready) {" , " return -1;" , " }" , " write_packet(c, buf, size);" , " free(buf);" , " return 0;" , "}" ]) [[ "error: `buf` may still hold memory when the function returns [-Wmemory-lifetime]" , " --> test.c:2:29" , " |" , " 2| uint8_t *buf = (uint8_t *)malloc(size);" , " | ^^^^^^" , " | |" , " | memory from here" , " 7| return -1;" , " | ^" , " | |" , " | returns here without freeing it" ]] it "accepts memory handed on before a later failure undoes it" $ do shouldAccept ["memory-lifetime"] (testC [ "int enable(Chats *g_c, uint32_t n) {" , " Group_AV *group_av = (Group_AV *)calloc(1, sizeof(Group_AV));" , " if (group_av == nullptr) {" , " return -1;" , " }" , " if (group_set_object(g_c, n, group_av) == -1) {" , " free(group_av);" , " return -1;" , " }" , " if (count(g_c) < 0) {" , " free(group_av);" , " return -1;" , " }" , " return 0;" , "}" ]) it "accepts a call that filled nothing when its stored result says it failed" $ do shouldAccept ["memory-lifetime"] (testC [ "static int32_t resolve(Tox *tox, IP_Port **root) {" , " *root = (IP_Port *)malloc(sizeof(IP_Port));" , " if (*root == nullptr) {" , " return -1;" , " }" , " return 1;" , "}" , "bool bootstrap(Tox *tox) {" , " IP_Port *root;" , " const int32_t count = resolve(tox, &root);" , " if (count == -1) {" , " return false;" , " }" , " use(tox, root, count);" , " free(root);" , " return true;" , "}" ]) it "accepts a return where a test with && shows the local is null" $ do shouldAccept ["memory-lifetime"] (testC [ "int set_password(Chat *chat) {" , " uint8_t *old = memdup(chat->password, chat->password_length);" , " if (old == nullptr && chat->password_length > 0) {" , " return -4;" , " }" , " free(old);" , " return 0;" , "}" , "uint8_t *memdup(const uint8_t *data, size_t size) {" , " if (size == 0) {" , " return nullptr;" , " }" , " uint8_t *copy = (uint8_t *)malloc(size);" , " return copy;" , "}" ]) it "accepts memory from a function that also keeps it in a list" $ do shouldAccept ["memory-lifetime"] (testC [ "static Entry *entry_new(List *list) {" , " Entry *entry = (Entry *)calloc(1, sizeof(Entry));" , " if (entry == nullptr) {" , " return nullptr;" , " }" , " list->root = entry;" , " return entry;" , "}" , "int add(List *list) {" , " Entry *entry = entry_new(list);" , " if (entry == nullptr) {" , " return -1;" , " }" , " entry->count = 1;" , " return 0;" , "}" ]) it "accepts memory still held where the program stops" $ do shouldAccept ["memory-lifetime"] (testC [ "bool ping(Chat *chat, uint16_t size) {" , " uint8_t *data = (uint8_t *)malloc(size);" , " if (data == nullptr) {" , " return false;" , " }" , " if (pack(data, size) != size) {" , " LOGGER_FATAL(chat->log, \"impossible\");" , " }" , " free(data);" , " return true;" , "}" ]) it "warns about freeing what the call it came from also stored" $ do shouldWarn ["memory-lifetime"] (testC [ "bool events_add(Events *events, const Entry *entry) {" , " if (events->size == events->capacity) {" , " return false;" , " }" , " events->entries[events->size] = *entry;" , " ++events->size;" , " return true;" , "}" , "static Event *event_add(Events *events) {" , " Event *ev = (Event *)calloc(1, sizeof(Event));" , " if (ev == nullptr) {" , " return nullptr;" , " }" , " Entry entry;" , " entry.ev = ev;" , " if (!events_add(events, &entry)) {" , " free(ev);" , " return nullptr;" , " }" , " return ev;" , "}" , "static void event_free(Event *ev) {" , " free(ev);" , "}" , "void handle(Events *events, const uint8_t *data, size_t length) {" , " Event *ev = event_add(events);" , " if (ev == nullptr) {" , " return;" , " }" , " if (!event_set_data(ev, data, length)) {" , " event_free(ev);" , " return;" , " }" , "}" ]) [[ "error: `ev` is freed while `event_add` may still have it stored [-Wmemory-lifetime]" , " --> test.c:31:5" , " |" , " 26| Event *ev = event_add(events);" , " | ^^^^^^^^^" , " | |" , " | stored by this call" , " 31| event_free(ev);" , " | ^^^^^^^^^^" , " | |" , " | freed here" ]] it "accepts freeing with a function that also removes it from where it is kept" $ do shouldAccept ["memory-lifetime"] (testC [ "static Call *call_new(Session *session, uint32_t n) {" , " Call *call = (Call *)calloc(1, sizeof(Call));" , " if (call == nullptr) {" , " return nullptr;" , " }" , " session->calls[n] = call;" , " return call;" , "}" , "static void call_kill(Session *session, Call *call) {" , " session->calls[call->n] = nullptr;" , " free(call);" , "}" , "void handle(Session *session, uint32_t n) {" , " Call *call = call_new(session, n);" , " if (call == nullptr) {" , " return;" , " }" , " if (!call_start(call)) {" , " call_kill(session, call);" , " }" , "}" ]) it "warns about freeing what a call stored when it succeeded" $ do shouldWarn ["memory-lifetime"] (testC [ "bool list_add(List *list, Item *item) {" , " if (list->size == list->capacity) {" , " return false;" , " }" , " list->items[list->size] = item;" , " ++list->size;" , " return true;" , "}" , "int add_item(List *list) {" , " Item *item = (Item *)calloc(1, sizeof(Item));" , " if (item == nullptr) {" , " return -1;" , " }" , " if (!list_add(list, item)) {" , " free(item);" , " return -1;" , " }" , " if (!item_init(item)) {" , " free(item);" , " return -1;" , " }" , " return 0;" , "}" ]) [[ "error: `item` is freed while `list_add` may still have it stored [-Wmemory-lifetime]" , " --> test.c:19:5" , " |" , " 14| if (!list_add(list, item)) {" , " | ^^^^^^^^" , " | |" , " | stored by this call" , " 19| free(item);" , " | ^^^^" , " | |" , " | freed here" ]] it "accepts freeing what was stored only in something it owns" $ do shouldAccept ["memory-lifetime"] (testC [ "void logger_callback(Logger *log, void *context) {" , " log->context = context;" , "}" , "Tox *tox_new(void) {" , " Tox *tox = (Tox *)calloc(1, sizeof(Tox));" , " if (tox == nullptr) {" , " return nullptr;" , " }" , " logger_callback(tox->log, tox);" , " if (!start(tox)) {" , " free(tox);" , " return nullptr;" , " }" , " return tox;" , "}" ]) it "accepts freeing a buffer a call only wrote into through a local struct" $ do shouldAccept ["memory-lifetime"] (testC [ "void packer_init(Packer *p, uint8_t *buf, uint32_t size) {" , " p->buf = buf;" , " p->size = size;" , "}" , "bool pack(const Obj *obj, uint8_t *buf, uint32_t size) {" , " Packer p;" , " packer_init(&p, buf, size);" , " return packer_write(&p, obj);" , "}" , "bool send_obj(Conn *c, const Obj *obj) {" , " uint8_t *buf = (uint8_t *)malloc(64);" , " if (buf == nullptr) {" , " return false;" , " }" , " pack(obj, buf, 64);" , " send(c, buf, 64);" , " free(buf);" , " return true;" , "}" ])