#!/usr/bin/env python3 """Resolve a guest crash stack from a core dump, which nothing else here can do. A guest process is `mldr` with Mach-O images mapped into it, so systemd-coredump and gdb both print `n/a` for every frame: they look for ELF modules and there are none at those addresses. The mapping information IS in the core, in the NT_FILE note that the kernel writes for every file-backed mapping, and mldr maps its images from files. This reads that note, turns each stack address into a file plus an offset, and asks llvm-symbolizer for a name. THE PATHS IN THE NOTE ARE GUEST PATHS. mldr opens its images through the container's view, so the note records /Applications/... and /usr/lib/..., which do not exist on the host. Pass one --root per tree to search: the prefix and the runtime libexec directory between them cover everything a guest maps. Usage: scripts/core-guest-stack.py [--root DIR]... [--threads] [
...] --threads lists EVERY thread and where it stopped, which is the question systemd-coredump cannot answer: it prints one thread, and a crash in any other is invisible. With no addresses it reads them from stdin, one per line, which is what pasting a `coredumpctl info` stack does. WHY THE OFFSET IS NOT ADDRESS MINUS START. NT_FILE records the file offset of each mapping, so the offset within the file is `addr - start + file_ofs`. Getting that wrong yields plausible symbol names from the wrong part of the binary, which is worse than none. """ import struct import subprocess import sys NT_FILE = 0x46494C45 NT_PRSTATUS = 1 # elf_prstatus on x86-64: the register block starts at 112, and rip is register 16 of the 27 in # user_regs_struct. Spelled out because an off-by-one here yields a plausible wrong address. PRSTATUS_REG_OFFSET = 112 RIP_INDEX = 16 RSP_INDEX = 19 _CORE_CACHE = {} def _core_bytes(path): """The core, read once. These are hundreds of megabytes and both note walks want them.""" if path not in _CORE_CACHE: with open(path, "rb") as f: _CORE_CACHE[path] = f.read() return _CORE_CACHE[path] def read_nt_file(path): """[(start, end, file_offset, filename)] from the core's NT_FILE note.""" data = _core_bytes(path) if data[:4] != b"\x7fELF" or data[4] != 2: raise SystemExit(f"{path}: not a 64-bit ELF core") e_phoff, = struct.unpack_from(" rsp={hex(rsp)}") continue start, _stop, file_ofs, name = hit offset = rip - start + file_ofs host = resolve_host_path(name, roots) sym = symbolize(host, offset) if host else None print(f"tid {pid} {name.rsplit('/', 1)[-1]}+{hex(offset)} {sym or '??'}") return if not maps: raise SystemExit("no NT_FILE note in the core, so nothing can be resolved") print(f"{len(maps)} file-backed mappings in the core", file=sys.stderr) for a in addrs: addr = int(a, 16) hit = next((m for m in maps if m[0] <= addr < m[1]), None) if not hit: print(f"{a} ") continue start, _stop, file_ofs, name = hit offset = addr - start + file_ofs host = resolve_host_path(name, roots) sym = symbolize(host, offset) if host else None short = name.rsplit("/", 1)[-1] note = "" if host else " " print(f"{a} {short}+{hex(offset)} {sym or '??'}{note}") if __name__ == "__main__": main()