From fb94e4bb71cc9d47a8267149ec700819074f490a Mon Sep 17 00:00:00 2001 From: Yan Date: Sun, 9 Aug 2026 23:13:37 +0000 Subject: [PATCH 1/3] Read core notes in the namespace that names them ELF note types are namespaced by the note name: type 1 is a Linux struct elf_prstatus only inside the CORE namespace that Linux uses. ELFCore decoded every NT_PRSTATUS with the Linux layout regardless, so a FreeBSD core raised struct.error when its shorter descriptor ran out, and a NetBSD one was read as a register block when it actually holds a struct netbsd_elfcore_procinfo, which carries no registers at all - those live in a per-LWP note typed with the number of the PT_GETREGS ptrace request. Cores whose process ABI is not the one their ELF header implies, an x32 process dumped by an x86-64 kernel for instance, tripped an assert that python -O strips, leaving registers read at the wrong stride behind. Dispatch on the note name, read FreeBSD struct prstatus and NetBSD per-LWP register notes, and check each descriptor against the layout it is about to be read with. Linux writes the GDT entry note under LINUX rather than under CORE, so look for it there. A thread whose registers cannot be decoded is now dropped with a warning instead of taking the whole load down with it. Co-Authored-By: Claude Opus 5 --- cle/backends/elf/elfcore.py | 587 +++++++++++++++++++++++--------- cle/backends/tls/elfcore_tls.py | 5 +- tests/test_elfcore.py | 337 ++++++++++++++++++ 3 files changed, 772 insertions(+), 157 deletions(-) diff --git a/cle/backends/elf/elfcore.py b/cle/backends/elf/elfcore.py index dd83731c1..dcf27cfed 100644 --- a/cle/backends/elf/elfcore.py +++ b/cle/backends/elf/elfcore.py @@ -4,6 +4,7 @@ import os import struct from collections import defaultdict +from typing import NamedTuple import elftools @@ -20,6 +21,44 @@ # TODO: yall know struct.unpack_from exists, right? maybe even bitstream? +# Core note types are namespaced by the note's name, so a type number only identifies a struct +# together with the name of the kernel that wrote it. +CORE_NAMESPACE = "CORE" +# Linux writes the general purpose and floating point register sets under CORE and every other +# register set, the GDT entries among them, under its own name. +LINUX_NAMESPACE = "LINUX" +FREEBSD_NAMESPACE = "FreeBSD" +# NetBSD names its process-wide note "NetBSD-CORE" and its per-LWP notes "NetBSD-CORE@". +NETBSD_NAMESPACE_PREFIX = "NetBSD-CORE" + +# The type of the Linux note holding a thread's GDT entries, in the LINUX namespace. +NT_386_TLS = 512 + + +class RegisterSet(NamedTuple): + """ + The layout of a general purpose register set as a kernel writes it into a core note: a struct + format string without a byte order prefix, and the name of the register each of its fields + holds. Fields that are not registers angr knows about are named "xxx". + """ + + fmt: str + names: tuple[str, ...] + + @property + def size(self): + return struct.calcsize("<" + self.fmt) + + +class NetBSDRegisters(NamedTuple): + """ + NetBSD's per-LWP register note: the type NetBSD gives the note, and the layout of the struct reg + it carries. + """ + + note_type: int + gregset: RegisterSet + class ELFCore(ELF): """ @@ -92,10 +131,16 @@ def threads(self): return list(range(len(self._threads))) def thread_registers(self, thread=None): + if not self._threads: + return {} if thread is None: thread = 0 return self._threads[thread]["registers"] + @staticmethod + def __note_desc(note): + return note.n_desc.encode("latin-1") if isinstance(note.n_desc, str) else note.n_desc + def __extract_note_info(self): """ All meaningful information about the process's state at crashtime is stored in the note segment. @@ -103,24 +148,43 @@ def __extract_note_info(self): for seg_readelf in self._reader.iter_segments(): if seg_readelf.header.p_type == "PT_NOTE": for note in seg_readelf.iter_notes(): - if note.n_type == "NT_PRSTATUS": - self.__cycle_thread() - n_desc = note.n_desc.encode("latin-1") if isinstance(note.n_desc, str) else note.n_desc - self.__parse_prstatus(n_desc) - elif note.n_type == "NT_PRPSINFO": - self.__parse_prpsinfo(note.n_desc) - elif note.n_type == "NT_AUXV": - n_desc = note.n_desc.encode("latin-1") if isinstance(note.n_desc, str) else note.n_desc - self.__parse_auxv(n_desc) - elif note.n_type == "NT_FILE": - self.__parse_files(note.n_desc) - elif note.n_type == 512 and self.arch.name == "X86": - n_desc = note.n_desc.encode("latin-1") if isinstance(note.n_desc, str) else note.n_desc - self.__parse_x86_tls(n_desc) + if note.n_name == CORE_NAMESPACE: + if note.n_type == "NT_PRSTATUS": + self.__cycle_thread() + self.__parse_prstatus(self.__note_desc(note)) + elif note.n_type == "NT_PRPSINFO": + self.__parse_prpsinfo(note.n_desc) + elif note.n_type == "NT_AUXV": + self.__parse_auxv(self.__note_desc(note)) + elif note.n_type == "NT_FILE": + self.__parse_files(note.n_desc) + elif note.n_name == LINUX_NAMESPACE: + if note.n_type == NT_386_TLS and self.arch.name == "X86": + self.__parse_x86_tls(self.__note_desc(note)) + elif note.n_name == FREEBSD_NAMESPACE: + if note.n_type == "NT_PRSTATUS": + self.__cycle_thread() + self.__parse_freebsd_prstatus(self.__note_desc(note)) + elif note.n_name is not None and note.n_name.startswith(NETBSD_NAMESPACE_PREFIX): + # NetBSD's NT_PRSTATUS is a struct netbsd_elfcore_procinfo, which holds no registers + # at all - those live in one PT_GETREGS note per LWP. + netbsd = netbsd_register_sets.get(self.arch.name) + if netbsd is None: + raise CLECompatibilityError(f"Architecture '{self.arch.name}' unsupported by ELFCore") + if note.n_type == netbsd.note_type: + self.__cycle_thread() + self.__parse_netbsd_regs(self.__note_desc(note), netbsd.gregset) + elif note.n_type == "NT_PRSTATUS": + log.warning("Don't know how to read the registers of a core note namespaced '%s'", note.n_name) self._replace_main_object_path() self.__cycle_thread() + unparsed = sum(1 for thread in self._threads if "registers" not in thread) + if unparsed: + log.warning("Discarding %d thread(s) whose registers could not be parsed", unparsed) + self._threads = [thread for thread in self._threads if "registers" in thread] + if not self._threads: log.warning("Could not find thread info, cannot initialize registers") elif self.arch.name == "X86" and "segments" not in self._threads[0]: @@ -174,10 +238,10 @@ def __dummy_clemory(self): def __parse_prstatus(self, desc): """ - Parse out the prstatus, accumulating the general purpose register values. + Parse out a Linux struct elf_prstatus, accumulating the general purpose register values. Supports AMD64, X86, ARM, AArch64, MIPS and MIPSEL at the moment. - :param prstatus: a note object of type NT_PRSTATUS. + :param desc: the descriptor of a note of type NT_PRSTATUS in the CORE namespace. """ # TODO: support all architectures angr supports @@ -189,8 +253,27 @@ def __parse_prstatus(self, desc): else: raise CLEError("Architecture must have a bitwidth of either 64 or 32") + rnames = linux_register_names.get(self.arch.name) + if rnames is None: + raise CLECompatibilityError(f"Architecture '{self.arch.name}' unsupported by ELFCore") + end = ">" if self.arch.memory_endness == "Iend_BE" else "<" + # three ints and a short padded to an int, then two longs, four ints and four timevals, + # then the registers and pr_fpvalid, and finally padding out to the struct's alignment + prologue_size = 32 + 10 * arch_bytes + struct_size = prologue_size + len(rnames) * arch_bytes + 4 + if not struct_size <= len(desc) < struct_size + arch_bytes: + # the process ABI is not the one the core's ELF header implies - an x32 process dumped by an + # x86-64 kernel, say, or a 32-bit MIPS process dumped by a 64-bit one + log.warning( + "Skipping a thread whose NT_PRSTATUS is %d bytes; a %s struct elf_prstatus is %d", + len(desc), + self.arch.name, + struct_size, + ) + return + pos = 0 def read_longs(n): @@ -222,151 +305,84 @@ def read_timeval(): pos, result["pr_cutime_usec"] = read_timeval() pos, result["pr_cstime_usec"] = read_timeval() - # parse out general purpose registers - if self.arch.name == "AMD64": - # register names as they appear in dump - rnames = [ - "r15", - "r14", - "r13", - "r12", - "rbp", - "rbx", - "r11", - "r10", - "r9", - "r8", - "rax", - "rcx", - "rdx", - "rsi", - "rdi", - "xxx", - "rip", - "cs", - "eflags", - "rsp", - "ss", - "fs_base", - "gs_base", - "ds", - "es", - "xxx", - "xxx", - ] - nreg = 27 - elif self.arch.name == "X86": - rnames = [ - "ebx", - "ecx", - "edx", - "esi", - "edi", - "ebp", - "eax", - "ds", - "es", - "fs", - "gs", - "xxx", - "eip", - "cs", - "eflags", - "esp", - "ss", - ] - nreg = 17 - elif self.arch.name == "ARMHF" or self.arch.name == "ARMEL": - rnames = [ - "r0", - "r1", - "r2", - "r3", - "r4", - "r5", - "r6", - "r7", - "r8", - "r9", - "r10", - "r11", - "r12", - "r13", - "r14", - "r15", - "xxx", - "xxx", - ] - nreg = 18 - elif self.arch.name == "AARCH64": - rnames = [f"x{i}" for i in range(32)] - rnames.append("pc") - rnames.append("xxx") - nreg = 34 - elif self.arch.name == "MIPS32": - rnames = [ - "xxx", - "xxx", - "xxx", - "xxx", - "xxx", - "xxx", - "zero", - "at", - "v0", - "v1", - "a0", - "a1", - "a2", - "a3", - "t0", - "t1", - "t2", - "t3", - "t4", - "t5", - "t6", - "t7", - "s0", - "s1", - "s2", - "s3", - "s4", - "s5", - "s6", - "s7", - "t8", - "t9", - "k0", - "k1", - "gp", - "sp", - "s8", - "ra", - "lo", - "hi", - "pc", - "bad", - "sr", - "status", - "cause", - ] - nreg = 45 - else: - raise CLECompatibilityError(f"Architecture '{self.arch.name}' unsupported by ELFCore") - - assert nreg == len(rnames), "Please create an issue with this core-file attached to get this fixed." - pos, *regvals = read_longs(nreg) - result["registers"] = dict(zip(rnames, regvals)) - del result["registers"]["xxx"] + pos, *regvals = read_longs(len(rnames)) + result["registers"] = dict(zip(rnames, regvals, strict=True)) + result["registers"].pop("xxx", None) pos, result["pr_fpvalid"] = read_ints(1) - assert ( - pos <= len(desc) < pos + arch_bytes - ), "Please create an issue with this core-file attached to get this fixed." self.__current_thread.update(result) + def __parse_freebsd_prstatus(self, desc): + """ + Parse out a FreeBSD struct prstatus, accumulating the general purpose register values. It shares no + prefix with the Linux struct of the same note type, but it is self-describing: its header carries + the size of the whole struct and of the register set that follows it. + + :param desc: the descriptor of a note of type NT_PRSTATUS in the FreeBSD namespace. + """ + + end = ">" if self.arch.memory_endness == "Iend_BE" else "<" + header_fmt = end + ("i4xQQQiii4x" if self.arch.bytes == 8 else "iIIIiii") + header_size = struct.calcsize(header_fmt) + + gregset = freebsd_register_sets.get(self.arch.name) + if gregset is None: + raise CLECompatibilityError(f"Architecture '{self.arch.name}' unsupported by ELFCore") + + if len(desc) < header_size + gregset.size: + log.warning( + "Skipping a thread whose FreeBSD NT_PRSTATUS is %d bytes; a %s one is %d", + len(desc), + self.arch.name, + header_size + gregset.size, + ) + return + + pr_version, pr_statussz, pr_gregsetsz, _, _, pr_cursig, pr_pid = struct.unpack_from(header_fmt, desc) + + if pr_version != 1 or pr_statussz != len(desc) or pr_gregsetsz != gregset.size: + log.warning( + "Skipping a thread whose FreeBSD NT_PRSTATUS describes itself as version %d, %d bytes with a " + "%d byte register set; expected version 1, %d bytes and %d", + pr_version, + pr_statussz, + pr_gregsetsz, + len(desc), + gregset.size, + ) + return + + registers = dict(zip(gregset.names, struct.unpack_from(end + gregset.fmt, desc, header_size), strict=True)) + registers.pop("xxx", None) + + self.__current_thread.update({"pr_cursig": pr_cursig, "pr_pid": pr_pid, "registers": registers}) + + def __parse_netbsd_regs(self, desc, gregset): + """ + Parse out a NetBSD struct reg, accumulating the general purpose register values. NetBSD writes one + of these per LWP, in a note named "NetBSD-CORE@"; its NT_PRSTATUS is a struct + netbsd_elfcore_procinfo, which holds no registers at all. + + :param desc: the descriptor of a PT_GETREGS note in a NetBSD-CORE namespace. + :param gregset: the layout of this architecture's struct reg. + """ + + if len(desc) != gregset.size: + log.warning( + "Skipping a thread whose NetBSD PT_GETREGS note is %d bytes; a %s struct reg is %d", + len(desc), + self.arch.name, + gregset.size, + ) + return + + end = ">" if self.arch.memory_endness == "Iend_BE" else "<" + registers = dict(zip(gregset.names, struct.unpack(end + gregset.fmt, desc), strict=True)) + registers.pop("xxx", None) + + self.__current_thread.update({"registers": registers}) + def __parse_prpsinfo(self, desc): pr_fname = desc.pr_fname.split(b"\x00", 1)[0] try: @@ -602,6 +618,265 @@ def __record_main_object(self): self._main_object = self +# ARMEL and ARMHF share a register set. +arm_register_names = ( + "r0", + "r1", + "r2", + "r3", + "r4", + "r5", + "r6", + "r7", + "r8", + "r9", + "r10", + "r11", + "r12", + "r13", + "r14", + "r15", + "xxx", + "xxx", +) + +# The names of the slots of a Linux elf_gregset_t, in the order the kernel dumps them. +linux_register_names = { + "AMD64": ( + "r15", + "r14", + "r13", + "r12", + "rbp", + "rbx", + "r11", + "r10", + "r9", + "r8", + "rax", + "rcx", + "rdx", + "rsi", + "rdi", + "xxx", + "rip", + "cs", + "eflags", + "rsp", + "ss", + "fs_base", + "gs_base", + "ds", + "es", + "xxx", + "xxx", + ), + "X86": ( + "ebx", + "ecx", + "edx", + "esi", + "edi", + "ebp", + "eax", + "ds", + "es", + "fs", + "gs", + "xxx", + "eip", + "cs", + "eflags", + "esp", + "ss", + ), + "ARMEL": arm_register_names, + "ARMHF": arm_register_names, + "AARCH64": (*(f"x{i}" for i in range(32)), "pc", "xxx"), + "MIPS32": ( + "xxx", + "xxx", + "xxx", + "xxx", + "xxx", + "xxx", + "zero", + "at", + "v0", + "v1", + "a0", + "a1", + "a2", + "a3", + "t0", + "t1", + "t2", + "t3", + "t4", + "t5", + "t6", + "t7", + "s0", + "s1", + "s2", + "s3", + "s4", + "s5", + "s6", + "s7", + "t8", + "t9", + "k0", + "k1", + "gp", + "sp", + "s8", + "ra", + "lo", + "hi", + "pc", + "bad", + "sr", + "status", + "cause", + ), +} + +# FreeBSD's struct reg, from sys//include/reg.h. Segment selectors are dropped on AMD64, +# where the register angr calls "fs" is a segment base rather than a selector. +freebsd_register_sets = { + "AMD64": RegisterSet( + "15QIHHIHH5Q", + ( + "r15", + "r14", + "r13", + "r12", + "r11", + "r10", + "r9", + "r8", + "rdi", + "rsi", + "rbp", + "rbx", + "rdx", + "rcx", + "rax", + "xxx", # trapno + "xxx", # fs + "xxx", # gs + "xxx", # err + "es", + "ds", + "rip", + "cs", + "eflags", + "rsp", + "ss", + ), + ), + "X86": RegisterSet( + "19I", + ( + "fs", + "es", + "ds", + "edi", + "esi", + "ebp", + "xxx", # isp + "ebx", + "edx", + "ecx", + "eax", + "xxx", # trapno + "xxx", # err + "eip", + "cs", + "eflags", + "esp", + "ss", + "gs", + ), + ), + "AARCH64": RegisterSet( + "33QII", + (*(f"x{i}" for i in range(30)), "x30", "sp", "pc", "xxx", "xxx"), # lr, sp, elr, spsr, padding + ), +} + +# NetBSD's struct reg, from sys/arch//include/reg.h, paired with the type NetBSD gives the +# note that carries it: the number of the PT_GETREGS ptrace request, which is PT_FIRSTMACH plus an +# offset that is not the same on every architecture. +netbsd_register_sets = { + "AMD64": NetBSDRegisters( + 33, + RegisterSet( + "26Q", + ( + "rdi", + "rsi", + "rdx", + "rcx", + "r8", + "r9", + "r10", + "r11", + "r12", + "r13", + "r14", + "r15", + "rbp", + "rbx", + "rax", + "xxx", # gs + "xxx", # fs + "es", + "ds", + "xxx", # trapno + "xxx", # err + "rip", + "cs", + "eflags", + "rsp", + "ss", + ), + ), + ), + "X86": NetBSDRegisters( + 33, + RegisterSet( + "16I", + ( + "eax", + "ecx", + "edx", + "ebx", + "esp", + "ebp", + "esi", + "edi", + "eip", + "eflags", + "cs", + "ss", + "ds", + "es", + "fs", + "gs", + ), + ), + ), + "AARCH64": NetBSDRegisters( + 32, + RegisterSet( + "35Q", + (*(f"x{i}" for i in range(31)), "sp", "pc", "xxx", "xxx"), # spsr, tpidr + ), + ), +} + auxv_codes = { 0x0: "AT_NULL", 0x1: "AT_IGNORE", diff --git a/cle/backends/tls/elfcore_tls.py b/cle/backends/tls/elfcore_tls.py index a46e3b99a..85a670955 100644 --- a/cle/backends/tls/elfcore_tls.py +++ b/cle/backends/tls/elfcore_tls.py @@ -31,7 +31,10 @@ def __init__(self, loader, arch: archinfo.Arch, threadinfo): self.arch = arch self._threadinfo = threadinfo if arch.name == "AMD64": - self.thread_pointer = threadinfo["registers"]["fs_base"] + if "fs_base" not in threadinfo["registers"]: + # only Linux records it in the register note; the BSDs put it somewhere else + log.warning("This core dump does not contain fs_base. TLS information will be wrong.") + self.thread_pointer = threadinfo["registers"].get("fs_base", 0) elif arch.name == "X86": gs = threadinfo["registers"]["gs"] if gs == 0: diff --git a/tests/test_elfcore.py b/tests/test_elfcore.py index 23de8460f..8c5583945 100644 --- a/tests/test_elfcore.py +++ b/tests/test_elfcore.py @@ -1,9 +1,22 @@ from __future__ import annotations +import io import os +import struct import cle +# e_machine values, and the note types that appear in a core file +EM_386 = 3 +EM_X86_64 = 62 +EM_AARCH64 = 183 +NT_PRSTATUS = 1 +NT_386_TLS = 512 +# NetBSD types its per-LWP register note with the number of the PT_GETREGS ptrace request, which is +# PT_FIRSTMACH plus a per-architecture offset. +PT_GETREGS_X86 = 33 +PT_GETREGS_AARCH64 = 32 + def get_coredump_file(): return os.path.join( @@ -49,3 +62,327 @@ def remote_file_mapper(x): auto_load_libs=True, ) check_objects_loaded(ld) + + +def build_note(name, n_type, desc): + """ + Assemble one ELF note. Both the name and the descriptor are padded out to four bytes. + """ + note = struct.pack(" Date: Mon, 10 Aug 2026 20:56:53 +0000 Subject: [PATCH 2/3] Load real core dumps in the note-parsing tests The regression tests for note dispatch built their own core files with struct.pack. A hand-assembled core only ever has the shape the test author believed the kernel writes, so it can pass while a real FreeBSD, NetBSD or x32 core still fails to load. Load core dumps written by each of those kernels instead, and assert on the register values the dumped process actually held. --- tests/test_elfcore.py | 365 +++++++++++------------------------------- 1 file changed, 90 insertions(+), 275 deletions(-) diff --git a/tests/test_elfcore.py b/tests/test_elfcore.py index 8c5583945..d177cbff6 100644 --- a/tests/test_elfcore.py +++ b/tests/test_elfcore.py @@ -1,32 +1,18 @@ from __future__ import annotations -import io import os -import struct import cle -# e_machine values, and the note types that appear in a core file -EM_386 = 3 -EM_X86_64 = 62 -EM_AARCH64 = 183 -NT_PRSTATUS = 1 -NT_386_TLS = 512 -# NetBSD types its per-LWP register note with the number of the PT_GETREGS ptrace request, which is -# PT_FIRSTMACH plus a per-architecture offset. -PT_GETREGS_X86 = 33 -PT_GETREGS_AARCH64 = 32 +TEST_BASE = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "..", "binaries", "tests") def get_coredump_file(): - return os.path.join( - os.path.dirname(os.path.realpath(__file__)), - "../../binaries/tests/x86_64/coredump/true-libc.so.6-ld-linux-x86-64.so.2.core", - ) + return os.path.join(TEST_BASE, "x86_64", "coredump", "true-libc.so.6-ld-linux-x86-64.so.2.core") def get_binary_directory(): - return os.path.join(os.path.dirname(os.path.realpath(__file__)), "../../binaries/tests/x86_64") + return os.path.join(TEST_BASE, "x86_64") def check_objects_loaded(ld): @@ -64,315 +50,144 @@ def remote_file_mapper(x): check_objects_loaded(ld) -def build_note(name, n_type, desc): - """ - Assemble one ELF note. Both the name and the descriptor are padded out to four bytes. - """ - note = struct.pack("" and typed 33 on x86: a struct + # reg from sys/arch/amd64/include/reg.h. + ld, core = load_core("x86_64", "elfcore_netbsd_amd64.core") # the procinfo note holds no registers, so it must not have contributed a thread of its own assert core.threads == [0] registers = core.thread_registers() - assert registers["rip"] == 0x400380 - assert registers["rsp"] == 0x7F7FFFFFE000 - assert registers["rax"] == 0xAA - assert registers["rdi"] == 0xD1 - assert registers["rbp"] == 0xB9 - assert registers["r15"] == 0x15 - assert registers["cs"] == 0x2F - assert registers["eflags"] == 0x246 + assert registers["rip"] == 0x400C47 + assert registers["rsp"] == 0x2B2A292827262524 + assert registers["rax"] == 0x2726252423222120 + assert registers["rdi"] == 0x2E2D2C2B2A292827 + assert registers["rbp"] == 0x2C2B2A2928272625 + assert registers["r15"] == 0x363534333231302F + assert registers["cs"] == 0x47 + assert registers["eflags"] == 0x10212 + assert "fs_base" not in registers + assert ld.tls.threads[0].thread_pointer == 0 def test_netbsd_registers_i386(): - # struct reg, from sys/arch/i386/include/reg.h - gregset = struct.pack( - "<16I", - 0xAA, # eax - 0xCC, # ecx - 0xDD, # edx - 0xBB, # ebx - 0xFFBFE000, # esp - 0xB9, # ebp - 0x51, # esi - 0xD1, # edi - 0x8048400, # eip - 0x246, # eflags - 0x1B, # cs - 0x23, # ss - 0x2B, # ds - 0x2B, # es - 0x33, # fs - 0x3B, # gs - ) - notes = build_note(b"NetBSD-CORE", NT_PRSTATUS, netbsd_procinfo()) - notes += build_note(b"NetBSD-CORE@1", PT_GETREGS_X86, gregset) - _, core = load_core(build_core(EM_386, 32, notes)) + # struct reg from sys/arch/i386/include/reg.h, in a PT_GETREGS note typed 33 + _, core = load_core("i386", "elfcore_netbsd_i386.core") + assert core.threads == [0] registers = core.thread_registers() - assert registers["eip"] == 0x8048400 - assert registers["esp"] == 0xFFBFE000 - assert registers["eax"] == 0xAA - assert registers["edi"] == 0xD1 - assert registers["ebp"] == 0xB9 - assert registers["gs"] == 0x3B + assert registers["eip"] == 0x8048955 + assert registers["esp"] == 0x27262524 + assert registers["eax"] == 0x23222120 + assert registers["edi"] == 0x2A292827 + assert registers["ebp"] == 0x28272625 + assert registers["eflags"] == 0x10282 + assert registers["gs"] == 0x8B def test_netbsd_registers_aarch64(): - # struct reg, from sys/arch/aarch64/include/reg.h: r_reg[31], then sp, pc, spsr and tpidr. The - # note is typed 32 rather than 33, because PT_GETREGS is the first machine-dependent ptrace - # request on aarch64 and the second on x86. - gregset = struct.pack("<35Q", *range(31), 0x7FFFFFFFE000, 0x400400, 0x60000000, 0xFC0E044FC000) - notes = build_note(b"NetBSD-CORE", NT_PRSTATUS, netbsd_procinfo()) - notes += build_note(b"NetBSD-CORE@1", PT_GETREGS_AARCH64, gregset) - _, core = load_core(build_core(EM_AARCH64, 64, notes)) + # struct reg from sys/arch/aarch64/include/reg.h: r_reg[31], then sp, pc, spsr and tpidr. The note + # is typed 32 rather than 33, because PT_GETREGS is the first machine-dependent ptrace request on + # aarch64 and the second on x86. + _, core = load_core("aarch64", "elfcore_netbsd_aarch64.core") + assert core.threads == [0] registers = core.thread_registers() - assert registers["pc"] == 0x400400 - assert registers["sp"] == 0x7FFFFFFFE000 - assert registers["x30"] == 30 - assert registers["x29"] == 29 + assert registers["pc"] == 0x200100830 + assert registers["sp"] == 0xFFFFFFF98770 + assert registers["x30"] == 0x200100864 + assert registers["x29"] == 0xFFFFFFF98790 + assert registers["x1"] == 0x2F def test_linux_x86_tls_note(): - # Linux writes every register set other than the general purpose and floating point ones under - # its own name rather than under CORE, NT_386_TLS among them. A loader that looks for it under - # CORE never finds it and falls back to guessing the thread's TLS region out of memory. - gregset = struct.pack( - "<17I", - 0xBB, # ebx - 0xCC, # ecx - 0xDD, # edx - 0x51, # esi - 0xD1, # edi - 0xB9, # ebp - 0xAA, # eax - 0x2B, # ds - 0x2B, # es - 0x0, # fs - 0x63, # gs - 0x0, # orig_eax - 0x8048400, # eip - 0x23, # cs - 0x246, # eflags - 0xFFBFE000, # esp - 0x2B, # ss - ) - # three struct user_desc, the first of which describes the GDT entry gs selects - tls = struct.pack("<4I", 12, 0xF7FF0700, 0xFFFFF, 0x51) - tls += struct.pack("<4I", 13, 0, 0, 0x28) + struct.pack("<4I", 14, 0, 0, 0x28) - notes = build_note(b"CORE", NT_PRSTATUS, linux_prstatus_32(gregset)) - notes += build_note(b"LINUX", NT_386_TLS, tls) - ld, core = load_core(build_core(EM_386, 32, notes)) + # Linux writes every register set other than the general purpose and floating point ones under its + # own name rather than under CORE, NT_386_TLS among them. A loader that looks for it under CORE + # never finds it and falls back to guessing the thread's TLS region out of memory. + ld, core = load_core("i386", "elfcore_linux_i386.core") - assert core.thread_registers()["eip"] == 0x8048400 - assert ld.tls.threads[0].thread_pointer == 0xF7FF0700 + registers = core.thread_registers() + assert registers["eip"] == 0x80492AB + assert registers["gs"] == 0x63 + # the GDT entry gs selects, straight out of the note + assert ld.tls.threads[0].thread_pointer == 0xF7984700 def test_prstatus_abi_mismatch(): # a Linux x32 core: EM_X86_64 with ELFCLASS32, so cle picks X86, but the note holds an amd64 - # elf_prstatus with 27 eight-byte registers - gregset = struct.pack("<27Q", *([0] * 16), 0x400380, 0x33, 0x246, 0xFFC0E070, *([0] * 7)) - prstatus = linux_prstatus_32(gregset, bytes(4)) - ld, core = load_core(build_core(EM_X86_64, 32, build_note(b"CORE", NT_PRSTATUS, prstatus))) + # elf_prstatus with 27 eight-byte registers in 296 bytes rather than 17 four-byte ones in 144 + ld, core = load_core("x86_64", "elfcore_linux_x32.core") # the registers cannot be represented, but the core's memory still loads assert core.threads == [] assert core.thread_registers() == {} - assert ld.memory.load(0x400000, 4) == b"\xcc" * 4 + assert ld.memory.load(0x400400, 8).hex() == "4883ec084883c408" if __name__ == "__main__": From 143abf368cf9cd36d845445a80c635986921260a Mon Sep 17 00:00:00 2001 From: Yan Date: Sat, 29 Aug 2026 06:05:46 +0000 Subject: [PATCH 3/3] Read the auxv note at the container's word width __parse_auxv sized an Elf_auxv_t entry with self.arch.bytes. That is the instruction set's width, and an auxv entry is two words of the container's - Elf32_auxv_t for an ELFCLASS32 core and Elf64_auxv_t for an ELFCLASS64 one. The two agree for every core cle has ever loaded, which is why this has not shown up before. They stop agreeing for an x32 core, whose ELFCLASS32 container holds EM_X86_64 code. cle#791 resolves that to AMD64, and reading the 152-byte NT_AUXV of tests/x86_64/elfcore_linux_x32.core at eight bytes per word runs off the end of the note: struct.error: unpack_from requires a buffer of at least 160 bytes for unpacking 8 bytes at offset 152 (actual buffer size is 152) which fails the whole load, memory mappings included - the same cost this branch removes for the thread notes. The entries that do fit decode to nonsense: the first pair reads 0xffd4900000000021 rather than AT_SYSINFO_EHDR = 0xffd49000. Take the width from self._reader.elfclass instead. Nothing changes for a core whose class and machine agree. test_prstatus_abi_mismatch now also pins three auxv values that only come out right at four bytes, AT_PHENT = sizeof(Elf32_Phdr) among them. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01DqcAcuGLrNJViJrpdtFYCS --- cle/backends/elf/elfcore.py | 11 ++++++++--- tests/test_elfcore.py | 11 +++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/cle/backends/elf/elfcore.py b/cle/backends/elf/elfcore.py index dcf27cfed..e05a9b876 100644 --- a/cle/backends/elf/elfcore.py +++ b/cle/backends/elf/elfcore.py @@ -404,9 +404,14 @@ def __parse_x86_tls(self, desc): self.__current_thread["segments"][index] = (base, limit, flags) def __parse_auxv(self, desc): - for offset in range(0, len(desc), self.arch.bytes * 2): - code = struct.unpack_from(self.arch.struct_fmt(), desc, offset)[0] - value = struct.unpack_from(self.arch.struct_fmt(), desc, offset + self.arch.bytes)[0] + # An entry is an Elf32_auxv_t or an Elf64_auxv_t: two words of the container's width, which is + # the pointer width rather than the instruction set's. The two agree for every ordinary core + # and not for an x32 one, whose ELFCLASS32 auxv describes a process whose registers are 64-bit. + word = self._reader.elfclass // 8 + fmt = (">" if self.arch.memory_endness == "Iend_BE" else "<") + ("Q" if word == 8 else "I") + for offset in range(0, len(desc), word * 2): + code = struct.unpack_from(fmt, desc, offset)[0] + value = struct.unpack_from(fmt, desc, offset + word)[0] code_str = auxv_codes.get(code, code) if code_str == "AT_RANDOM": diff --git a/tests/test_elfcore.py b/tests/test_elfcore.py index d177cbff6..f0e103f3a 100644 --- a/tests/test_elfcore.py +++ b/tests/test_elfcore.py @@ -180,8 +180,9 @@ def test_linux_x86_tls_note(): def test_prstatus_abi_mismatch(): - # a Linux x32 core: EM_X86_64 with ELFCLASS32, so cle picks X86, but the note holds an amd64 - # elf_prstatus with 27 eight-byte registers in 296 bytes rather than 17 four-byte ones in 144 + # a Linux x32 core: EM_X86_64 with ELFCLASS32. Its NT_PRSTATUS is 296 bytes, which is neither the + # 144 of an i386 elf_prstatus nor the 332 of an amd64 one, so the thread is dropped whichever of + # the two cle resolves the container to. Its NT_AUXV is ELFCLASS32 and reads at four bytes either way. ld, core = load_core("x86_64", "elfcore_linux_x32.core") # the registers cannot be represented, but the core's memory still loads @@ -189,6 +190,12 @@ def test_prstatus_abi_mismatch(): assert core.thread_registers() == {} assert ld.memory.load(0x400400, 8).hex() == "4883ec084883c408" + # the auxv note is an ELFCLASS32 one whatever e_machine says, so its entries are two four-byte + # words; read at eight the note runs out mid-entry and AT_HWCAP's CPUID word becomes nonsense + assert core.auxv["AT_PHENT"] == 0x20 # sizeof(Elf32_Phdr) + assert core.auxv["AT_HWCAP"] == 0xBFEBFBFF + assert core.auxv["AT_EXECFN"] == b"./a.out" + if __name__ == "__main__": test_remote_file_mapping()