From f16a901207eb92157899397575817abc30656fdc Mon Sep 17 00:00:00 2001 From: Lorenz Stechauner Date: Tue, 6 May 2025 14:18:34 +0200 Subject: [PATCH] proj/test-memory: Output well-formatted report --- proj/server/src/intercept/__init__.py | 29 +++++++---- proj/server/src/intercept/standard.py | 71 ++++++++++++++++----------- proj/server/src/test-memory | 59 +++++++++++++++++++++- 3 files changed, 119 insertions(+), 40 deletions(-) diff --git a/proj/server/src/intercept/__init__.py b/proj/server/src/intercept/__init__.py index 14e5ff0..cc39631 100644 --- a/proj/server/src/intercept/__init__.py +++ b/proj/server/src/intercept/__init__.py @@ -23,7 +23,7 @@ StructAddrInfo = TypedDict('StructAddrInfo', {'ai_flags': Flags, 'ai_family': Co StructMsgHdr = TypedDict('StructMsgHdr', {}) -RET_ADDR_RE = re.compile(r': *((0x)?[0-9a-fA-Fx]+) *\((.+?)\+(.+?)(, *(.+?))?\)$') +RET_ADDR_RE = re.compile(r': *((0x)?[0-9a-fA-Fx]+) *\((.+?)\+(.+?)(, *([^:]+?))?(, *(([^:]+?):([0-9]+)))?\)$') class ThreadedUnixStreamServer(ThreadingMixIn, UnixStreamServer): @@ -36,11 +36,13 @@ class Parser: pid: Optional[int] tid: Optional[int] path: Optional[str] - stack: list[tuple[int, str, int, Optional[str], str, tuple]] + stack: list[tuple[int, int, str, Optional[str], Optional[str], Optional[int], str, tuple]] ret_addr: int - dli_file_name: str rel_ret_addr: int + dli_file_name: str dli_sym_name: Optional[str] + src_file_name: Optional[str] + src_line_num: Optional[int] def __init__(self, rfile: BinaryIO, wfile: BinaryIO = None): self.rfile = rfile @@ -226,15 +228,21 @@ class Parser: call = data.decode('utf-8') #print(f'[{self.pid}][{self.tid}] {call}') func_name = call[:call.find('(')] - ret = call[call.rfind(':'):] + ret = call[call.rfind(': '):] m = RET_ADDR_RE.match(ret) - g_ret, _, g_fname, g_rel, _, g_sym = m.groups() + g_ret, _, g_fname, g_rel, _, g_sym, _, _, src_fname, src_line = m.groups() self.ret_addr = int(g_ret, 0) + self.rel_ret_addr = int(g_rel, 0) self.dli_file_name = g_fname - self.rel_ret_addr = int(g_ret, 0) self.dli_sym_name = g_sym - args, _ = Parser.parse_args(call[call.find('(') + 1:call.rfind(':') - 1]) - self.stack.append((self.ret_addr, self.dli_file_name, self.rel_ret_addr, self.dli_sym_name, func_name, args)) + self.src_file_name = src_fname + self.src_line_num = int(src_line, 0) if src_line else None + args, _ = Parser.parse_args(call[call.find('(') + 1:call.rfind(': ') - 1]) + self.stack.append( + (self.ret_addr, self.rel_ret_addr, + self.dli_file_name, self.dli_sym_name, + self.src_file_name, self.src_line_num, + func_name, args)) try: func = getattr(self, f'before_{func_name}') if not callable(func): @@ -262,7 +270,10 @@ class Parser: other_vals = ret[1].strip() if len(ret) > 1 else '' if len(other_vals) > 0: kwargs, _ = Parser.parse_args(other_vals, named=True, ret=True) - (self.ret_addr, self.dli_file_name, self.rel_ret_addr, self.dli_sym_name, func_name, args) = self.stack.pop() + (self.ret_addr, self.rel_ret_addr, + self.dli_file_name, self.dli_sym_name, + self.src_file_name, self.src_line_num, + func_name, args) = self.stack.pop() try: func = getattr(self, f'after_{func_name}') if not callable(func): diff --git a/proj/server/src/intercept/standard.py b/proj/server/src/intercept/standard.py index d0a67a1..ab2fc65 100644 --- a/proj/server/src/intercept/standard.py +++ b/proj/server/src/intercept/standard.py @@ -54,71 +54,80 @@ SKIP_ERRORS: list[str] = ['EINTR'] class MemoryAllocationParser(Parser): - allocated: dict[int, tuple[str, str, str, int, int]] + allocated: dict[int, tuple[str, int, str, str, int, str, int]] + invalid_frees: list[tuple[str, int, str, str, int, str, int]] max_allocated: int num_alloc: int num_realloc: int num_free: int - num_invalid_free: int def before(self): self.allocated = {} + self.invalid_frees = [] self.max_allocated = 0 self.num_alloc = 0 self.num_realloc = 0 self.num_free = 0 - self.num_invalid_free = 0 - - def after(self): - if len(self.allocated) > 0: - print("\x1B[31;1mNot free'd:\x1B[0m", file=sys.stderr) - for ptr, (func, fname, sname, ret, size) in self.allocated.items(): - print(f'\x1B[31;1m 0x{ptr:x}: {size} bytes ({func}, return address {fname}+0x{ret:x} {sname})\x1B[0m', file=sys.stderr) - else: - print("\x1B[32;1mAll blocks free'd!\x1B[0m", file=sys.stderr) - print(f'Max allocated: {self.max_allocated} bytes', file=sys.stderr) def update_max_allocated(self): - total = sum(a[-1] for a in self.allocated.values()) + total = sum(a[1] for a in self.allocated.values()) if total > self.max_allocated: self.max_allocated = total def after_malloc(self, size, ret_value, errno=None) -> None: self.num_alloc += 1 if ret_value != 0: - self.allocated[ret_value] = ('malloc', self.dli_file_name, self.dli_sym_name, self.rel_ret_addr, size) - self.update_max_allocated() + self.allocated[ret_value] = ('malloc', size, + self.dli_file_name, self.dli_sym_name, self.rel_ret_addr, + self.src_file_name, self.src_line_num) + self.update_max_allocated() def after_calloc(self, nmemb, size, ret_value, errno=None) -> None: self.num_alloc += 1 if ret_value != 0: - self.allocated[ret_value] = ('calloc', self.dli_file_name, self.dli_sym_name, self.rel_ret_addr, nmemb * size) - self.update_max_allocated() + self.allocated[ret_value] = ('calloc', nmemb * size, + self.dli_file_name, self.dli_sym_name, self.rel_ret_addr, + self.src_file_name, self.src_line_num) + self.update_max_allocated() def after_realloc(self, ptr, size, ret_value, errno=None) -> None: - self.num_realloc += 1 - if ptr != 0: + if ptr == 0: + self.num_alloc += 1 + if ret_value != 0: + self.allocated[ret_value] = ('realloc', size, + self.dli_file_name, self.dli_sym_name, self.rel_ret_addr, + self.src_file_name, self.src_line_num) + else: + self.num_realloc += 1 if ret_value != 0 and ptr in self.allocated: v = self.allocated[ptr] del self.allocated[ptr] - self.allocated[ret_value] = (v[0], v[1], v[2], v[3], size) - self.update_max_allocated() + self.allocated[ret_value] = (v[0], size, v[2], v[3], v[4], v[5], v[6]) + self.update_max_allocated() def after_reallocarray(self, ptr, nmemb, size, ret_value, errno=None) -> None: - self.num_realloc += 1 - if ptr != 0: + if ptr == 0: + self.num_alloc += 1 + if ret_value != 0: + self.allocated[ret_value] = ('reallocarray', nmemb * size, + self.dli_file_name, self.dli_sym_name, self.rel_ret_addr, + self.src_file_name, self.src_line_num) + else: + self.num_realloc += 1 if ret_value != 0: v = self.allocated[ptr] del self.allocated[ptr] - self.allocated[ret_value] = (v[0], v[1], v[2], v[3], nmemb * size) - self.update_max_allocated() + self.allocated[ret_value] = (v[0], nmemb * size, v[2], v[3], v[4], v[5], v[6]) + self.update_max_allocated() def after_getaddrinfo(self, node, service, hints, res_ptr, ret_value, errno=None, res=None) -> None: self.num_alloc += 1 if ret_value[0] == 0 and res is not None: size = sum(48 + r['ai_addrlen'] for r in res[1]) - self.allocated[res[0]] = ('getaddrinfo', self.dli_file_name, self.dli_sym_name, self.rel_ret_addr, size) - self.update_max_allocated() + self.allocated[res[0]] = ('getaddrinfo', size, + self.dli_file_name, self.dli_sym_name, self.rel_ret_addr, + self.src_file_name, self.src_line_num) + self.update_max_allocated() def after_free(self, ptr) -> None: self.num_free += 1 @@ -127,7 +136,9 @@ class MemoryAllocationParser(Parser): del self.allocated[ptr] else: self.num_free -= 1 - self.num_invalid_free += 1 + self.invalid_frees.append(('free', ptr, + self.dli_file_name, self.dli_sym_name, self.rel_ret_addr, + self.src_file_name, self.src_line_num)) def after_freeaddrinfo(self, res: Pointer) -> None: self.num_free += 1 @@ -136,7 +147,9 @@ class MemoryAllocationParser(Parser): del self.allocated[res] else: self.num_free -= 1 - self.num_invalid_free += 1 + self.invalid_frees.append(('freeaddrinfo', res, + self.dli_file_name, self.dli_sym_name, self.rel_ret_addr, + self.src_file_name, self.src_line_num)) class MemoryAllocationTester(MemoryAllocationParser, Handler): diff --git a/proj/server/src/test-memory b/proj/server/src/test-memory index b41f6d4..7033a19 100755 --- a/proj/server/src/test-memory +++ b/proj/server/src/test-memory @@ -10,18 +10,39 @@ import intercept import intercept.standard +def neutral(text: str) -> None: + print(text, file=sys.stderr) + +def color(text: str, col: str) -> None: + print(('\x1B[' + col + 'm' if sys.stderr.isatty() else '') + + text + + ('\x1B[0m' if sys.stderr.isatty() else ''), + file=sys.stderr) + +def bold(text: str) -> None: + color(text, '1') + +def red(text: str) -> None: + color(text, '31;1') + +def green(text: str) -> None: + color(text, '32;1') + + def main() -> None: parser = argparse.ArgumentParser() + parser.add_argument('-i', '--stdin') args, extra = parser.parse_known_args() if len(extra) > 0 and extra[0] == '--': extra.pop(0) if len(extra) == 0: - parser.error('command expected after arguments or \'--\'') + parser.error("command expected after arguments or '--'") + stdin = open(args.stdin) if args.stdin else sys.stdin log_file = f'/tmp/intercept.memory.{os.getpid()}.log' try: - subprocess.run(extra, stdin=sys.stdin, env={ + subprocess.run(extra, stdin=stdin, env={ 'LD_PRELOAD': os.getcwd() + '/../../intercept/intercept.so', 'INTERCEPT': 'file:' + log_file, 'INTERCEPT_VERBOSE': '1', @@ -32,6 +53,40 @@ def main() -> None: with open(log_file, 'rb') as file: parser = intercept.standard.MemoryAllocationParser(file) parser.parse() + bold(':: REPORT ::') + neutral('::') + if len(parser.allocated) > 0: + red(':: TEST :: MEMORY LEAKS :: FAILED ::') + red(":: Not free'd:") + for ptr, (func, size, fname, sname, ret, src, line) in parser.allocated.items(): + fname = fname.split('/')[-1] + pos = [func, f'{fname}+0x{ret:x}'] + if sname: + pos.append(sname) + if src and line: + pos.append(f'{src}:{line}') + red(f':: 0x{ptr:x}: {size:>6} bytes ({", ".join(pos)})') + else: + green(':: TEST :: MEMORY LEAKS :: PASSED ::') + green(":: All allocated memory blocks were free'd!") + neutral('::') + if len(parser.invalid_frees) > 0: + red(':: TEST :: INVALID FREES :: FAILED ::') + red(':: Invalid/double frees:') + for (ptr, func, fname, sname, ret, src, line) in parser.invalid_frees: + fname = fname.split('/')[-1] + pos = [f'{fname}+0x{ret:x}'] + if sname: + pos.append(sname) + if src and line: + pos.append(f'{src}:{line}') + red(f':: {func}: 0x{ptr:x} ({", ".join(pos)})') + else: + green(':: TEST :: INVALID FREES :: PASSED ::') + green(':: No invalid/double frees occured!') + neutral('::') + neutral(f':: #allocs: {parser.num_alloc}, #reallocs: {parser.num_realloc}, #frees: {parser.num_free}') + neutral(f':: Max dynamically allocated: {parser.max_allocated} bytes') os.remove(log_file)