From de790eabf9912c486ae9d5bc9685c6b31b68070a Mon Sep 17 00:00:00 2001 From: Lorenz Stechauner Date: Tue, 4 Mar 2025 14:49:15 +0100 Subject: [PATCH] proj: Restructure --- proj/intercept/.gitignore | 2 + proj/intercept/Makefile | 28 +++ proj/{test1 => intercept}/src/intercept.c | 6 +- .../{intercept.py => intercept/__init__.py} | 151 +--------------- proj/server/src/intercept/standard.py | 165 ++++++++++++++++++ proj/server/src/server.py | 46 ----- proj/server/src/test-interrupts | 28 +++ proj/server/src/test-memory | 28 +++ proj/server/src/test-return-values | 28 +++ proj/test1/Makefile | 16 +- 10 files changed, 291 insertions(+), 207 deletions(-) create mode 100644 proj/intercept/.gitignore create mode 100644 proj/intercept/Makefile rename proj/{test1 => intercept}/src/intercept.c (99%) rename proj/server/src/{intercept.py => intercept/__init__.py} (73%) mode change 100755 => 100644 create mode 100644 proj/server/src/intercept/standard.py delete mode 100755 proj/server/src/server.py create mode 100755 proj/server/src/test-interrupts create mode 100755 proj/server/src/test-memory create mode 100755 proj/server/src/test-return-values diff --git a/proj/intercept/.gitignore b/proj/intercept/.gitignore new file mode 100644 index 0000000..dd5b4ef --- /dev/null +++ b/proj/intercept/.gitignore @@ -0,0 +1,2 @@ +/main +/main_* diff --git a/proj/intercept/Makefile b/proj/intercept/Makefile new file mode 100644 index 0000000..2f2dc4f --- /dev/null +++ b/proj/intercept/Makefile @@ -0,0 +1,28 @@ + +CC=gcc +CFLAGS=-std=c99 -pedantic -Wall -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_SVID_SOURCE -D_POSIX_C_SOURCE=200809L -g + +.PHONY: all clean +all: default +default: bin intercept.so main_intercept + +bin: + mkdir -p bin/ + +bin/main.o: ../test1/src/main.c + $(CC) -c -o $@ $^ $(CFLAGS) + +bin/intercept_preload.o: src/intercept.c + $(CC) -fPIC -c -o $@ $^ $(CFLAGS) -DINTERCEPT_PRELOAD + +intercept.so: bin/intercept_preload.o + $(CC) -shared -o $@ $^ $(CFLAGS) -lc -ldl + +main_intercept: bin/main.o src/intercept.c + $(CC) -o $@ $^ $(CFLAGS) -lc -Wl,--wrap=malloc,--wrap=free,--wrap=calloc,--wrap=realloc,--wrap=reallocarray,--wrap=getopt,--wrap=exit,--wrap=close,--wrap=sigaction,\ +--wrap=sem_init,--wrap=sem_open,--wrap=sem_post,--wrap=sem_wait,--wrap=sem_trywait,--wrap=sem_timedwait,--wrap=sem_getvalue,--wrap=sem_close,--wrap=sem_unlink,--wrap=sem_destroy,\ +--wrap=shm_open,--wrap=shm_unlink,--wrap=mmap,--wrap=munmap,\ +--wrap=ftruncate + +clean: + rm -rf main_intercept bin/* *.so *.ko *.o diff --git a/proj/test1/src/intercept.c b/proj/intercept/src/intercept.c similarity index 99% rename from proj/test1/src/intercept.c rename to proj/intercept/src/intercept.c index c2a198f..041d009 100644 --- a/proj/test1/src/intercept.c +++ b/proj/intercept/src/intercept.c @@ -45,7 +45,7 @@ static int (*__real_shm_open)(const char *, int, mode_t); static int (*__real_shm_unlink)(const char *); static void *(*__real_mmap)(void *, size_t, int, int, int, off_t); static int (*__real_munmap)(void *, size_t); -extern int (*__real_ftruncate)(int, off_t); +static int (*__real_ftruncate)(int, off_t); #define load(name) \ if (((__real_ ## name) = dlsym(RTLD_NEXT, #name)) == NULL) { \ fprintf(stderr, "intercept: unable to load symbol '%s': %s", #name, strerror(errno)); \ @@ -558,7 +558,9 @@ static void init(void) { return; } fprintf(stderr, "intercept: intercepting function/system calls and logging to unix socket\n"); - msg("PID:%li", getpid()); + char buf[256] = ""; + const ssize_t ret = readlink("/proc/self/exe", buf, sizeof(buf)); + msg("PID:%li%s%s", getpid(), ret != -1 ? ";PATH:" : "", buf); } else if (val && strncmp(val, "tcp://", 6) == 0) { mode = 5; // TODO socket/tcp mode diff --git a/proj/server/src/intercept.py b/proj/server/src/intercept/__init__.py old mode 100755 new mode 100644 similarity index 73% rename from proj/server/src/intercept.py rename to proj/server/src/intercept/__init__.py index b8a7b30..0063653 --- a/proj/server/src/intercept.py +++ b/proj/server/src/intercept/__init__.py @@ -3,7 +3,6 @@ from typing import Optional, TypedDict, NotRequired from socketserver import UnixStreamServer, StreamRequestHandler, ThreadingMixIn -import argparse import os import re @@ -21,7 +20,8 @@ class ThreadedUnixStreamServer(ThreadingMixIn, UnixStreamServer): class Handler(StreamRequestHandler): - pid: int + pid: Optional[int] + path: Optional[str] stack: list[tuple[int, str, tuple]] ret_addr: int @@ -32,9 +32,11 @@ class Handler(StreamRequestHandler): def handle(self): first = self.rfile.readline() - self.pid = int(first.split(b':')[1]) + meta = {a[0]: a[1] for a in [tuple(p.decode('utf-8').split(':', 1)) for p in first.split(b' ', 1)[1].strip().split(b';')]} + self.pid = int(meta['PID']) if 'PID' in meta else None + self.path = meta['PATH'] if 'PATH' in meta else None self.stack = [] - print(f'Process with PID {self.pid} connected') + print(f'Process with PID {self.pid} connected ({self.path})') self.before() try: while True: @@ -317,136 +319,6 @@ class Handler(StreamRequestHandler): raise NotImplementedError() -class MemoryAllocationTester(Handler): - allocated: dict[int, tuple[str, int, int]] - max_allocated: int - num_malloc: int - num_realloc: int - num_free: int - num_invalid_free: int - - def before(self): - self.allocated = {} - self.max_allocated = 0 - self.num_malloc = 0 - self.num_realloc = 0 - self.num_free = 0 - self.num_invalid_free = 0 - - def after(self): - if len(self.allocated) > 0: - print("Not free'd:") - for ptr, (func, ret, size) in self.allocated.items(): - print(f' 0x{ptr:x}: {size} bytes ({func}, return address 0x{ret:x})') - else: - print("All blocks free'd!") - print(f'Max allocated: {self.max_allocated} bytes') - - def update_max_allocated(self): - total = sum(a[2] 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_malloc += 1 - if ret_value != 0: - self.allocated[ret_value] = ('malloc', self.ret_addr, size) - self.update_max_allocated() - - def after_calloc(self, nmemb, size, ret_value, errno=None) -> None: - self.num_malloc += 1 - if ret_value != 0: - self.allocated[ret_value] = ('calloc', self.ret_addr, nmemb * size) - self.update_max_allocated() - - def after_realloc(self, ptr, size, ret_value, errno=None) -> None: - self.num_realloc += 1 - if ptr != 0: - if ret_value != 0: - v = self.allocated[ptr] - del self.allocated[ptr] - self.allocated[ret_value] = (v[0], v[1], size) - self.update_max_allocated() - - def after_reallocarray(self, ptr, nmemb, size, ret_value, errno=None) -> None: - self.num_realloc += 1 - if ptr != 0: - if ret_value != 0: - v = self.allocated[ptr] - del self.allocated[ptr] - self.allocated[ret_value] = (v[0], v[1], nmemb * size) - self.update_max_allocated() - - def after_free(self, ptr) -> None: - self.num_free += 1 - if ptr != 0: - if ptr in self.allocated: - del self.allocated[ptr] - else: - self.num_free -= 1 - self.num_invalid_free += 1 - - -class ReturnValueCheckTester(Handler): - pass - - -class InterruptedCheckTester(Handler): - cycles: int = 50 - functions: dict[str, tuple[str or None, str]] = { - 'sem_wait': ('fail EINTR', 'return 0'), - 'sem_trywait': ('fail EINTR', 'return 0'), - 'sem_timedwait': ('fail EINTR', 'return 0'), - 'sem_post': (None, 'return 0'), - 'ftruncate': ('fail EINTR', 'ok'), - } - - counter: int = 0 - last_func_name: Optional[str] = None - last_ret_addr: Optional[int] = None - tested_functions: dict[tuple[str, int], str] - - @property - def while_testing(self) -> bool: - return self.counter % self.cycles != 0 - - def before(self) -> None: - self.tested_functions = {} - - def after(self) -> None: - if self.while_testing: - self.error() - for (name, ret_addr), status in self.tested_functions.items(): - print(f'{name} (0x{ret_addr:x}) -> {status}') - - def error(self): - print(f'Error: Return value and errno EINTR not handled correctly in {self.last_func_name} (return address 0x{self.last_ret_addr:x})') - self.tested_functions[(self.last_func_name, self.last_ret_addr)] = 'failed' - self.counter = 0 - self.last_func_name = None - self.last_ret_addr = None - - def before_fallback(self, func_name: str, *args) -> str: - if self.while_testing and (self.last_func_name != func_name or self.last_ret_addr != self.ret_addr): - self.error() - return 'ok' - elif func_name not in self.functions: - return 'ok' - elif self.functions[func_name][0] is None: - return self.functions[func_name][1] - self.counter += 1 - if self.while_testing: - self.last_ret_addr = self.ret_addr - self.last_func_name = func_name - self.tested_functions[(self.last_func_name, self.last_ret_addr)] = 'running' - return self.functions[func_name][0] - else: - self.tested_functions[(self.last_func_name, self.last_ret_addr)] = 'passed' - self.last_ret_addr = None - self.last_func_name = None - return self.functions[func_name][1] - - def intercept(socket: str, handler: type[Handler]) -> None: try: with ThreadedUnixStreamServer(socket, handler) as server: @@ -459,14 +331,3 @@ def intercept(socket: str, handler: type[Handler]) -> None: os.unlink(socket) except FileNotFoundError: pass - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument('socket', metavar='FILE') - args = parser.parse_args() - intercept(args.socket, Handler) - - -if __name__ == '__main__': - main() diff --git a/proj/server/src/intercept/standard.py b/proj/server/src/intercept/standard.py new file mode 100644 index 0000000..e2f71e7 --- /dev/null +++ b/proj/server/src/intercept/standard.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from intercept import * + + +FUNCTION_ERRORS: dict[str, list[str]] = { + 'malloc': ['ENOMEM'], + 'calloc': ['ENOMEM'], + 'realloc': ['ENOMEM'], + 'reallocarray': ['ENOMEM'], + 'close': ['EBADF'], # EINTR, EIO + 'sigaction': ['EINVAL'], + 'sem_init': ['EINVAL', 'ENOSYS'], + 'sem_open': ['EACCES', 'EEXIST', 'EINVAL', 'EMFILE', 'ENAMETOOLONG', 'ENFILE', 'ENOENT', 'ENOMEM'], + 'sem_post': ['EINVAL', 'EOVERFLOW'], + 'sem_wait': ['EINTR', 'EINVAL'], + 'sem_trywait': ['EAGAIN', 'EINTR', 'EINVAL'], + 'sem_timedwait': ['EINTR', 'EINVAL', 'ETIMEDOUT'], + 'sem_getvalule': ['EINVAL'], + 'sem_close': ['EINVAL'], + 'sem_unlink': ['EACCES', 'ENAMETOOLONG', 'ENOENT'], + 'sem_destroy': ['EINVAL'], + 'shm_open': ['EACCES', 'EEXIST', 'EINVAL', 'EMFILE', 'ENAMETOOLONG', 'ENFILE', 'ENOENT'], + 'shm_unlink': ['EACCES', 'ENAMETOOLONG', 'ENOENT'], + 'mmap': ['EACCES', 'EBADF', 'EINVAL', 'EMFILE', 'ENODEV', 'ENOMEM', 'ENOTSUP', 'ENXIO', 'EOVERFLOW'], # EAGAIN + 'munmap': ['EINVAL'], + 'ftruncate': ['EINTR', 'EINVAL', 'EFBIG', 'EIO', 'EBADF'], +} + +SKIP_ERRORS: list[str] = ['EINTR'] +IGNORE_ERRORS: list[str] = ['EINVAL', 'EBADF', 'EOVERFLOW', 'ENAMETOOLONG'] + + +class MemoryAllocationTester(Handler): + allocated: dict[int, tuple[str, int, int]] + max_allocated: int + num_malloc: int + num_realloc: int + num_free: int + num_invalid_free: int + + def before(self): + self.allocated = {} + self.max_allocated = 0 + self.num_malloc = 0 + self.num_realloc = 0 + self.num_free = 0 + self.num_invalid_free = 0 + + def after(self): + if len(self.allocated) > 0: + print("Not free'd:") + for ptr, (func, ret, size) in self.allocated.items(): + print(f' 0x{ptr:x}: {size} bytes ({func}, return address 0x{ret:x})') + else: + print("All blocks free'd!") + print(f'Max allocated: {self.max_allocated} bytes') + + def update_max_allocated(self): + total = sum(a[2] 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_malloc += 1 + if ret_value != 0: + self.allocated[ret_value] = ('malloc', self.ret_addr, size) + self.update_max_allocated() + + def after_calloc(self, nmemb, size, ret_value, errno=None) -> None: + self.num_malloc += 1 + if ret_value != 0: + self.allocated[ret_value] = ('calloc', self.ret_addr, nmemb * size) + self.update_max_allocated() + + def after_realloc(self, ptr, size, ret_value, errno=None) -> None: + self.num_realloc += 1 + if ptr != 0: + if ret_value != 0: + v = self.allocated[ptr] + del self.allocated[ptr] + self.allocated[ret_value] = (v[0], v[1], size) + self.update_max_allocated() + + def after_reallocarray(self, ptr, nmemb, size, ret_value, errno=None) -> None: + self.num_realloc += 1 + if ptr != 0: + if ret_value != 0: + v = self.allocated[ptr] + del self.allocated[ptr] + self.allocated[ret_value] = (v[0], v[1], nmemb * size) + self.update_max_allocated() + + def after_free(self, ptr) -> None: + self.num_free += 1 + if ptr != 0: + if ptr in self.allocated: + del self.allocated[ptr] + else: + self.num_free -= 1 + self.num_invalid_free += 1 + + +class InterruptedCheckTester(Handler): + cycles: int = 50 + functions: dict[str, tuple[str or None, str]] = { + fn: ('fail EINTR' if fn not in ('sem_post',) else None, + 'return 0' if fn.startswith('sem_') else 'ok') + for fn, errors in FUNCTION_ERRORS.items() + if 'EINTR' in errors or fn in ('sem_post',) + } + + counter: int = 0 + last_func_name: Optional[str] = None + last_ret_addr: Optional[int] = None + tested_functions: dict[tuple[str, int], str] + + @property + def while_testing(self) -> bool: + return self.counter % self.cycles != 0 + + def before(self) -> None: + self.tested_functions = {} + + def after(self) -> None: + if self.while_testing: + self.error() + for (name, ret_addr), status in self.tested_functions.items(): + print(f'{name} (0x{ret_addr:x}) -> {status}') + + def error(self): + print(f'Error: Return value and errno EINTR not handled correctly in {self.last_func_name} (return address 0x{self.last_ret_addr:x})') + self.tested_functions[(self.last_func_name, self.last_ret_addr)] = 'failed' + self.counter = 0 + self.last_func_name = None + self.last_ret_addr = None + + def before_fallback(self, func_name: str, *args) -> str: + if self.while_testing and (self.last_func_name != func_name or self.last_ret_addr != self.ret_addr): + self.error() + return 'ok' + elif func_name not in self.functions: + return 'ok' + elif self.functions[func_name][0] is None: + return self.functions[func_name][1] + self.counter += 1 + if self.while_testing: + self.last_ret_addr = self.ret_addr + self.last_func_name = func_name + self.tested_functions[(self.last_func_name, self.last_ret_addr)] = 'running' + return self.functions[func_name][0] + else: + self.tested_functions[(self.last_func_name, self.last_ret_addr)] = 'passed' + self.last_ret_addr = None + self.last_func_name = None + return self.functions[func_name][1] + + +class ReturnValueCheckTester(Handler): + functions: dict[str, list[str]] = { + fn: [e for e in errors if e not in SKIP_ERRORS and e not in IGNORE_ERRORS] + for fn, errors in FUNCTION_ERRORS.items() + if len(set(errors) - set(SKIP_ERRORS) - set(IGNORE_ERRORS)) > 0 + } diff --git a/proj/server/src/server.py b/proj/server/src/server.py deleted file mode 100755 index 260c825..0000000 --- a/proj/server/src/server.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import argparse - -import intercept - - -class TestHandler(intercept.Handler): - allocated: dict[int, int] - - def before(self): - self.allocated = {} - - def after(self): - if len(self.allocated) > 0: - print("Not free'd:") - for ptr, size in self.allocated.items(): - print(f' 0x{ptr:x}: {size} bytes') - else: - print("All blocks free'd!") - - def after_malloc(self, size, ret_value, errno=None) -> None: - if ret_value != 0: - self.allocated[ret_value] = size - - def before_free(self, ptr) -> str: - if ptr != 0: - del self.allocated[ptr] - return 'ok' - - -class GetoptHandler(intercept.Handler): - def after_getopt(self, argc, argv, optstring, ret_value) -> None: - print(argc, [v[1] for v in argv[1]], optstring[1], ret_value) - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument('socket', metavar='FILE') - args = parser.parse_args() - intercept.intercept(args.socket, intercept.MemoryAllocationTester) - - -if __name__ == '__main__': - main() diff --git a/proj/server/src/test-interrupts b/proj/server/src/test-interrupts new file mode 100755 index 0000000..9c2e3a9 --- /dev/null +++ b/proj/server/src/test-interrupts @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import argparse +import threading +import subprocess + +import intercept +import intercept.standard + + +def socket_thread(socket: str) -> None: + intercept.intercept(socket, intercept.standard.InterruptedCheckTester) + + +def main() -> None: + parser = argparse.ArgumentParser() + args, extra = parser.parse_known_args() + socket_name = f'/tmp/intercept.interrupts.{os.getpid()}.sock' + t1 = threading.Thread(target=socket_thread, args=(socket_name,)) + t1.daemon = True + t1.start() + subprocess.run(extra, env={'LD_PRELOAD': os.getcwd() + '/../../intercept/intercept.so', 'INTERCEPT': 'unix:' + socket_name}) + + +if __name__ == '__main__': + main() diff --git a/proj/server/src/test-memory b/proj/server/src/test-memory new file mode 100755 index 0000000..228d15f --- /dev/null +++ b/proj/server/src/test-memory @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import argparse +import threading +import subprocess + +import intercept +import intercept.standard + + +def socket_thread(socket: str) -> None: + intercept.intercept(socket, intercept.standard.MemoryAllocationTester) + + +def main() -> None: + parser = argparse.ArgumentParser() + args, extra = parser.parse_known_args() + socket_name = f'/tmp/intercept.memory.{os.getpid()}.sock' + t1 = threading.Thread(target=socket_thread, args=(socket_name,)) + t1.daemon = True + t1.start() + subprocess.run(extra, env={'LD_PRELOAD': os.getcwd() + '/../../intercept/intercept.so', 'INTERCEPT': 'unix:' + socket_name}) + + +if __name__ == '__main__': + main() diff --git a/proj/server/src/test-return-values b/proj/server/src/test-return-values new file mode 100755 index 0000000..f805bb1 --- /dev/null +++ b/proj/server/src/test-return-values @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import argparse +import threading +import subprocess + +import intercept +import intercept.standard + + +def socket_thread(socket: str) -> None: + intercept.intercept(socket, intercept.standard.ReturnValueCheckTester) + + +def main() -> None: + parser = argparse.ArgumentParser() + args, extra = parser.parse_known_args() + socket_name = f'/tmp/intercept.return-values.{os.getpid()}.sock' + t1 = threading.Thread(target=socket_thread, args=(socket_name,)) + t1.daemon = True + t1.start() + subprocess.run(extra, env={'LD_PRELOAD': os.getcwd() + '/../../intercept/intercept.so', 'INTERCEPT': 'unix:' + socket_name}) + + +if __name__ == '__main__': + main() diff --git a/proj/test1/Makefile b/proj/test1/Makefile index 892a803..e331c74 100644 --- a/proj/test1/Makefile +++ b/proj/test1/Makefile @@ -4,7 +4,7 @@ CFLAGS=-std=c99 -pedantic -Wall -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_SVID_SOURCE - .PHONY: all clean all: default -default: bin main intercept.so main_intercept #test_kernel.ko +default: bin main bin: mkdir -p bin/ @@ -12,26 +12,14 @@ bin: bin/main.o: src/main.c $(CC) -c -o $@ $^ $(CFLAGS) -bin/intercept_preload.o: src/intercept.c - $(CC) -fPIC -c -o $@ $^ $(CFLAGS) -DINTERCEPT_PRELOAD - -intercept.so: bin/intercept_preload.o - $(CC) -shared -o $@ $^ $(CFLAGS) -lc -ldl - test_kernel.ko: src/test_kernel.c $(CC) -D__KERNEL__ -DMODULE -I/usr/src/linux/include -o $@ $^ main: bin/main.o $(CC) -o $@ $^ $(CFLAGS) -lc -lpthread -main_intercept: bin/main.o src/intercept.c - $(CC) -o $@ $^ $(CFLAGS) -lc -Wl,--wrap=malloc,--wrap=free,--wrap=calloc,--wrap=realloc,--wrap=reallocarray,--wrap=getopt,--wrap=exit,--wrap=close,--wrap=sigaction,\ ---wrap=sem_init,--wrap=sem_open,--wrap=sem_post,--wrap=sem_wait,--wrap=sem_trywait,--wrap=sem_timedwait,--wrap=sem_getvalue,--wrap=sem_close,--wrap=sem_unlink,--wrap=sem_destroy,\ ---wrap=shm_open,--wrap=shm_unlink,--wrap=mmap,--wrap=munmap,\ ---wrap=ftruncate - clean: - rm -rf main main_wrapped bin/* *.so *.ko *.o + rm -rf main bin/* *.so *.ko *.o #ifneq ($(KERNELRELEASE),)