Skip to content

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

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

ELFCore: Read register notes in the namespace that names them#734
zardus wants to merge 2 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

ELF note types are namespaced by the note's name, but ELFCore decoded every NT_PRSTATUS as a Linux struct elf_prstatus: a FreeBSD core raised struct.error when its shorter descriptor ran out, and a NetBSD one was read as registers though its type-1 note is a struct netbsd_elfcore_procinfo that holds none. Only an assert, which python -O strips, caught a length mismatch.

ELFCore now dispatches on the note name, reads FreeBSD's struct prstatus and NetBSD's per-LWP register note, and checks each descriptor against the layout it is about to be read with. Linux namespaces its GDT entry note LINUX rather than CORE, so that one is read there. A thread whose registers will not decode is dropped with a warning instead of failing the load.

The regression test loads core dumps written by each of those kernels, added as fixtures by angr/binaries#176, so the checks here stay red until that merges: CI checks out binaries master.

Validation: #734 (comment)

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>
@zardus

zardus commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 529cca7f3e6373fe10b0bc7fae927bdcc177b404 against baseline b58ea02a446106647cdaae32bdf91b7062404cc1.

  • Focused: python -m pytest tests/test_elfcore.py — 10 passed
  • Fails without the fix: reverting only cle/backends/elf/elfcore.py and cle/backends/tls/elfcore_tls.py to the baseline, keeping the tests, gives 7 failed and 3 passed. Five of the failures are struct.error: unpack requires a buffer of N bytes at elfcore.py:198, 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 AssertionError at elfcore.py:365, the trailing length check that 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 behavior this change must preserve rather than behavior it adds: the baseline matched note type 512 without looking at the note's name, so it found the note that is namespaced LINUX by accident.
  • Full suite: python -m pytest tests — 210 passed, 9 skipped
  • Lint/type: pylint and pyright against the merge base, on the changed files — no regressions; cle/backends/elf/elfcore.py 9.97 -> 10.00, cle/backends/tls/elfcore_tls.py 9.14 -> 9.19
  • Workspace gate: cle and its configured pre-commit hooks — passed over all files, tree unchanged by the hooks

The tests load core dumps written by the kernels they name, added as fixtures by angr/binaries#176; the checks on this PR stay red until that merges, because CI checks out binaries master. All of the cores are public upstream postmortem test inputs. Loaded with cle.Loader(path, main_opts={"backend": "elfcore"}, auto_load_libs=False):

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
2lwp_process_SIGSEGV.aarch64.core 5c2b0d1e struct.error, 272-byte buffer 2 threads, pc 0x200100a44
linux-mips64el-gnuabin32.core 6912fbbf AssertionError loads; thread skipped with a warning
elfutils-0.188/tests/testfile-x32-core.bz2 766c70a8 AssertionError loads; thread skipped with a warning

Seven of those nine are the fixtures the tests now load, as elfcore_{freebsd,netbsd}_{i386,amd64,aarch64}.core and elfcore_linux_x32.core; the two-LWP NetBSD aarch64 core and the mips64el n32 one stayed out. An eighth fixture, elfcore_linux_i386.core, carries the NT_386_TLS note test_linux_x86_tls_note reads.

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.

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), give identical results on both revisions: the same thread counts, TLS segment tables and thread pointers where they load, and the same errors where they do not.

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.

@angr-bot

Copy link
Copy Markdown
Member

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

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.
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