proj/test-memory: Output well-formatted report
This commit is contained in:
@@ -23,7 +23,7 @@ StructAddrInfo = TypedDict('StructAddrInfo', {'ai_flags': Flags, 'ai_family': Co
|
|||||||
StructMsgHdr = TypedDict('StructMsgHdr', {})
|
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):
|
class ThreadedUnixStreamServer(ThreadingMixIn, UnixStreamServer):
|
||||||
@@ -36,11 +36,13 @@ class Parser:
|
|||||||
pid: Optional[int]
|
pid: Optional[int]
|
||||||
tid: Optional[int]
|
tid: Optional[int]
|
||||||
path: Optional[str]
|
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
|
ret_addr: int
|
||||||
dli_file_name: str
|
|
||||||
rel_ret_addr: int
|
rel_ret_addr: int
|
||||||
|
dli_file_name: str
|
||||||
dli_sym_name: Optional[str]
|
dli_sym_name: Optional[str]
|
||||||
|
src_file_name: Optional[str]
|
||||||
|
src_line_num: Optional[int]
|
||||||
|
|
||||||
def __init__(self, rfile: BinaryIO, wfile: BinaryIO = None):
|
def __init__(self, rfile: BinaryIO, wfile: BinaryIO = None):
|
||||||
self.rfile = rfile
|
self.rfile = rfile
|
||||||
@@ -226,15 +228,21 @@ class Parser:
|
|||||||
call = data.decode('utf-8')
|
call = data.decode('utf-8')
|
||||||
#print(f'[{self.pid}][{self.tid}] {call}')
|
#print(f'[{self.pid}][{self.tid}] {call}')
|
||||||
func_name = call[:call.find('(')]
|
func_name = call[:call.find('(')]
|
||||||
ret = call[call.rfind(':'):]
|
ret = call[call.rfind(': '):]
|
||||||
m = RET_ADDR_RE.match(ret)
|
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.ret_addr = int(g_ret, 0)
|
||||||
|
self.rel_ret_addr = int(g_rel, 0)
|
||||||
self.dli_file_name = g_fname
|
self.dli_file_name = g_fname
|
||||||
self.rel_ret_addr = int(g_ret, 0)
|
|
||||||
self.dli_sym_name = g_sym
|
self.dli_sym_name = g_sym
|
||||||
args, _ = Parser.parse_args(call[call.find('(') + 1:call.rfind(':') - 1])
|
self.src_file_name = src_fname
|
||||||
self.stack.append((self.ret_addr, self.dli_file_name, self.rel_ret_addr, self.dli_sym_name, func_name, args))
|
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:
|
try:
|
||||||
func = getattr(self, f'before_{func_name}')
|
func = getattr(self, f'before_{func_name}')
|
||||||
if not callable(func):
|
if not callable(func):
|
||||||
@@ -262,7 +270,10 @@ class Parser:
|
|||||||
other_vals = ret[1].strip() if len(ret) > 1 else ''
|
other_vals = ret[1].strip() if len(ret) > 1 else ''
|
||||||
if len(other_vals) > 0:
|
if len(other_vals) > 0:
|
||||||
kwargs, _ = Parser.parse_args(other_vals, named=True, ret=True)
|
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:
|
try:
|
||||||
func = getattr(self, f'after_{func_name}')
|
func = getattr(self, f'after_{func_name}')
|
||||||
if not callable(func):
|
if not callable(func):
|
||||||
|
|||||||
@@ -54,71 +54,80 @@ SKIP_ERRORS: list[str] = ['EINTR']
|
|||||||
|
|
||||||
|
|
||||||
class MemoryAllocationParser(Parser):
|
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
|
max_allocated: int
|
||||||
num_alloc: int
|
num_alloc: int
|
||||||
num_realloc: int
|
num_realloc: int
|
||||||
num_free: int
|
num_free: int
|
||||||
num_invalid_free: int
|
|
||||||
|
|
||||||
def before(self):
|
def before(self):
|
||||||
self.allocated = {}
|
self.allocated = {}
|
||||||
|
self.invalid_frees = []
|
||||||
self.max_allocated = 0
|
self.max_allocated = 0
|
||||||
self.num_alloc = 0
|
self.num_alloc = 0
|
||||||
self.num_realloc = 0
|
self.num_realloc = 0
|
||||||
self.num_free = 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):
|
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:
|
if total > self.max_allocated:
|
||||||
self.max_allocated = total
|
self.max_allocated = total
|
||||||
|
|
||||||
def after_malloc(self, size, ret_value, errno=None) -> None:
|
def after_malloc(self, size, ret_value, errno=None) -> None:
|
||||||
self.num_alloc += 1
|
self.num_alloc += 1
|
||||||
if ret_value != 0:
|
if ret_value != 0:
|
||||||
self.allocated[ret_value] = ('malloc', self.dli_file_name, self.dli_sym_name, self.rel_ret_addr, size)
|
self.allocated[ret_value] = ('malloc', size,
|
||||||
self.update_max_allocated()
|
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:
|
def after_calloc(self, nmemb, size, ret_value, errno=None) -> None:
|
||||||
self.num_alloc += 1
|
self.num_alloc += 1
|
||||||
if ret_value != 0:
|
if ret_value != 0:
|
||||||
self.allocated[ret_value] = ('calloc', self.dli_file_name, self.dli_sym_name, self.rel_ret_addr, nmemb * size)
|
self.allocated[ret_value] = ('calloc', nmemb * size,
|
||||||
self.update_max_allocated()
|
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:
|
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:
|
if ret_value != 0 and ptr in self.allocated:
|
||||||
v = self.allocated[ptr]
|
v = self.allocated[ptr]
|
||||||
del self.allocated[ptr]
|
del self.allocated[ptr]
|
||||||
self.allocated[ret_value] = (v[0], v[1], v[2], v[3], size)
|
self.allocated[ret_value] = (v[0], size, v[2], v[3], v[4], v[5], v[6])
|
||||||
self.update_max_allocated()
|
self.update_max_allocated()
|
||||||
|
|
||||||
def after_reallocarray(self, ptr, nmemb, size, ret_value, errno=None) -> None:
|
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:
|
if ret_value != 0:
|
||||||
v = self.allocated[ptr]
|
v = self.allocated[ptr]
|
||||||
del self.allocated[ptr]
|
del self.allocated[ptr]
|
||||||
self.allocated[ret_value] = (v[0], v[1], v[2], v[3], nmemb * size)
|
self.allocated[ret_value] = (v[0], nmemb * size, v[2], v[3], v[4], v[5], v[6])
|
||||||
self.update_max_allocated()
|
self.update_max_allocated()
|
||||||
|
|
||||||
def after_getaddrinfo(self, node, service, hints, res_ptr, ret_value, errno=None, res=None) -> None:
|
def after_getaddrinfo(self, node, service, hints, res_ptr, ret_value, errno=None, res=None) -> None:
|
||||||
self.num_alloc += 1
|
self.num_alloc += 1
|
||||||
if ret_value[0] == 0 and res is not None:
|
if ret_value[0] == 0 and res is not None:
|
||||||
size = sum(48 + r['ai_addrlen'] for r in res[1])
|
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.allocated[res[0]] = ('getaddrinfo', size,
|
||||||
self.update_max_allocated()
|
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:
|
def after_free(self, ptr) -> None:
|
||||||
self.num_free += 1
|
self.num_free += 1
|
||||||
@@ -127,7 +136,9 @@ class MemoryAllocationParser(Parser):
|
|||||||
del self.allocated[ptr]
|
del self.allocated[ptr]
|
||||||
else:
|
else:
|
||||||
self.num_free -= 1
|
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:
|
def after_freeaddrinfo(self, res: Pointer) -> None:
|
||||||
self.num_free += 1
|
self.num_free += 1
|
||||||
@@ -136,7 +147,9 @@ class MemoryAllocationParser(Parser):
|
|||||||
del self.allocated[res]
|
del self.allocated[res]
|
||||||
else:
|
else:
|
||||||
self.num_free -= 1
|
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):
|
class MemoryAllocationTester(MemoryAllocationParser, Handler):
|
||||||
|
|||||||
@@ -10,18 +10,39 @@ import intercept
|
|||||||
import intercept.standard
|
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:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('-i', '--stdin')
|
||||||
args, extra = parser.parse_known_args()
|
args, extra = parser.parse_known_args()
|
||||||
if len(extra) > 0 and extra[0] == '--':
|
if len(extra) > 0 and extra[0] == '--':
|
||||||
extra.pop(0)
|
extra.pop(0)
|
||||||
if len(extra) == 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'
|
log_file = f'/tmp/intercept.memory.{os.getpid()}.log'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.run(extra, stdin=sys.stdin, env={
|
subprocess.run(extra, stdin=stdin, env={
|
||||||
'LD_PRELOAD': os.getcwd() + '/../../intercept/intercept.so',
|
'LD_PRELOAD': os.getcwd() + '/../../intercept/intercept.so',
|
||||||
'INTERCEPT': 'file:' + log_file,
|
'INTERCEPT': 'file:' + log_file,
|
||||||
'INTERCEPT_VERBOSE': '1',
|
'INTERCEPT_VERBOSE': '1',
|
||||||
@@ -32,6 +53,40 @@ def main() -> None:
|
|||||||
with open(log_file, 'rb') as file:
|
with open(log_file, 'rb') as file:
|
||||||
parser = intercept.standard.MemoryAllocationParser(file)
|
parser = intercept.standard.MemoryAllocationParser(file)
|
||||||
parser.parse()
|
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)
|
os.remove(log_file)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user