diff --git a/leakmalloc/README b/leakmalloc/README new file mode 100644 index 0000000..e531460 --- /dev/null +++ b/leakmalloc/README @@ -0,0 +1,19 @@ +This is a very simple memory leak detector using libexecinfo (in +OpenBSD ports as devel/libexecinfo). It could be adapted to use the +backtrace function in glibc too. + +It tracks memory allocations by redirecting malloc/free and friends +to its own functions using preprocessor defines. When an allocation +is made, leakmalloc grabs the call chain from the stack and stores it. +When the program exits, leakmalloc dumps all unfreed allocations. + +leakresolve.py consumes one of these dumps and produces a fairly +readable report with leaks coalesced by stack trace and sorted by +number of leak instances. Usually the last entry on this report is +the one to care most about. + +leaky.c in regress/ shows how you might use it (run 'make' in that directory +and run it manually, or look at the Makefile for tips). + +Note that everything needs to be compiled without optimisation and with +debugging symbols. diff --git a/leakmalloc/leakmalloc/Makefile b/leakmalloc/leakmalloc/Makefile new file mode 100644 index 0000000..ad01bc2 --- /dev/null +++ b/leakmalloc/leakmalloc/Makefile @@ -0,0 +1,28 @@ +# $OpenBSD$ + +LIB= leakmalloc +SRCS= leakmalloc.c +HDRS= leakmalloc.h +#MAN= leakmalloc.3 +NOMAN=1 +NOPIC=1 +NOPROFILE=1 + +CDIAGFLAGS= -Wall +CDIAGFLAGS+= -Werror +CDIAGFLAGS+= -Wstrict-prototypes +CDIAGFLAGS+= -Wmissing-prototypes +CDIAGFLAGS+= -Wmissing-declarations +CDIAGFLAGS+= -Wshadow +CDIAGFLAGS+= -Wpointer-arith +CDIAGFLAGS+= -Wcast-qual +CDIAGFLAGS+= -Wsign-compare +CDIAGFLAGS+= -Wcast-align +CDIAGFLAGS+= -Wbad-function-cast + +CPPFLAGS+= -I/usr/local/include +DEBUG=-ggdb3 +COPTS=-O0 +INSTALL_STRIP= + +.include diff --git a/leakmalloc/leakmalloc/leakmalloc.c b/leakmalloc/leakmalloc/leakmalloc.c new file mode 100644 index 0000000..3e2979f --- /dev/null +++ b/leakmalloc/leakmalloc/leakmalloc.c @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2012 Damien Miller + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#define LEAKMALLOC_NO_REDIRECT +#include "leakmalloc.h" + +#define OPT_EXIT_ON_LEAKS 0x01 +#define OPT_DUMP_TO_FILE 0x02 +#define OPT_QUIET 0x04 + +u_int leakmalloc_options = 0; +static int initialised; +static FILE *dumpfile; + +struct alloc { + RB_ENTRY(alloc) entry; + void *addr; + size_t len; + void *bt[BT_MAX_DEPTH]; + int depth; +}; + +static int +alloc_cmp(struct alloc *a, struct alloc *b) +{ + if (a->addr == b->addr) + return 0; + if (a->addr > b->addr) + return 1; + else + return -1; +} + +RB_HEAD(alloc_tree, alloc); +RB_GENERATE_STATIC(alloc_tree, alloc, entry, alloc_cmp); +static struct alloc_tree alloc_tree = RB_INITIALIZER(&alloc_tree); + +static void +dump_leak(FILE *f, const char *tag, struct alloc *alloc) +{ + int i; + + fprintf(f, "%s %p %zu TRACE", tag, alloc->addr, alloc->len); + for (i = 1; i < alloc->depth; i++) + fprintf(f, " %p", alloc->bt[i]); + fprintf(f, "\n"); +} + +/* Called atexit to dump unfreed leak objects */ +static void +dump_leaks(void) +{ + struct alloc *alloc; + int i = 0; + + if (initialised != 1) + return; + RB_FOREACH(alloc, alloc_tree, &alloc_tree) { + if ((leakmalloc_options & OPT_QUIET) == 0) + dump_leak(dumpfile ? dumpfile : stdout, "LEAK", alloc); + i++; + } + if (dumpfile) + fclose(dumpfile); + if ((leakmalloc_options & OPT_EXIT_ON_LEAKS) != 0) + _exit(99); +} + +static void +internal_error(struct alloc *alloc) +{ + alloc->addr = NULL; + alloc->len = -1; + dump_leak(stderr, "ERROR", alloc); + initialised = -1; +} + +static struct alloc * +new_alloc(void *addr, size_t len) +{ + struct alloc *alloc; + + if ((alloc = calloc(1, sizeof(*alloc))) == NULL) + errx(1, "%s: calloc failed", __func__); + alloc->addr = addr; + alloc->len = len; + if ((alloc->depth = backtrace(alloc->bt, BT_MAX_DEPTH)) == -1) + errx(1, "%s: backtrace failed", __func__); + return alloc; +} + +static void +__record_leak(void *addr, size_t len, void *oaddr) +{ + struct alloc oalloc, *alloc; + char *cp; + + if (initialised == -1) + return; + else if (initialised == 0) { + atexit(dump_leaks); + if (!issetugid() && + (cp = getenv("LEAKMALLOC_OPTIONS")) != NULL) { + if (strchr(cp, 'X') != NULL) + leakmalloc_options |= OPT_EXIT_ON_LEAKS; + if (strchr(cp, 'D') != NULL) + leakmalloc_options |= OPT_DUMP_TO_FILE; + if (strchr(cp, 'Q') != NULL) + leakmalloc_options |= OPT_QUIET; + } + if ((leakmalloc_options & OPT_DUMP_TO_FILE) != 0 && + (dumpfile = fopen("leakmalloc.out", "w+")) == NULL) + err(1, "fopen(\"leakmalloc.out\")"); + initialised = 1; + } + + if (oaddr == NULL) { + /* + * malloc/calloc/realloc(NULL,...): allocate a leak object + * and fill in the trace. + */ + if (addr == NULL) + return; /* alloc failed or free(NULL) */ + alloc = new_alloc(addr, len); + if (RB_INSERT(alloc_tree, &alloc_tree, alloc) != NULL) { + internal_error(alloc); + errx(1, "%s: alloc for %p already exists", + __func__, addr); + } + } else { + oalloc.addr = oaddr; + alloc = RB_FIND(alloc_tree, &alloc_tree, &oalloc); + if (addr == NULL) { + /* + * free: delete the tracked leak. + */ + if (alloc == NULL) + return; /* Ignore untracked memory */ + RB_REMOVE(alloc_tree, &alloc_tree, alloc); + free(alloc); + } else { + /* + * realloc: update the original address so we can + * trace it when it is freed. + */ + if (alloc == NULL) { + alloc = new_alloc(NULL, -1); + internal_error(alloc); + errx(1, "%s: realloc original addr %p missing", + __func__, oaddr); + } + RB_REMOVE(alloc_tree, &alloc_tree, alloc); + alloc->addr = addr; + alloc->len = len; + if (RB_INSERT(alloc_tree, &alloc_tree, alloc) != NULL) { + internal_error(alloc); + errx(1, "%s: alloc for %p already exists", + __func__, addr); + } + } + } +} + +char * +leak_strdup(const char *s) +{ + char *ret = strdup(s); + + __record_leak(ret, ret == NULL ? 0 : strlen(ret), NULL); + return ret; +} + +void * +leak_malloc(size_t len) +{ + void *ret = malloc(len); + + __record_leak(ret, len, NULL); + return ret; +} + +void * +leak_calloc(size_t nmemb, size_t size) +{ + void *ret = calloc(nmemb, size); + + __record_leak(ret, nmemb * size, NULL); + return ret; +} + +void * +leak_realloc(void *s, size_t len) +{ + void *ret = realloc(s, len); + + __record_leak(ret, len, s); + return ret; +} + +void +leak_free(void *s) +{ + free(s); + __record_leak(NULL, 0, s); +} + diff --git a/leakmalloc/leakmalloc/leakmalloc.h b/leakmalloc/leakmalloc/leakmalloc.h new file mode 100644 index 0000000..8134c0b --- /dev/null +++ b/leakmalloc/leakmalloc/leakmalloc.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012 Damien Miller + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef _LEAKMALLOC_H +#define _LEAKMALLOC_H + +#include +#include +#include + +char *leak_strdup(const char *s); +void *leak_malloc(size_t len); +void *leak_calloc(size_t nmemb, size_t size); +void *leak_realloc(void *s, size_t len); +void leak_free(void *s); + +#ifndef LEAKMALLOC_NO_REDIRECT +#define malloc leak_malloc +#define strdup leak_strdup +#define calloc leak_calloc +#define realloc leak_realloc +#define free leak_free +#endif /* LEAKMALLOC_NO_REDIRECT */ + +#endif /* _LEAKMALLOC_H */ + diff --git a/leakmalloc/leakresolve.py b/leakmalloc/leakresolve.py new file mode 100755 index 0000000..e219d08 --- /dev/null +++ b/leakmalloc/leakresolve.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python + +# Copyright (c) 2012 Damien Miller +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +# Resolve leak dump to backtraces + +import sys +import getopt +import subprocess + +TRIM_TRACE=True + +def usage(): + print >> sys.stderr, "leakresolve.py -p executable < trace" + sys.exit(1); + +class LineResolver: + """Resolves addresses to source lines""" + def __init__(self, executable): + self.resolver = subprocess.Popen( + ["addr2line", "-e", executable, '-C', '-f'], + bufsize=1, # line-buffered + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + self.cache = {} + def resolve(self, addr): + if addr not in self.cache: + self.resolver.stdin.write(addr + "\n") + func = self.resolver.stdout.readline() + loc = self.resolver.stdout.readline() + result = "%s: in %s()" % (loc.strip(), func.strip()) + self.cache[addr] = result + return self.cache[addr] + +class Leak: + """Represents a memory leak site""" + def __init__(self, backtrace, resolver): + self.backtrace = backtrace; + self.resolver = resolver + self.nleaks = 0 + self.nbytes = 0; + def leak(self, nbytes): + self.nbytes += nbytes + self.nleaks += 1 + def __str__(self): + s = "Leaked %d objects totalling %d bytes\n" % \ + (self.nleaks, self.nbytes) + s += "\n".join(map(self.resolver.resolve, self.backtrace)) + return s + +class LeakTracker: + """Tracks all memory leaks""" + def __init__(self, executable): + self.resolver = LineResolver(executable) + self.leaks = {} + def addleak(self, nbytes, trace): + if TRIM_TRACE: + trace = trace[:-1] + trace = tuple(trace) + if trace not in self.leaks: + leak = Leak(trace, self.resolver) + self.leaks[trace] = leak + self.leaks[trace].leak(nbytes) + def _leakcmp(self, a, b): + r = cmp(self.leaks[a].nleaks, self.leaks[b].nleaks) + if r: + return r + return cmp(self.leaks[a].nbytes, self.leaks[b].nbytes) + def __str__(self): + s = "Memory leaks\n" + s+= "------------\n" + total_sites = 0 + total_leaks = 0 + total_bytes = 0 + for trace in sorted(self.leaks.keys(), cmp=self._leakcmp): + s += str(self.leaks[trace]) + "\n\n" + total_sites += 1 + total_leaks += self.leaks[trace].nleaks + total_bytes += self.leaks[trace].nbytes + s+= "Total: %d leaks from %d sites, containing %d bytes\n" % \ + (total_leaks, total_sites, total_bytes) + return s + +def main(): + executable = None + try: + opts, args = getopt.getopt(sys.argv[1:], 'hp:') + except getopt.GetoptError: + print >> sys.stderr, "Invalid commandline arguments" + usage() + for o, a in opts: + if o in ('-h', '--help'): + usage() + if o in ('-p', '--program'): + executable = a + continue + + if not executable: + print >> sys.stderr, "Missing executable name" + usage(); + + leaks = LeakTracker(executable) + for line in sys.stdin: + if line.startswith("LEAK "): + leakinfo = line.split() + if len(leakinfo) < 4 or leakinfo[3] != "TRACE": + sys.stdout.write(line); + continue + backtrace = leakinfo[4:] + nbytes = int(leakinfo[2]) + leaks.addleak(nbytes, backtrace) + + print leaks + +if __name__ == '__main__': main() + diff --git a/leakmalloc/regress/Makefile b/leakmalloc/regress/Makefile new file mode 100644 index 0000000..491715c --- /dev/null +++ b/leakmalloc/regress/Makefile @@ -0,0 +1,57 @@ +# $OpenBSD$ + +.include +.include + +CDIAGFLAGS= -Wall +CDIAGFLAGS+= -Werror +CDIAGFLAGS+= -Wpointer-arith +CDIAGFLAGS+= -Wstrict-prototypes +CDIAGFLAGS+= -Wmissing-prototypes +CDIAGFLAGS+= -Wunused +CDIAGFLAGS+= -Wsign-compare +CDIAGFLAGS+= -Wshadow +CDIAGFLAGS+= -Wformat +.if (${CC:L} == "gcc" || ${CC:L} == "cc") +CDIAGFLAGS+= -Wbounded +.endif +DEBUG=-g +COPTS=-O0 + +CPPFLAGS+=-I${.CURDIR}/../leakmalloc + +.if exists(${.CURDIR}/../leakmalloc/${__objdir}) +LDADD+=-L${.CURDIR}/../leakmalloc/${__objdir} -lleakmalloc +DPADD+=${.CURDIR}/../leakmalloc/${__objdir}/libleakmalloc.a +.else +LDADD+=-L${.CURDIR}/../leakmalloc -lleakmalloc +DPADD+=${.CURDIR}/../leakmalloc/libleakmalloc.a +.endif + +LDFLAGS+=-L/usr/local/lib +LDADD+=-lexecinfo + +PROG=leaky +SRCS=leaky.c +REGRESS_TARGETS=does-leak outfile exit-leak summary + +does-leak: ${PROG} + ./${PROG} | grep -q "^LEAK " + +outfile: ${PROG} + rm -f leakmalloc.out + env LEAKMALLOC_OPTIONS=D ./${PROG} + grep -q "^LEAK " leakmalloc.out + +exit-leak: ${PROG} + sh -c 'export LEAKMALLOC_OPTIONS=XQ ; ./${PROG} ; test $$? -eq 99' + +summary: ${PROG} + test `./${PROG} | ${.CURDIR}/../leakresolve.py -p ${PROG} | \ + grep "^Total:" | cut -d' ' -f 2` -eq 22 + +clean: + rm ${PROG} *.o *.core leakmalloc.out + +.include + diff --git a/leakmalloc/regress/leaky.c b/leakmalloc/regress/leaky.c new file mode 100644 index 0000000..9f37797 --- /dev/null +++ b/leakmalloc/regress/leaky.c @@ -0,0 +1,60 @@ +#include +#include + +#include "leakmalloc.h" + +static void *a, *b, *c, *d, *e, *f; + +static void +f6(void) +{ + d = strdup("hello"); + e = malloc(789); +} + +static void +f5(void) +{ + c = calloc(1, 678); + f6(); +} + +static void +f4(void) +{ + b = malloc(456); + f5(); + free(e); +} + +static void +f3(void) +{ + a = malloc(123); + f = realloc(NULL, 321); +} + +static void +f2(void) +{ + f4(); + b = realloc(b, 567); +} + +static void +f1(void) +{ + int i; + + f2(); + for (i = 0; i < 10; i++) + f3(); +} + +int +main(void) +{ + f1(); + free(f); + return 0; +}