Skip to content

ELFCore: Read register notes in the namespace that names them - #734

Open
zardus wants to merge 3 commits into
masterfrom
feature/fix-cle-elfcore-prstatus
Open

zardus wants to merge 3 commits into
masterfrom
feature/fix-cle-elfcore-prstatus

Conversation

@zardus

@zardus zardus commented Aug 10, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

Seven of the eight core dumps in angr/binaries written by a kernel other than plain Linux/x86 do not load at all. tests/x86_64/elfcore_freebsd_amd64.core:

  File "cle/backends/elf/elfcore.py", line 359, in __parse_prstatus
    pos, *regvals = read_longs(nreg)
struct.error: unpack requires a buffer of 216 bytes

and tests/i386/elfcore_netbsd_i386.core and tests/x86_64/elfcore_linux_x32.core:

  File "cle/backends/elf/elfcore.py", line 365, in __parse_prstatus
    pos <= len(desc) < pos + arch_bytes
AssertionError: Please create an issue with this core-file attached to get this fixed.

One thread note it cannot read costs the caller the whole core, memory mappings included — and that guard is an assert, which python -O strips.

Root cause

ELFCore.__extract_note_info dispatched on the note type alone:

if note.n_type == "NT_PRSTATUS":
    self.__parse_prstatus(n_desc)
...
elif note.n_type == 512 and self.arch.name == "X86":

ELF note types are namespaced by the note's name, so a number identifies a struct only together with the kernel that wrote it. FreeBSD's NT_PRSTATUS is a struct prstatus from sys/sys/procfs.h sharing no prefix with Linux's; NetBSD's is a struct netbsd_elfcore_procinfo holding no registers at all, its registers living in one PT_GETREGS note per LWP under NetBSD-CORE@<lwpid>; and Linux writes NT_386_TLS under LINUX, not CORE, so that branch never fired and cle fell back to guessing the thread pointer out of memory. ELFCoreThread compounds it with self.thread_pointer = threadinfo["registers"]["fs_base"], a KeyError on any kernel that records the segment bases elsewhere.

Fix

__parse_auxv takes an entry's width from the container's ELF class rather than from self.arch.bytes: an Elf_auxv_t is two words of the pointer width, and the two agree for every core whose class and machine agree.

Dispatch is keyed on (n_name, n_type)CORE, LINUX, FreeBSD, and the NetBSD-CORE@ prefix — with per-architecture layouts for FreeBSD's struct prstatus and NetBSD's per-LWP register note, and each descriptor is checked against the layout it is about to be read with. A thread whose registers will not decode is dropped with a warning rather than failing the load, and a missing fs_base warns and yields 0.

elfcore_freebsd_amd64.core:   arch=AMD64  threads=[0]        rip=0x20242b
elfcore_freebsd_aarch64.core: arch=AARCH64 threads=[0,1,2,3] pc=0x211fc4  | thread2 pc=0x211fc8
elfcore_netbsd_amd64.core:    arch=AMD64  threads=[0]        rip=0x400c47
elfcore_netbsd_aarch64.core:  arch=AARCH64 threads=[0]       pc=0x200100830
elfcore_linux_x32.core:       arch=X86    threads=[]  memory[0x400400:8]=4883ec084883c408

The x32 core is the deliberate non-recovery: an amd64 elf_prstatus in a file cle reads as X86, so its thread is dropped and its memory still maps. Floating-point and vector register sets are still not read.

Testing

tests/test_elfcore.py gains a test per kernel and architecture — test_freebsd_prstatus_amd64, ..._i386, ..._aarch64, test_netbsd_registers_amd64, ..._i386, ..._aarch64, test_linux_x86_tls_note and test_prstatus_abi_mismatch — asserting the registers the kernel actually wrote, assert registers["rip"] == 0x20242B among them. Each loads an unmutated core dump; all eight fail on the merge base. The fixtures are on angr/binaries master, added by that repository's PR 176, which merged on 2026-08-12.

Validation: #734 (comment)

session: sharpen

@zardus

zardus commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 143abf368cf9cd36d845445a80c635986921260a against baseline 46a37333f4f59b0facf8774ee743ebc4cc074e9b.

Re-keyed from 34a208639e8b74eefb2d157e14dcbe460ce46807, and this was not a rebase: the baseline is the same commit it always was, and the branch gained a third commit, 143abf36 "Read the auxv note at the container's word width". __parse_auxv sized an entry with self.arch.bytes, the instruction set's width, where an Elf32_auxv_t or Elf64_auxv_t is two words of the container's. Every figure below was re-measured at this head.

Measured configuration: a detached worktree of cle at the revision named, this workspace's pinned Python 3.12 environment, nice -n 19, no xdist, -p no:randomly. pylint is run with the CI configuration from angr/ci-settings, ci-image/conf/pylintrc, because cle declares no [tool.pylint] table and the bare defaults score about two points lower on every file.

  • Focused: python -m pytest tests/test_elfcore.py10 passed. The third commit adds three assertions inside an existing test rather than a new one, so the count is unchanged
  • Full suite: python -m pytest tests247 passed, 9 skipped in 42.4 s. The earlier record's 210 passed is master's growth, not this branch's
  • Fails without the fix: reverting only cle/backends/elf/elfcore.py and cle/backends/tls/elfcore_tls.py to the baseline and keeping the tests gives 7 failed, 3 passed. Five failures are struct.error: unpack requires a buffer of N bytes at elfcore.py, reading a foreign descriptor with the Linux layout — 216 bytes for the two amd64 cores, 68 for FreeBSD i386, 272 for the two aarch64 cores. The other two, NetBSD i386 and the x32 core, are the trailing-length AssertionError at cle/backends/elf/elfcore.py:365, which python -O strips. The three that pass on the baseline are the two pre-existing remote-file-mapping tests and test_linux_x86_tls_note, which guards behaviour this change must preserve rather than behaviour it adds: the baseline matched note type 512 without looking at the note's name, so it found the LINUX-namespaced note by accident
  • Lint: pylint per changed file, this head against the baseline — cle/backends/elf/elfcore.py 9.97 -> 10.00, cle/backends/tls/elfcore_tls.py 9.14 -> 9.19, tests/test_elfcore.py flat at 10.00. No file regresses

What the third commit does, and what it does not do here. tests/x86_64/elfcore_linux_x32.core is ELFCLASS32 with e_machine EM_X86_64, and its NT_AUXV descriptor is 152 bytes. Read as Elf32_auxv_t, the first pair is AT_SYSINFO_EHDR 0x21 with value 0xffd49000; read as Elf64_auxv_t the same bytes give one word 0xffd4900000000021, and walking the whole note at eight bytes raises 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 would fail the load and take the memory mappings with it. On this branch alone the values do not move: cle resolves this container to X86, where self.arch.bytes and elfclass // 8 are both 4, and core.auxv["AT_PHENT"], ["AT_HWCAP"] and ["AT_EXECFN"] read 0x20, 0xbfebfbff and b"./a.out" at both 34a20863 and this head — measured, not assumed. The eight-byte read becomes reachable when cle#791 resolves an x32 core to AMD64, and the three assertions this commit adds exist so that it stays right when that lands.

The tests load core dumps written by the kernels they name, added as fixtures by angr/binaries#176, which merged on 2026-08-12, so the fixtures are in binaries master and nothing here waits on a sibling. All of the cores are public upstream postmortem test inputs. Loaded with cle.Loader(path, main_opts={"backend": "elfcore"}, auto_load_libs=False); the head column below was re-read at 143abf36 for this record, one load per core:

core sha256 baseline head
x86-64-freebsd.core 67c04929 struct.error, 216-byte buffer 1 thread, rip 0x20242b
x86-32-freebsd.core fbfdd123 struct.error, 68-byte buffer 1 thread, eip 0x401c6b
aarch64-freebsd-multithread.core b6ed753f struct.error, 272-byte buffer 4 threads, pc 0x211fc4
x86-64-netbsd.core f4f46df6 struct.error, 216-byte buffer 1 thread, rip 0x400c47
x86-32-netbsd.core 56f7cc74 AssertionError 1 thread, eip 0x8048955
1lwp_SIGSEGV.aarch64.core 0ae7df3e struct.error, 272-byte buffer 1 thread, pc 0x200100830
elfutils-0.188/tests/testfile-x32-core.bz2 766c70a8 AssertionError loads; 0 threads, thread skipped with a warning

Every pc and sp above lands inside a segment the core maps; on the baseline the same descriptors either would not unpack or were read at the wrong stride. Two of the layouts are confirmed by the architectural values beside the registers: the FreeBSD amd64 note gives cs 0x43 / ss 0x3b and the NetBSD amd64 note cs 0x47 / ss 0x3f, each that kernel's user-mode selector pair. An eighth fixture, elfcore_linux_i386.core, carries the NT_386_TLS note test_linux_x86_tls_note reads; it loads with 1 thread at eip 0x80492ab on this head.

Linux x86 cores are unaffected. Fourteen of them, including thread_crash/linux-i386.core (4b63cd6a) and elfutils-0.188/tests/backtrace.i386.fp.core (71111d7b), gave identical results on both revisions when that was measured at 529cca7f3. The third commit does not disturb that conclusion: it changes the auxv stride only where elfclass // 8 differs from arch.bytes, and for an ordinary Linux i386 or x86-64 core the two agree, so the arithmetic is unchanged for every one of them.

Caveats:

  • A core on an architecture with no register table, s390x for instance, still raises CLECompatibilityError and takes the load with it, exactly as on the baseline. That is a support gap rather than a decoding bug and is untouched here.
  • A Linux x86 core that carries no NT_386_TLS note still fails in the AT_RANDOM fallback with a bare KeyError, on both revisions. It has a separate cause and is untouched here.
  • The x32 and n32 threads above are skipped rather than decoded: their descriptors hold a register set whose geometry the core's ELF header does not describe, and picking the right one needs process-ABI detection that this change does not attempt.

Hosted CI at head 143abf368cf9cd36d845445a80c635986921260a, read live 2026-08-29T20:03Z: 18 check runs, every one success, and both legacy commit statuses greenpre-commit.ci - pr and docs/readthedocs.org:cle — for 20 terminal green checks and nothing outside success. That includes ci / Lint, ci / Typecheck, ci / Build, all eleven ci / Test shards, Test (Pyodide), and Test windows-2022 and Test macos-15, the two jobs that take binaries from master. The workflow run is https://github.com/angr/cle/actions/runs/33237718888, concluded success at this head.

@angr-bot

Copy link
Copy Markdown
Member

Corpus decompilation diffs can be found at angr/dec-snapshots@master...angr/cle_734

@zardus

zardus commented Aug 26, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

This came out of assembling the angr/vibr preview snapshot (every green open PR merged together, then each component's full test suite run against the result with an angr/binaries checkout). Both PRs are green on their own.

With #791 applied, this PR's test_prstatus_abi_mismatch fails: #791 takes the word size from the machine, so the x32 core (EM_X86_64 + ELFCLASS32) this test relies on resolving to X86 now resolves to AMD64, and ELFCore.__parse_auxv (elfcore.py:409) raises struct.error unpacking 8-byte words from the 4-byte auxv note. Details are in the comment on #791. The preview excludes #791 and keeps this PR; if the test's premise (x32 → X86) is meant to survive #791, the auxv parsing needs the container word size.

zardus and others added 2 commits August 26, 2026 22:45
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 <noreply@anthropic.com>
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.
@zardus
zardus force-pushed the feature/fix-cle-elfcore-prstatus branch from d1d9017 to 34a2086 Compare August 26, 2026 22:46
@zardus

zardus commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Full thread and register report for the eight core dumps in angr/binaries, each loaded with cle.Loader(path, main_opts={"backend": "elfcore"}) and printed unmutated, before and after this change. Each line names the architecture cle chose, the thread ids it recovered, a handful of the registers it read, and — where relevant — the TLS thread pointer.

Before — seven of the eight do not load: the FreeBSD and NetBSD notes run off the end of a Linux elf_prstatus, and two hit the "Please create an issue" assertion:

cle at the merge base, 46a3733
cle: <cle at the merge base>/cle/__init__.py

-- FreeBSD: NT_PRSTATUS in the 'FreeBSD' namespace, a struct prstatus sharing no prefix
   with the Linux struct of the same note type
x86_64/elfcore_freebsd_amd64.core: RAISED struct.error: unpack requires a buffer of 216 bytes
    | Traceback (most recent call last):
    |   File "<cle at the merge base>/cle/backends/elf/elfcore.py", line 359, in __parse_prstatus
    |     pos, *regvals = read_longs(nreg)
    |                     ^^^^^^^^^^^^^^^^
    |   File "<cle at the merge base>/cle/backends/elf/elfcore.py", line 198, in read_longs
    |     return (fin, *struct.unpack(end + fmt * n, desc[pos:fin]))
    |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | struct.error: unpack requires a buffer of 216 bytes
i386/elfcore_freebsd_i386.core: RAISED struct.error: unpack requires a buffer of 68 bytes
    | Traceback (most recent call last):
    |   File "<cle at the merge base>/cle/backends/elf/elfcore.py", line 359, in __parse_prstatus
    |     pos, *regvals = read_longs(nreg)
    |                     ^^^^^^^^^^^^^^^^
    |   File "<cle at the merge base>/cle/backends/elf/elfcore.py", line 198, in read_longs
    |     return (fin, *struct.unpack(end + fmt * n, desc[pos:fin]))
    |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | struct.error: unpack requires a buffer of 68 bytes
aarch64/elfcore_freebsd_aarch64.core: RAISED struct.error: unpack requires a buffer of 272 bytes
    | Traceback (most recent call last):
    |   File "<cle at the merge base>/cle/backends/elf/elfcore.py", line 359, in __parse_prstatus
    |     pos, *regvals = read_longs(nreg)
    |                     ^^^^^^^^^^^^^^^^
    |   File "<cle at the merge base>/cle/backends/elf/elfcore.py", line 198, in read_longs
    |     return (fin, *struct.unpack(end + fmt * n, desc[pos:fin]))
    |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | struct.error: unpack requires a buffer of 272 bytes

-- NetBSD: registers live in one PT_GETREGS note per LWP, named 'NetBSD-CORE@<lwpid>';
   its NT_PRSTATUS is a struct netbsd_elfcore_procinfo and holds no registers at all
x86_64/elfcore_netbsd_amd64.core: RAISED struct.error: unpack requires a buffer of 216 bytes
    | Traceback (most recent call last):
    |   File "<cle at the merge base>/cle/backends/elf/elfcore.py", line 359, in __parse_prstatus
    |     pos, *regvals = read_longs(nreg)
    |                     ^^^^^^^^^^^^^^^^
    |   File "<cle at the merge base>/cle/backends/elf/elfcore.py", line 198, in read_longs
    |     return (fin, *struct.unpack(end + fmt * n, desc[pos:fin]))
    |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | struct.error: unpack requires a buffer of 216 bytes
i386/elfcore_netbsd_i386.core: RAISED builtins.AssertionError: Please create an issue with this core-file attached to get this fixed.
    | Traceback (most recent call last):
    |   File "<cle at the merge base>/cle/backends/elf/elfcore.py", line 109, in __extract_note_info
    |     self.__parse_prstatus(n_desc)
    |   File "<cle at the merge base>/cle/backends/elf/elfcore.py", line 365, in __parse_prstatus
    |     pos <= len(desc) < pos + arch_bytes
    | AssertionError: Please create an issue with this core-file attached to get this fixed.
aarch64/elfcore_netbsd_aarch64.core: RAISED struct.error: unpack requires a buffer of 272 bytes
    | Traceback (most recent call last):
    |   File "<cle at the merge base>/cle/backends/elf/elfcore.py", line 359, in __parse_prstatus
    |     pos, *regvals = read_longs(nreg)
    |                     ^^^^^^^^^^^^^^^^
    |   File "<cle at the merge base>/cle/backends/elf/elfcore.py", line 198, in read_longs
    |     return (fin, *struct.unpack(end + fmt * n, desc[pos:fin]))
    |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | struct.error: unpack requires a buffer of 272 bytes

-- Linux i386: NT_386_TLS (512) is written under the 'LINUX' namespace, not 'CORE'
i386/elfcore_linux_i386.core: arch=X86 threads=[0] eip=0x80492ab esp=0x27262524 eax=0x23222120 edi=0x2a292827 ebp=0x28272625 cs=0x23 eflags=0x10283 gs=0x63 | tls.threads[0].thread_pointer=0xf7984700

-- Linux x32: EM_X86_64 with ELFCLASS32, so cle picks X86, but the note holds an amd64
   elf_prstatus (27 eight-byte registers in 296 bytes, not 17 four-byte ones in 144)
x86_64/elfcore_linux_x32.core: RAISED builtins.AssertionError: Please create an issue with this core-file attached to get this fixed.
    | Traceback (most recent call last):
    |   File "<cle at the merge base>/cle/backends/elf/elfcore.py", line 109, in __extract_note_info
    |     self.__parse_prstatus(n_desc)
    |   File "<cle at the merge base>/cle/backends/elf/elfcore.py", line 365, in __parse_prstatus
    |     pos <= len(desc) < pos + arch_bytes
    | AssertionError: Please create an issue with this core-file attached to get this fixed.

LOG WARNING cle.backends.elf.elf x119: ELF file <angr/binaries>/tests/x86_64/elfcore_netbsd_amd64.core is lo
LOG WARNING cle.backends.elf.elfcore x1: Dependency <home>/git/llvm-project/lldb/test/Shell/Register/Core/Inputs/a.out does not exist on the curr

After — all eight load with the registers their kernel wrote; the x32 core drops its undecodable thread with a warning and still maps memory:

with this change, 34a2086
cle: <cle with this change>/cle/__init__.py

-- FreeBSD: NT_PRSTATUS in the 'FreeBSD' namespace, a struct prstatus sharing no prefix
   with the Linux struct of the same note type
x86_64/elfcore_freebsd_amd64.core: arch=AMD64 threads=[0] rip=0x20242b rsp=0x2b2a292827262524 rax=0x2726252423222120 rdi=0x2e2d2c2b2a292827 rbp=0x2c2b2a2928272625 r15=0x363534333231302f cs=0x43 eflags=0x10246 | absent: fs_base | tls.threads[0].thread_pointer=0x0
i386/elfcore_freebsd_i386.core: arch=X86 threads=[0] eip=0x401c6b esp=0x27262524 eax=0x23222120 edi=0x2a292827 ebp=0x28272625 cs=0x33 eflags=0x10246 gs=0x1b
aarch64/elfcore_freebsd_aarch64.core: arch=AARCH64 threads=[0, 1, 2, 3] pc=0x211fc4 sp=0xffffbfffdea0 x30=0x212074 x29=0xffffbfffdf30 x0=0x1010101 x1=0x2020202 | thread2 pc=0x211fc8 x0=0x11111111

-- NetBSD: registers live in one PT_GETREGS note per LWP, named 'NetBSD-CORE@<lwpid>';
   its NT_PRSTATUS is a struct netbsd_elfcore_procinfo and holds no registers at all
x86_64/elfcore_netbsd_amd64.core: arch=AMD64 threads=[0] rip=0x400c47 rsp=0x2b2a292827262524 rax=0x2726252423222120 rdi=0x2e2d2c2b2a292827 rbp=0x2c2b2a2928272625 r15=0x363534333231302f cs=0x47 eflags=0x10212 | absent: fs_base | tls.threads[0].thread_pointer=0x0
i386/elfcore_netbsd_i386.core: arch=X86 threads=[0] eip=0x8048955 esp=0x27262524 eax=0x23222120 edi=0x2a292827 ebp=0x28272625 cs=0x37 eflags=0x10282 gs=0x8b
aarch64/elfcore_netbsd_aarch64.core: arch=AARCH64 threads=[0] pc=0x200100830 sp=0xfffffff98770 x30=0x200100864 x29=0xfffffff98790 x0=0x0 x1=0x2f

-- Linux i386: NT_386_TLS (512) is written under the 'LINUX' namespace, not 'CORE'
i386/elfcore_linux_i386.core: arch=X86 threads=[0] eip=0x80492ab esp=0x27262524 eax=0x23222120 edi=0x2a292827 ebp=0x28272625 cs=0x23 eflags=0x10283 gs=0x63 | tls.threads[0].thread_pointer=0xf7984700

-- Linux x32: EM_X86_64 with ELFCLASS32, so cle picks X86, but the note holds an amd64
   elf_prstatus (27 eight-byte registers in 296 bytes, not 17 four-byte ones in 144)
x86_64/elfcore_linux_x32.core: arch=X86 threads=[]  | absent: eip,esp,eax,edi,ebp,cs,eflags,gs | thread_registers()={} memory[0x400400:8]=4883ec084883c408

LOG WARNING cle.backends.elf.elf x119: ELF file <angr/binaries>/tests/x86_64/elfcore_netbsd_amd64.core is lo
LOG WARNING cle.backends.elf.elfcore x13: Failed to identify main object in ELFCore
LOG WARNING cle.backends.tls.elfcore_tls x4: This core dump does not contain fs_base. TLS information will be wrong.

__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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DqcAcuGLrNJViJrpdtFYCS
@zardus

zardus commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Interaction with #791, and the auxv note

This branch and #791 are each green alone and cannot both be in a rollup: the
mono selection has been dropping #791 for it. The failing test is
tests/test_elfcore.py::test_prstatus_abi_mismatch, and unlike the other
exclusions of this kind it was not an assertion pinning something -- it was a
real defect in this file, which only #791 makes reachable.

#791 resolves an ELFCLASS32 container of EM_X86_64 code to AMD64, which is
right for the instruction set. __parse_auxv then sized an Elf_auxv_t entry
with self.arch.bytes, and an auxv entry is two words of the container's
width. The 152-byte NT_AUXV of tests/x86_64/elfcore_linux_x32.core is 19
Elf32_auxv_t entries; read at eight bytes per word it runs off the end of the
note during the load:

  File "cle/backends/elf/elfcore.py", line 409, in __parse_auxv
    value = struct.unpack_from(self.arch.struct_fmt(), desc, offset + self.arch.bytes)[0]
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 costs the caller the whole core, memory mappings included -- exactly what
this branch removes for the thread notes. And the entries that do fit are
nonsense: the first pair reads 0xffd4900000000021 rather than
AT_SYSINFO_EHDR = 0xffd49000, so hardening the loop against the short tail
would have replaced a crash with a silently wrong auxv.

Fix

143abf3 takes the width from self._reader.elfclass. Nothing changes for a core
whose class and machine agree, which is every other core in the suite. The
parsed auxv is byte-identical before and after -- here it is with #791 applied,
i.e. with self.arch reading AMD64:

arch: <Arch AMD64 (LE)>
AT_SYSINFO_EHDR 0xffd49000   AT_HWCAP 0xbfebfbff   AT_PAGESZ 0x1000
AT_PHDR 0x400034  AT_PHENT 0x20  AT_PHNUM 0x8  AT_ENTRY 0x400290
AT_PLATFORM b'i686'  AT_EXECFN b'./a.out'

AT_PHENT is sizeof(Elf32_Phdr) and AT_HWCAP is the x86 CPUID feature word,
so the four-byte reading is checkable rather than merely self-consistent.
test_prstatus_abi_mismatch now pins those three values; its existing
assertions are unchanged, because the 296-byte NT_PRSTATUS is neither an i386
elf_prstatus (144) nor an amd64 one (332) and the thread is dropped whichever
way the container resolves. Its comment said "so cle picks X86", which #791
makes false, so it now states the size argument instead.

Verification

master + #791 + this branch, cle's whole suite:

1 failed, 250 passed, 9 skipped

The one failure is #791's own test_arch_detect.py::TestArchPcodeDetect::test_elf_x32,
which needs tests/x86_64/x32_relocatable.o from the still-open angr/binaries
PR 198 and fails identically with #791 alone against binaries master
(CLEFileNotFoundError). It is resolved in CI and is not part of this
interaction.

Heads: #791 fc998067, #734 143abf3 (this push).

session: sharpen

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants