diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e29008..3da99b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Systrack changelog ================== +Unreleased +---------- + +**Improvements**: + +- Add FreeBSD 13+ kernel syscall extraction support (amd64/arm64). + v0.8 ---- diff --git a/README.md b/README.md index b193ba8..69e5dda 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,14 @@ Systrack **See [mebeim/linux-syscalls](https://github.com/mebeim/linux-syscalls) for live syscall tables powered by Systrack**. -Systrack is a tool to analyze Linux kernel images (`vmlinux`) and extract -information about implemented syscalls. Given a `vmlinux` image, Systrack can -extract syscall numbers, names, symbol names, definition locations within kernel -sources, function signatures, and more. - -Systrack can configure and build kernels for all its +Systrack is a tool to analyze kernel images and extract information about +implemented syscalls. For Linux, given a `vmlinux` image, Systrack can extract +syscall numbers, names, symbol names, definition locations within kernel +sources, function signatures, and more. FreeBSD 13+ kernels are also supported +(amd64/arm64, native syscall vector only) by parsing `sysent` and +`syscallnames`. + +Systrack can configure and build Linux kernels for all its [supported architectures](#supported-architectures-and-abis), and works best at analyzing kernels that it has configured and built by itself. @@ -40,16 +42,22 @@ pip install dist/systrack-XXX.whl Usage ----- -Systrack can mainly be used for two purposes: analyzing or building Linux -kernels. See also [Command line help](#command-line-help) (`systrack --help`) +Systrack can mainly be used for two purposes: analyzing kernel images, or +building Linux kernels. See also [Command line help](#command-line-help) +(`systrack --help`) and [Supported architectures and ABIs](#supported-architectures-and-abis) (`systrack --arch help`) below. -- **Analyzing** a kernel image can be done given a `vmlinux` ELF with symbols, - and optionally also a kernel source directory (`--kdir`). Systrack will - extract information about implemented syscalls from the symbol table present - in the given `vmlinux` ELF, and if debugging information is present, it will - also extract file and line number information for syscall definitions. +- **Analyzing** a kernel image can be done given a kernel ELF with symbols + (Linux `vmlinux` or FreeBSD kernel image). Systrack will extract information + about implemented syscalls from the symbol table present in the given ELF, + and if debugging information is present, it will also extract file and line + number information for syscall definitions. + + FreeBSD support is implemented by parsing the kernel's `sysent` table and + `syscallnames` array (FreeBSD 13+, amd64/arm64). Use `--os freebsd` to + override auto-detection when needed. + Supplying a `--kdir` pointing Systrack to the checked-out sources for the right kernel version (the same as the one to analyze) will help refine and/or correct the location of the definitions. @@ -64,10 +72,15 @@ and [Supported architectures and ABIs](#supported-architectures-and-abis) systrack --format html path/to/vmlinux systrack --kdir path/to/linux_git_repo path/to/vmlinux systrack --kdir path/to/linux_git_repo --arch x86-64-ia32 path/to/vmlinux + + # FreeBSD examples (13+) + systrack /boot/kernel/kernel + systrack --format html /boot/kernel/kernel.debug + systrack --os freebsd /boot/kernel/kernel ``` -- **Building** can be done through the `--build` option. You will need to - provide a kernel source directory (`--kdir`) and an architecture/ABI +- **Building** (Linux-only) can be done through the `--build` option. You will + need to provide a kernel source directory (`--kdir`) and an architecture/ABI combination to build for (`--arch`). ```none @@ -189,16 +202,17 @@ $ systrack --help usage: systrack [OPTIONS...] [VMLINUX] -Analyze a Linux kernel image and extract information about implemented syscalls +Analyze a kernel image and extract information about implemented syscalls positional arguments: - VMLINUX path to vmlinux, if not inside KDIR or no KDIR supplied + VMLINUX path to kernel image, if not inside KDIR or no KDIR supplied options: -h, --help show this help message and exit -k KDIR, --kdir KDIR kernel source directory -a ARCH, --arch ARCH kernel architecture/ABI combination; pass "help" for a list (default: autodetect) + --os OS kernel OS: linux or freebsd; if omitted it will be auto-detected from the image -b, --build configure and build kernel and exit -c, --config configure kernel and exit -C, --clean clean kernel sources (make distclean) and exit diff --git a/pyproject.toml b/pyproject.toml index 2b81ad5..e18e1bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ readme = 'README.md' platforms = 'any' requires-python = '>=3.8' dynamic = ['version'] -keywords = ['systrack', 'linux', 'kernel', 'syscall', 'kconfig', 'elf', 'abi'] +keywords = ['systrack', 'linux', 'freebsd', 'bsd', 'kernel', 'syscall', 'kconfig', 'elf', 'abi'] classifiers = [ 'Development Status :: 4 - Beta', 'Environment :: Console', diff --git a/src/systrack/__main__.py b/src/systrack/__main__.py index 2d2e536..9adc0e5 100644 --- a/src/systrack/__main__.py +++ b/src/systrack/__main__.py @@ -8,14 +8,24 @@ from textwrap import TextWrapper from .arch import SUPPORTED_ARCHS, SUPPORTED_ARCHS_HELP +from .elf import ELF +from .freebsd_kernel import FreeBSDKernel, FreeBSDKernelError from .kernel import Kernel, KernelError, KernelArchError, KernelMultiABIError from .kernel import KernelVersionError, KernelWithoutSymbolsError from .log import log_setup, eprint +from .os_detect import detect_kernel_os from .output import output_syscalls from .utils import command_argv_to_string, command_available from .utils import gcc_version, git_checkout, maybe_rel, format_duration +from .utils import readelf_command from .version import VERSION, VERSION_HELP +FREEBSD_ARCHS_HELP = '''Supported FreeBSD architectures: + +amd64 AMD64 64-bit +arm64 ARM64 64-bit +''' + def sigint_handler(_, __): sys.stderr.write('Caught SIGINT, stopping\n') sys.exit(1) @@ -33,17 +43,20 @@ def parse_args() -> argparse.Namespace: ap = argparse.ArgumentParser( prog='systrack', usage='systrack [OPTIONS...] [VMLINUX]', - description='Analyze a Linux kernel image and extract information about implemented syscalls', + description='Analyze a kernel image and extract information about implemented syscalls', formatter_class=argparse.RawTextHelpFormatter ) ap.add_argument('vmlinux', metavar='VMLINUX', nargs='?', - help=wrap_help('path to vmlinux, if not inside KDIR or no KDIR supplied')) + help=wrap_help('path to kernel image, if not inside KDIR or no KDIR supplied')) ap.add_argument('-k', '--kdir', metavar='KDIR', help=wrap_help('kernel source directory')) ap.add_argument('-a', '--arch', metavar='ARCH', help=wrap_help('kernel architecture/ABI combination; pass "help" for a ' 'list (default: autodetect)')) + ap.add_argument('--os', metavar='OS', choices=('linux', 'freebsd'), + type=str.lower, help=wrap_help('kernel OS: linux or freebsd; if omitted ' + 'it will be auto-detected from the image')) ap.add_argument('-b', '--build', action='store_true', help=wrap_help('configure and build kernel and exit')) ap.add_argument('-c', '--config', action='store_true', @@ -127,26 +140,33 @@ def main() -> int: logging.debug('Systrack v%s', VERSION) logging.debug('Command line: systrack %s', command_argv_to_string(sys.argv[1:])) + os_name = args.os arch_name = args.arch if arch_name is not None: arch_name = arch_name.lower() - if arch_name not in SUPPORTED_ARCHS: - if arch_name not in ('help', '?'): - eprint(f'Unsupported architecture/ABI combination: {arch_name}') - eprint('See --arch HELP for a list') - return 1 - - eprint(SUPPORTED_ARCHS_HELP) + if arch_name in ('help', '?'): + if os_name == 'freebsd': + eprint(FREEBSD_ARCHS_HELP) + else: + eprint(SUPPORTED_ARCHS_HELP) return 0 + if os_name == 'freebsd' and (args.clean or args.config or args.build): + eprint('Building/configuring kernels is only supported for Linux images.') + return 1 + + if os_name == 'freebsd' and (args.checkout or args.cross or args.disable_opt): + eprint('Options related to building/checking out kernel sources are only supported for Linux images.') + return 1 + if not args.kdir and not args.vmlinux: - eprint('Need to specify a kernel source direcory and/or path to vmlinux') + eprint('Need to specify a kernel source direcory and/or path to a kernel image') eprint('See --help for more information') return 1 - if not args.kdir and (args.checkout or args.config or args.build): + if not args.kdir and (args.checkout or args.config or args.build or args.clean): eprint('Need to specify a kernel source direcory (--kdir)') return 1 @@ -161,12 +181,21 @@ def main() -> int: outdir = Path(args.out) if args.out else None rdir = Path(args.remap) if args.remap else None - # Checkout before building only if not set to auto - if args.checkout and args.checkout != 'auto': - eprint('Checking out to', args.checkout) - git_checkout(kdir, args.checkout) - if args.clean or args.config or args.build: + if os_name == 'freebsd': + eprint('Building/configuring kernels is only supported for Linux images.') + return 1 + + if arch_name and arch_name not in SUPPORTED_ARCHS: + eprint(f'Unsupported architecture/ABI combination: {arch_name}') + eprint('See --arch HELP for a list') + return 1 + + # Checkout before building only if not set to auto + if args.checkout and args.checkout != 'auto': + eprint('Checking out to', args.checkout) + git_checkout(kdir, args.checkout) + if args.out: out = Path(args.out) @@ -217,25 +246,68 @@ def main() -> int: return 0 - # Auto-checkout to the correct tag is only possible if we already have a - # vmlinux to extract the version from - if args.checkout == 'auto' and not vmlinux: - eprint('Cannot perform auto-checkout without a vmlinux image!') - return 1 - if not vmlinux: vmlinux = kdir / 'vmlinux' if not vmlinux.is_file(): - eprint(f'Unable to find vmlinux at "{vmlinux}".') + eprint(f'Unable to find kernel image at "{vmlinux}".') eprint('Build the kernel or provide a valid path.') return 1 - if not command_available('readelf'): + if readelf_command() is None: eprint('Command "readelf" unavailable, can\'t do much without it!') return 127 - kernel = instantiate_kernel(arch_name, vmlinux, kdir, outdir, rdir) + try: + elf = ELF(vmlinux) + except Exception as e: + eprint(f'Bad kernel ELF: {e}') + return 1 + + detected_os = detect_kernel_os(elf) + + if os_name is not None and detected_os is not None and detected_os != os_name: + eprint(f'Kernel image looks like {detected_os}, but --os {os_name} was specified.') + return 1 + + os_name = os_name or detected_os + if os_name is None: + eprint('Unable to detect kernel OS. Specify --os linux|freebsd.') + return 1 + + if os_name == 'freebsd': + if args.checkout or args.cross or args.disable_opt: + eprint('Options related to building/checking out kernel sources are only supported for Linux images.') + return 1 + + if arch_name and arch_name not in ('amd64', 'arm64'): + eprint(f'Unsupported FreeBSD architecture: {arch_name}') + eprint('See --os freebsd --arch HELP for a list') + return 1 + + try: + kernel = FreeBSDKernel(vmlinux, kdir=kdir, rdir=rdir, arch_name=arch_name) + except FreeBSDKernelError as e: + eprint(str(e)) + return 1 + else: + if arch_name and arch_name not in SUPPORTED_ARCHS: + eprint(f'Unsupported architecture/ABI combination: {arch_name}') + eprint('See --arch HELP for a list') + return 1 + + # Auto-checkout to the correct tag is only possible if we already have a + # vmlinux to extract the version from + if args.checkout == 'auto' and not args.vmlinux: + eprint('Cannot perform auto-checkout without a vmlinux image!') + return 1 + + # Checkout before analyzing only if not set to auto + if args.checkout and args.checkout != 'auto': + eprint('Checking out to', args.checkout) + git_checkout(kdir, args.checkout) + + kernel = instantiate_kernel(arch_name, vmlinux, kdir, outdir, rdir) eprint('Detected kernel version:', kernel.version_str) if args.checkout == 'auto': diff --git a/src/systrack/elf.py b/src/systrack/elf.py index 28862e1..2c8362f 100644 --- a/src/systrack/elf.py +++ b/src/systrack/elf.py @@ -8,7 +8,7 @@ from collections import namedtuple from typing import Union, Dict, Optional -from .utils import ensure_command +from .utils import ensure_command, readelf_command # Only EM_* macros relevant for vmlinux ELFs class E_MACHINE(IntEnum): @@ -89,7 +89,8 @@ def sections(self) -> Dict[str,Section]: # We actually only really care about SHT_PROGBITS or SHT_NOBITS exp = re.compile(r'\s([.\w]+)\s+(PROGBITS|NOBITS)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)') - out = ensure_command(['readelf', '-WS', self.path]) + readelf = readelf_command() or 'readelf' + out = ensure_command([readelf, '-WS', self.path]) secs = {} for match in exp.finditer(out): @@ -117,7 +118,8 @@ def has_debug_info(self) -> bool: def __extract_symbols(self): exp = re.compile(r'\d+:\s+([0-9a-fA-F]+)\s+(\d+)\s+(\w+).+\s+(\S+)$') - out = ensure_command(['readelf', '-Ws', self.path]).splitlines() + readelf = readelf_command() or 'readelf' + out = ensure_command([readelf, '-Ws', self.path]).splitlines() syms = {} funcs = {} diff --git a/src/systrack/freebsd_kernel.py b/src/systrack/freebsd_kernel.py new file mode 100644 index 0000000..91cea3d --- /dev/null +++ b/src/systrack/freebsd_kernel.py @@ -0,0 +1,227 @@ +import logging +import struct + +from pathlib import Path +from typing import List, Optional + +from .elf import ELF, E_MACHINE, Symbol +from .location import addr2line +from .syscall import Syscall +from .utils import addr2line_command, maybe_rel + + +class FreeBSDKernelError(RuntimeError): + pass + + +def freebsd_arch_from_vmlinux(vmlinux: ELF) -> str: + if vmlinux.e_machine == E_MACHINE.EM_X86_64: + return 'amd64' + if vmlinux.e_machine == E_MACHINE.EM_AARCH64: + return 'arm64' + + raise FreeBSDKernelError(f'Unsupported FreeBSD architecture: e_machine={vmlinux.e_machine}') + + +class FreeBSDArchInfo: + __slots__ = ( + 'name', 'bits32', + 'abi', 'compat', 'abi_bits32', + 'syscall_table_name', + 'syscall_num_reg', 'syscall_arg_regs', + '__base_arg_regs' + ) + + def __init__(self, name: str): + assert name in ('amd64', 'arm64'), f'Unexpected FreeBSD arch name: {name!r}' + + self.name = name + self.bits32 = False + self.abi = 'freebsd' + self.compat = False + self.abi_bits32 = False + self.syscall_table_name = 'sysent' + + if name == 'amd64': + self.syscall_num_reg = 'rax' + self.syscall_arg_regs = ['rdi', 'rsi', 'rdx', 'r10', 'r8', 'r9'] + else: + self.syscall_num_reg = 'x8' + self.syscall_arg_regs = ['x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7'] + + self.__base_arg_regs = len(self.syscall_arg_regs) + + def extend_arg_regs_to(self, n: int): + '''Ensure syscall_arg_regs has at least n entries by appending + stack{idx} placeholders. + ''' + while len(self.syscall_arg_regs) < n: + self.syscall_arg_regs.append(f'stack{len(self.syscall_arg_regs) - self.__base_arg_regs}') + + +class FreeBSDKernel: + os = 'freebsd' + __syscalls = None + + def __init__(self, vmlinux: Path, kdir: Optional[Path] = None, + rdir: Optional[Path] = None, arch_name: Optional[str] = None): + self.vmlinux = ELF(vmlinux) + self.kdir = kdir + self.rdir = rdir + + arch = self.__detect_arch() + if arch_name is not None: + arch_name = arch_name.lower() + if arch_name not in ('amd64', 'arm64'): + raise FreeBSDKernelError(f'Unsupported FreeBSD architecture: {arch_name}') + if arch_name != arch: + raise FreeBSDKernelError(f'Selected arch "{arch_name}" does not match vmlinux ({arch})') + + arch = arch_name + + self.arch = FreeBSDArchInfo(arch) + + self.__version_tag, self.__version_source = self.__extract_version() + + def __detect_arch(self) -> str: + return freebsd_arch_from_vmlinux(self.vmlinux) + + def __extract_version(self): + vers_sym = self.vmlinux.symbols.get('version') + if vers_sym is not None: + try: + s = self.vmlinux.vaddr_read_string(vers_sym.vaddr) + except Exception: + s = '' + + if s.startswith('FreeBSD '): + parts = s.split() + if len(parts) >= 2: + return parts[1], 'version' + + return 'unknown', 'elf' + + @property + def version_tag(self) -> str: + return self.__version_tag + + @property + def version_source(self) -> str: + return self.__version_source + + @property + def version_str(self) -> str: + return f'{self.version_tag} (from {self.version_source})' + + @property + def can_extract_location_info(self) -> bool: + return self.vmlinux.has_debug_info + + @property + def syscalls(self) -> List[Syscall]: + if self.__syscalls is None: + self.__syscalls = self.__extract_syscalls() + return self.__syscalls + + def __extract_syscalls(self) -> List[Syscall]: + syms = self.vmlinux.symbols + syscallnames_sym = syms.get('syscallnames') + sysent_sym = syms.get('sysent') + + if syscallnames_sym is None or sysent_sym is None: + raise FreeBSDKernelError('Missing required symbols ("sysent" and/or "syscallnames")') + + ptr_sz = 4 if self.vmlinux.bits32 else 8 + ptr_fmt = '<>'[self.vmlinux.big_endian] + 'QL'[self.vmlinux.bits32] + + if syscallnames_sym.size <= 0 or syscallnames_sym.size % ptr_sz: + raise FreeBSDKernelError('Bad syscallnames size') + + n = syscallnames_sym.size // ptr_sz + names_data = self.vmlinux.vaddr_read(syscallnames_sym.vaddr, syscallnames_sym.size) + name_ptrs = map(lambda u: u[0], struct.iter_unpack(ptr_fmt, names_data)) + names = list(map(lambda p: self.vmlinux.vaddr_read_string(p).strip(), name_ptrs)) + + if sysent_sym.size <= 0 or sysent_sym.size % n: + raise FreeBSDKernelError('Bad sysent size') + + ent_sz = sysent_sym.size // n + if ent_sz < ptr_sz * 2 + 1: + raise FreeBSDKernelError('Unsupported FreeBSD sysent layout') + + text = self.vmlinux.sections.get('.text') + if not text: + raise FreeBSDKernelError('vmlinux has no .text section') + + text_vstart = text.vaddr + text_vend = text_vstart + text.size + + unimpl_vaddrs = set() + for name in ('nosys', 'lkmnosys', 'sys_nosys'): + sym = syms.get(name) + if sym is not None: + unimpl_vaddrs.add(sym.vaddr) + unimpl_vaddrs.add(sym.real_vaddr) + + sysent_data = self.vmlinux.vaddr_read(sysent_sym.vaddr, sysent_sym.size) + + calls = [] + nargs = [] + invalid = 0 + + for i in range(n): + off = i * ent_sz + call = struct.unpack(ptr_fmt, sysent_data[off:off + ptr_sz])[0] + narg = sysent_data[off + ptr_sz * 2] + calls.append(call) + nargs.append(narg) + + if call == 0 or not (text_vstart <= call < text_vend): + invalid += 1 + + if n > 0 and invalid / n > 0.25: + raise FreeBSDKernelError( + 'sysent entries do not point into .text; relocation entries/PIE kernels ' + 'are not supported yet (or sysent layout is unsupported)' + ) + + symbols_by_vaddr = {sym.vaddr: sym for sym in syms.values()} + syscalls = [] + max_narg = 0 + + for i, (name, call, narg) in enumerate(zip(names, calls, nargs)): + max_narg = max(max_narg, narg) + + if call == 0 or call in unimpl_vaddrs: + continue + + sym = symbols_by_vaddr.get(call) + if sym is None: + sym = Symbol(call, call, 0, 'FUNC', f'{call:#x}') + + sig = [f'? arg{j}' for j in range(1, narg + 1)] + syscalls.append(Syscall(i, i, name, name, sym, None, signature=sig)) + + self.arch.extend_arg_regs_to(max_narg) + + if self.can_extract_location_info and addr2line_command() is not None: + addrs = list(map(lambda s: s.symbol.real_vaddr, syscalls)) + locs = list(addr2line(self.vmlinux.path, addrs)) + + if self.kdir: + if self.rdir: + remap = lambda p: self.kdir / maybe_rel(p, self.rdir) if p is not None else None + else: + remap = lambda p: self.kdir / p if p is not None else None + else: + remap = lambda p: p + + for sc, (file, line) in zip(syscalls, locs): + file = remap(file) + sc.file = file + sc.line = line + sc.good_location = file is not None and line is not None + else: + logging.debug('No DWARF/addr2line available, skipping FreeBSD location extraction') + + return syscalls diff --git a/src/systrack/kernel.py b/src/systrack/kernel.py index 0cd1042..f1ee76f 100644 --- a/src/systrack/kernel.py +++ b/src/systrack/kernel.py @@ -2,9 +2,9 @@ import logging import struct import atexit +import os from pathlib import Path from time import monotonic -from os import sched_getaffinity from operator import itemgetter, attrgetter from collections import defaultdict, Counter from typing import Tuple, List, Dict, Iterable, Iterator, Union, Any, Optional @@ -648,7 +648,10 @@ def __edit_config_with_deps(self, options: Dict[str,List[str]]): kconfig_check_with_deps(config_file, self.kdir, options) def make(self, target: str, stdin=None, ensure=True) -> int: - j = max(len(sched_getaffinity(0)) - 1, 1) + try: + j = max(len(os.sched_getaffinity(0)) - 1, 1) + except AttributeError: + j = max((os.cpu_count() or 1) - 1, 1) cmd = ['make', f'-j{j}', f'ARCH={self.arch.name}'] # Generate debug info with relative paths to make our life easier for diff --git a/src/systrack/location.py b/src/systrack/location.py index a1ea223..cedbf65 100644 --- a/src/systrack/location.py +++ b/src/systrack/location.py @@ -9,10 +9,11 @@ from .arch import Arch from .elf import ELF from .syscall import Syscall -from .utils import ensure_command, command_available, maybe_rel +from .utils import ensure_command, command_available, maybe_rel, addr2line_command def addr2line(elf: Path, addrs: Iterable[int]) -> Iterator[Tuple[Optional[Path],Optional[int]]]: - out = ensure_command(['addr2line', '-e', elf, *map(hex, addrs)]) + addr2line = addr2line_command() or 'addr2line' + out = ensure_command([addr2line, '-e', elf, *map(hex, addrs)]) for file, line in map(lambda d: d.split(':'), out.splitlines()): if file == '??': @@ -236,7 +237,7 @@ def adjust_line(file: Path, line: int, sc: Syscall) -> int: def extract_syscall_locations(syscalls: List[Syscall], vmlinux: ELF, arch: Arch, kdir: Optional[Path], rdir: Optional[Path]): - if not command_available('addr2line'): + if addr2line_command() is None: logging.warning('Command "addr2line" unavailable, skipping location info extraction') return diff --git a/src/systrack/os_detect.py b/src/systrack/os_detect.py new file mode 100644 index 0000000..a2767c5 --- /dev/null +++ b/src/systrack/os_detect.py @@ -0,0 +1,20 @@ +from typing import Optional + +from .elf import ELF + + +def detect_kernel_os(elf: ELF) -> Optional[str]: + '''Try to detect the kernel OS from ELF symbols. + + Returns one of: "linux", "freebsd", or None if unknown. + ''' + syms = elf.symbols + + if 'linux_banner' in syms: + return 'linux' + + if 'sysent' in syms and 'syscallnames' in syms: + return 'freebsd' + + return None + diff --git a/src/systrack/output.py b/src/systrack/output.py index 58b4e62..106fe9f 100644 --- a/src/systrack/output.py +++ b/src/systrack/output.py @@ -74,9 +74,11 @@ def output_syscalls_text(syscalls: Iterable[Syscall], spacing: int = 2): sys.stdout.flush() def output_syscalls_json(kernel: Kernel): + os_name = getattr(kernel, 'os', 'linux') data = { 'systrack_version': VERSION, 'kernel': { + 'os': os_name, 'version': kernel.version_tag, 'version_source': kernel.version_source, 'architecture': { @@ -97,6 +99,9 @@ def output_syscalls_json(kernel: Kernel): 'syscalls': kernel.syscalls } + if os_name == 'freebsd': + data['kernel']['syscall_names_symbol'] = 'syscallnames' + dump(data, sys.stdout, cls=SyscallJSONEncoder, sort_keys=True, indent='\t') def output_syscalls_html(kernel: Kernel): @@ -109,9 +114,11 @@ def output_syscalls_html(kernel: Kernel): env = Environment(loader=PackageLoader('systrack'), line_statement_prefix='#', autoescape=True) template = env.get_template('syscall_table.html') - max_args = max(len(s.signature) for s in kernel.syscalls if s.signature is not None) + max_args = max((len(s.signature) for s in kernel.syscalls if s.signature is not None), default=0) + os_name = getattr(kernel, 'os', 'linux') template.stream( + os_name=os_name, kernel_version_tag=kernel.version_tag, arch=kernel.arch.name, bits=32 if kernel.arch.bits32 else 64, diff --git a/src/systrack/templates/syscall_table.html b/src/systrack/templates/syscall_table.html index 7bf0d11..8f3c40b 100644 --- a/src/systrack/templates/syscall_table.html +++ b/src/systrack/templates/syscall_table.html @@ -1,13 +1,13 @@ - Linux {{kernel_version_tag}} {{arch}} {{bits}}-bit, {{'compat ' if compat else ''}}{{abi_bits}}-bit {{abi}} syscall table + {{'FreeBSD' if os_name == 'freebsd' else (os_name | title)}} {{kernel_version_tag}} {{arch}} {{bits}}-bit, {{'compat ' if compat else ''}}{{abi_bits}}-bit {{abi}} syscall table -

Linux {{kernel_version_tag}} syscall table

+

{{'FreeBSD' if os_name == 'freebsd' else (os_name | title)}} {{kernel_version_tag}} syscall table

Architecture: {{arch}} {{bits}}-bit

ABI: {{'compat ' if compat else ''}}{{abi_bits}}-bit {{abi}}

@@ -39,7 +39,13 @@

ABI: {{'compat ' if compat else ''}}{{abi_bits}}-bit {{abi}}

# if sc.file.is_absolute() {{sc.file}}:{{sc.line}} # else + # if os_name == 'linux' {{sc.file}}:{{sc.line}} + # elif os_name == 'freebsd' + {{sc.file}}:{{sc.line}} + # else + {{sc.file}}:{{sc.line}} + # endif # endif # elif sc.file @@ -47,7 +53,13 @@

ABI: {{'compat ' if compat else ''}}{{abi_bits}}-bit {{abi}}

# if sc.file.is_absolute() {{sc.file}}:?? # else + # if os_name == 'linux' {{sc.file}}:?? + # elif os_name == 'freebsd' + {{sc.file}}:?? + # else + {{sc.file}}:?? + # endif # endif # else diff --git a/src/systrack/utils.py b/src/systrack/utils.py index 48db6ae..ec196d2 100644 --- a/src/systrack/utils.py +++ b/src/systrack/utils.py @@ -1,7 +1,9 @@ +import os import sys import logging from collections import defaultdict +from functools import lru_cache from pathlib import Path from shlex import join as shlex_join from shutil import which @@ -264,6 +266,44 @@ def command_available(name: AnyStr) -> bool: ''' return which(name) is not None +def _find_suffixed_command(suffix: str) -> Optional[str]: + suffix = '-' + suffix + + for dir_ in os.environ.get('PATH', '').split(os.pathsep): + if not dir_: + continue + + try: + for entry in sorted(os.listdir(dir_)): + if not entry.endswith(suffix): + continue + + path = Path(dir_) / entry + if path.is_file() and os.access(path, os.X_OK): + return str(path) + except (FileNotFoundError, NotADirectoryError, PermissionError): + continue + + return None + +def _binutils_command(name: str) -> Optional[str]: + for cand in (name, 'g' + name, 'llvm-' + name): + path = which(cand) + if path is not None: + return path + + return _find_suffixed_command(name) + +@lru_cache(maxsize=None) +def readelf_command() -> Optional[str]: + '''Find a suitable readelf command (GNU or LLVM).''' + return _binutils_command('readelf') + +@lru_cache(maxsize=None) +def addr2line_command() -> Optional[str]: + '''Find a suitable addr2line command (GNU or LLVM).''' + return _binutils_command('addr2line') + def gcc_version(gcc_cmd: AnyStr) -> str: '''Run GCC to get its version and return it as a string. Execution will be aborted if the given GCC command is not found. diff --git a/tests/data/Makefile b/tests/data/Makefile index f6f177d..eca6600 100644 --- a/tests/data/Makefile +++ b/tests/data/Makefile @@ -1,6 +1,13 @@ ASMS = $(wildcard *.s) BINS = $(ASMS:.s=) +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) +# On macOS the default toolchain produces Mach-O binaries. Systrack tests need +# ELF binaries, so build x86_64 Linux ELFs using clang + lld. +ELF_CLANG_FLAGS = --target=x86_64-unknown-linux-gnu -fuse-ld=lld -no-pie +endif + .PHONY: all clean all: $(BINS) @@ -8,7 +15,7 @@ all: $(BINS) # OTOH clang is generates a .o w/o relocations for call insns, might consider # using it in the future. %: %.s - $(CC) -ffreestanding -nostdlib -o $@ $@.s + $(CC) $(ELF_CLANG_FLAGS) -ffreestanding -nostdlib -o $@ $@.s clean: rm -f $(BINS) diff --git a/tests/data/freebsd_sysent.s b/tests/data/freebsd_sysent.s new file mode 100644 index 0000000..a95e958 --- /dev/null +++ b/tests/data/freebsd_sysent.s @@ -0,0 +1,82 @@ +.section .text + +.globl nosys +.type nosys @function +nosys: + xor %eax, %eax + ret +.size nosys, . - nosys + +.globl sys_nosys +.type sys_nosys @function +sys_nosys: + xor %eax, %eax + ret +.size sys_nosys, . - sys_nosys + +.globl sys_foo +.type sys_foo @function +sys_foo: + xor %eax, %eax + ret +.size sys_foo, . - sys_foo + +.globl sys_bar +.type sys_bar @function +sys_bar: + xor %eax, %eax + ret +.size sys_bar, . - sys_bar + + +.section .rodata + +.globl version +.type version @object +version: + .asciz "FreeBSD 13.2-RELEASE-p1 #0: test" +.size version, . - version + +str_nosys: + .asciz "nosys" +str_foo: + .asciz "foo" +str_bar: + .asciz "bar" +str_baz: + .asciz "baz" + +.globl syscallnames +.type syscallnames @object +syscallnames: + .quad str_nosys + .quad str_foo + .quad str_bar + .quad str_baz +.size syscallnames, . - syscallnames + +.globl sysent +.type sysent @object +sysent: + /* #0: unimplemented -> nosys, narg=0 */ + .quad nosys + .quad 0 + .byte 0 + .zero 7 + /* #1: foo, narg=1 */ + .quad sys_foo + .quad 0 + .byte 1 + .zero 7 + /* #2: bar, narg=2 */ + .quad sys_bar + .quad 0 + .byte 2 + .zero 7 + /* #3: unimplemented -> sys_nosys, narg=0 */ + .quad sys_nosys + .quad 0 + .byte 0 + .zero 7 +.size sysent, . - sysent + diff --git a/tests/test_freebsd.py b/tests/test_freebsd.py new file mode 100644 index 0000000..af81de2 --- /dev/null +++ b/tests/test_freebsd.py @@ -0,0 +1,55 @@ +import io + +from contextlib import redirect_stdout + +from systrack.elf import E_MACHINE, ELF +from systrack.freebsd_kernel import FreeBSDArchInfo, FreeBSDKernel, freebsd_arch_from_vmlinux +from systrack.os_detect import detect_kernel_os +from systrack.output import output_syscalls_html, output_syscalls_json + +from .utils import * + + +def test_freebsd_os_detection(): + elf = ELF(make_test_elf('freebsd_sysent')) + assert detect_kernel_os(elf) == 'freebsd' + + +def test_freebsd_syscall_extraction(): + kernel = FreeBSDKernel(make_test_elf('freebsd_sysent')) + syscalls = kernel.syscalls + + # Should filter out nosys/sys_nosys entries + assert [sc.name for sc in syscalls] == ['foo', 'bar'] + assert [sc.number for sc in syscalls] == [1, 2] + + # Placeholder signatures based on sy_narg + assert [len(sc.signature) for sc in syscalls] == [1, 2] + + assert kernel.arch.name == 'amd64' + + +def test_freebsd_html_and_json_dont_crash(): + kernel = FreeBSDKernel(make_test_elf('freebsd_sysent')) + + out = io.StringIO() + with redirect_stdout(out): + output_syscalls_json(kernel) + assert out.getvalue().strip() + + out = io.StringIO() + with redirect_stdout(out): + output_syscalls_html(kernel) + assert out.getvalue().strip() + + +def test_freebsd_arm64_archinfo(): + class Dummy: + e_machine = E_MACHINE.EM_AARCH64 + + assert freebsd_arch_from_vmlinux(Dummy()) == 'arm64' + + arch = FreeBSDArchInfo('arm64') + assert arch.syscall_num_reg == 'x8' + assert arch.syscall_arg_regs[:8] == ['x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7'] + diff --git a/tests/test_x86.py b/tests/test_x86.py index e77ff4e..0094672 100644 --- a/tests/test_x86.py +++ b/tests/test_x86.py @@ -1,3 +1,10 @@ +import sys + +import pytest + +if sys.platform != 'linux': + pytest.skip('x86 syscall handler extraction test requires GNU toolchain', allow_module_level=True) + from systrack.arch import ArchX86 from systrack.elf import ELF