Skip to content

PE: State little-endian for the PowerPC machine types - #805

Open
zardus wants to merge 1 commit into
masterfrom
feature/pe-ppc-endness
Open

PE: State little-endian for the PowerPC machine types#805
zardus wants to merge 1 commit into
masterfrom
feature/pe-ppc-endness

Conversation

@zardus

@zardus zardus commented Sep 1, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

cle attaches the wrong byte order to a Windows NT PowerPC PE. cle/backends/pe/pe.py turns FILE_HEADER.Machine into an architecture at two sites -- the self._arch is None branch of PE.__init__ and PE.check_compatibility -- and both call archinfo.arch_from_id with no endness argument. Windows NT on PowerPC was little-endian, so IMAGE_FILE_MACHINE_POWERPC (0x1f0) and IMAGE_FILE_MACHINE_POWERPCFP (0x1f1) have to resolve little-endian. Against archinfo master 384c676 they resolve big-endian:

$ python -P -c 'import archinfo, pefile
print(archinfo.arch_from_id(pefile.MACHINE_TYPE[0x1f0]).memory_endness)'
Iend_BE

pefile parses the headers little-endian whatever the machine type, so the parse itself survives. What does not is everything that reads a word through the object's Arch: Clemory.unpack_word, the TLS and gopclntab readers, and the lifting and CFG built on top.

angr/archinfo#380 made this true on 2026-09-01, by changing ArchPPC32.default_endness from Iend_LE to Iend_BE. That pull request named this repair and said it "belongs in cle, as a separate change". An earlier version of this description said nothing about a PowerPC PE was observably wrong; that was true before angr/archinfo#380 and is not true now.

Root cause

The registration a PowerPC machine type falls through to is register_arch([r".*p\w*pc.*"], 32, Endness.ANY, ArchPPC32), and for an entry whose endness is ANY a call that names none is answered with cls(cls.default_endness). The identifier string carries no byte order, so the endness of a PowerPC PE is decided by an architecture's default rather than by the container format that knows the answer.

Fix

Both sites go through one module-level helper in cle/backends/pe/pe.py that passes Endness.LE for those two machine types and Endness.ANY -- arch_from_id's own default -- for everything else. Each site keeps the lookup it had: PE.__init__ still falls back to hex(machine_type) for a machine pefile does not name, and check_compatibility still indexes the table directly. Of the 35 machine types pefile names, plus three numbers it does not, 36 resolve to exactly the same architecture as on master; the two that move are 0x1f0 and 0x1f1, from big-endian to little-endian. The full before and after tables are in the output comment. archinfo's PowerPC default is left alone on purpose: angr/archinfo#380 set it deliberately, and the byte order of a PE is the container's to state.

Testing

TestPEMachineTypes in tests/test_pe.py asserts that both PowerPC machine types resolve to a 32-bit little-endian PPC32. Take the endness argument back out of the helper and it fails -- AssertionError: assert <Endness.BE: 'Iend_BE'> == <Endness.LE: 'Iend_LE'>. It asserts the mapping instead of loading a PE because angr/binaries has no PowerPC one: of the 1,834 blobs tracked at 3de2c41a, 106 carry a valid PE\0\0 signature, and their machine types are 0x8664, 0x14c, 0xaa64, 0x1c4 and 0x5064.

#757 rewrites the same two call sites for an unrelated reason, so the two conflict; whichever merges second needs a rebase.

Validation: #805 (comment)

session: sharpen

@zardus

zardus commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Every machine type in pefile.MACHINE_TYPE, and three numbers it does not name, resolved through the mapping cle/backends/pe/pe.py uses, before and after this change. Each side prints one row per machine type -- the class, name, bits and memory_endness, or ArchNotFound -- from the same script, with the checkout on PYTHONPATH deciding which side runs:

side = "cle states the endness" if hasattr(pe, "arch_from_machine_type") else "cle master"
resolve = getattr(pe, "arch_from_machine_type", None) or (lambda ident: archinfo.arch_from_id(ident))

Both sides ran against archinfo master 384c676de767e1216a7fee2a08f2d549ff2a1028, where ArchPPC32.default_endness is Iend_BE.

Before -- on cle master 0e77ade3c39a3cee05f65051e57955675e1ac21b the two PowerPC machine types come back big-endian, following archinfo's default:

cle master
# cle master; ArchPPC32.default_endness = Iend_BE
0x00000 IMAGE_FILE_MACHINE_UNKNOWN       ArchNotFound
0x0014c IMAGE_FILE_MACHINE_I386          ArchX86 name=X86 bits=32 mem=Iend_LE
0x00162 IMAGE_FILE_MACHINE_R3000         ArchNotFound
0x00166 IMAGE_FILE_MACHINE_R4000         ArchNotFound
0x00168 IMAGE_FILE_MACHINE_R10000        ArchNotFound
0x00169 IMAGE_FILE_MACHINE_WCEMIPSV2     ArchMIPS32 name=MIPS32 bits=32 mem=Iend_BE
0x00184 IMAGE_FILE_MACHINE_ALPHA         ArchNotFound
0x001a2 IMAGE_FILE_MACHINE_SH3           ArchNotFound
0x001a3 IMAGE_FILE_MACHINE_SH3DSP        ArchNotFound
0x001a4 IMAGE_FILE_MACHINE_SH3E          ArchNotFound
0x001a6 IMAGE_FILE_MACHINE_SH4           ArchNotFound
0x001a8 IMAGE_FILE_MACHINE_SH5           ArchNotFound
0x001c0 IMAGE_FILE_MACHINE_ARM           ArchARMEL name=ARMEL bits=32 mem=Iend_LE
0x001c2 IMAGE_FILE_MACHINE_THUMB         ArchARM name=ARMEL bits=32 mem=Iend_LE
0x001c4 IMAGE_FILE_MACHINE_ARMNT         ArchARMEL name=ARMEL bits=32 mem=Iend_LE
0x001d3 IMAGE_FILE_MACHINE_AM33          ArchNotFound
0x001f0 IMAGE_FILE_MACHINE_POWERPC       ArchPPC32 name=PPC32 bits=32 mem=Iend_BE
0x001f1 IMAGE_FILE_MACHINE_POWERPCFP     ArchPPC32 name=PPC32 bits=32 mem=Iend_BE
0x00200 IMAGE_FILE_MACHINE_IA64          ArchNotFound
0x00266 IMAGE_FILE_MACHINE_MIPS16        ArchMIPS32 name=MIPS32 bits=32 mem=Iend_BE
0x00284 IMAGE_FILE_MACHINE_AXP64         ArchNotFound
0x00366 IMAGE_FILE_MACHINE_MIPSFPU       ArchMIPS32 name=MIPS32 bits=32 mem=Iend_BE
0x00466 IMAGE_FILE_MACHINE_MIPSFPU16     ArchMIPS32 name=MIPS32 bits=32 mem=Iend_BE
0x00520 IMAGE_FILE_MACHINE_TRICORE       ArchNotFound
0x00cef IMAGE_FILE_MACHINE_CEF           ArchNotFound
0x00ebc IMAGE_FILE_MACHINE_EBC           ArchNotFound
0x05032 IMAGE_FILE_MACHINE_RISCV32       ArchNotFound
0x05064 IMAGE_FILE_MACHINE_RISCV64       ArchRISCV64 name=RISCV64 bits=64 mem=Iend_LE
0x05128 IMAGE_FILE_MACHINE_RISCV128      ArchRISCV64 name=RISCV64 bits=64 mem=Iend_LE
0x06232 IMAGE_FILE_MACHINE_LOONGARCH32   ArchNotFound
0x06264 IMAGE_FILE_MACHINE_LOONGARCH64   ArchNotFound
0x08664 IMAGE_FILE_MACHINE_AMD64         ArchAMD64 name=AMD64 bits=64 mem=Iend_LE
0x09041 IMAGE_FILE_MACHINE_M32R          ArchNotFound
0x0aa64 IMAGE_FILE_MACHINE_ARM64         ArchAArch64 name=AARCH64 bits=64 mem=Iend_LE
0x0c0ee IMAGE_FILE_MACHINE_CEE           ArchNotFound
0x001f3 0x1f3                            ArchNotFound
0x04242 0x4242                           ArchNotFound
0x0dead 0xdead                           ArchNotFound

After -- the two PowerPC machine types are little-endian, and every other machine-type row is byte-identical:

with this change
# cle states the endness; ArchPPC32.default_endness = Iend_BE
0x00000 IMAGE_FILE_MACHINE_UNKNOWN       ArchNotFound
0x0014c IMAGE_FILE_MACHINE_I386          ArchX86 name=X86 bits=32 mem=Iend_LE
0x00162 IMAGE_FILE_MACHINE_R3000         ArchNotFound
0x00166 IMAGE_FILE_MACHINE_R4000         ArchNotFound
0x00168 IMAGE_FILE_MACHINE_R10000        ArchNotFound
0x00169 IMAGE_FILE_MACHINE_WCEMIPSV2     ArchMIPS32 name=MIPS32 bits=32 mem=Iend_BE
0x00184 IMAGE_FILE_MACHINE_ALPHA         ArchNotFound
0x001a2 IMAGE_FILE_MACHINE_SH3           ArchNotFound
0x001a3 IMAGE_FILE_MACHINE_SH3DSP        ArchNotFound
0x001a4 IMAGE_FILE_MACHINE_SH3E          ArchNotFound
0x001a6 IMAGE_FILE_MACHINE_SH4           ArchNotFound
0x001a8 IMAGE_FILE_MACHINE_SH5           ArchNotFound
0x001c0 IMAGE_FILE_MACHINE_ARM           ArchARMEL name=ARMEL bits=32 mem=Iend_LE
0x001c2 IMAGE_FILE_MACHINE_THUMB         ArchARM name=ARMEL bits=32 mem=Iend_LE
0x001c4 IMAGE_FILE_MACHINE_ARMNT         ArchARMEL name=ARMEL bits=32 mem=Iend_LE
0x001d3 IMAGE_FILE_MACHINE_AM33          ArchNotFound
0x001f0 IMAGE_FILE_MACHINE_POWERPC       ArchPPC32 name=PPC32 bits=32 mem=Iend_LE
0x001f1 IMAGE_FILE_MACHINE_POWERPCFP     ArchPPC32 name=PPC32 bits=32 mem=Iend_LE
0x00200 IMAGE_FILE_MACHINE_IA64          ArchNotFound
0x00266 IMAGE_FILE_MACHINE_MIPS16        ArchMIPS32 name=MIPS32 bits=32 mem=Iend_BE
0x00284 IMAGE_FILE_MACHINE_AXP64         ArchNotFound
0x00366 IMAGE_FILE_MACHINE_MIPSFPU       ArchMIPS32 name=MIPS32 bits=32 mem=Iend_BE
0x00466 IMAGE_FILE_MACHINE_MIPSFPU16     ArchMIPS32 name=MIPS32 bits=32 mem=Iend_BE
0x00520 IMAGE_FILE_MACHINE_TRICORE       ArchNotFound
0x00cef IMAGE_FILE_MACHINE_CEF           ArchNotFound
0x00ebc IMAGE_FILE_MACHINE_EBC           ArchNotFound
0x05032 IMAGE_FILE_MACHINE_RISCV32       ArchNotFound
0x05064 IMAGE_FILE_MACHINE_RISCV64       ArchRISCV64 name=RISCV64 bits=64 mem=Iend_LE
0x05128 IMAGE_FILE_MACHINE_RISCV128      ArchRISCV64 name=RISCV64 bits=64 mem=Iend_LE
0x06232 IMAGE_FILE_MACHINE_LOONGARCH32   ArchNotFound
0x06264 IMAGE_FILE_MACHINE_LOONGARCH64   ArchNotFound
0x08664 IMAGE_FILE_MACHINE_AMD64         ArchAMD64 name=AMD64 bits=64 mem=Iend_LE
0x09041 IMAGE_FILE_MACHINE_M32R          ArchNotFound
0x0aa64 IMAGE_FILE_MACHINE_ARM64         ArchAArch64 name=AARCH64 bits=64 mem=Iend_LE
0x0c0ee IMAGE_FILE_MACHINE_CEE           ArchNotFound
0x001f3 0x1f3                            ArchNotFound
0x04242 0x4242                           ArchNotFound
0x0dead 0xdead                           ArchNotFound

The whole diff between the two:

1c1
< # cle master; ArchPPC32.default_endness = Iend_BE
---
> # cle states the endness; ArchPPC32.default_endness = Iend_BE
18,19c18,19
< 0x001f0 IMAGE_FILE_MACHINE_POWERPC       ArchPPC32 name=PPC32 bits=32 mem=Iend_BE
< 0x001f1 IMAGE_FILE_MACHINE_POWERPCFP     ArchPPC32 name=PPC32 bits=32 mem=Iend_BE
---
> 0x001f0 IMAGE_FILE_MACHINE_POWERPC       ArchPPC32 name=PPC32 bits=32 mem=Iend_LE
> 0x001f1 IMAGE_FILE_MACHINE_POWERPCFP     ArchPPC32 name=PPC32 bits=32 mem=Iend_LE

@zardus

zardus commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 03e94c845d1b3e8a1b6d87fdfb4f7ad3f967bbef against baseline 0e77ade3c39a3cee05f65051e57955675e1ac21b. Each suite and probe arm was run against its own tree, with that tree and archinfo master 384c676de767e1216a7fee2a08f2d549ff2a1028 on PYTHONPATH, so cle and archinfo resolve to the checkouts under test and not to an installed copy. The lint and type arms are the exception: they run under the workspace predictor's own configuration, which is what the closing note is about. angr/binaries was at 3de2c41a297606c45be7985aa09b3ae615424d61.

  • Live defect: on the baseline, archinfo.arch_from_id(pefile.MACHINE_TYPE[0x1f0]).memory_endness is Iend_BE, and the same for 0x1f1. This is the state since Treat an unqualified PowerPC identifier as big-endian archinfo#380 merged at 2026-09-01T16:20:48Z; at that commit's parent cdc01d5 the same probe returns Iend_LE
  • Regression: pytest tests/test_pe.py -k TestPEMachineTypes -- 1 passed, 15 deselected on head. It cannot run on the baseline, where the helper it imports does not exist; the control is the head with the endness= argument removed from that helper, which gives 1 failed, 15 deselected and AssertionError: assert <Endness.BE: 'Iend_BE'> == <Endness.LE: 'Iend_LE'>
  • Focused: pytest tests/test_pe.py -- 16 passed on head
  • Full suite: pytest tests -- 3 failed, 259 passed, 9 skipped on head; 3 failed, 258 passed, 9 skipped on the baseline. The same three fail on both sides and are not this branch's: test_gopclntab.py::TestGoPclntab::test_macho_binary, ::test_macho_binary_supplies_the_function_symbols and test_macho.py::test_relocatable_object, each a CLEFileNotFoundError for an aarch64 Mach-O this machine's angr/binaries checkout does not have. The one-test difference is the new TestPEMachineTypes
  • Equivalence: enumerate_machine_types.py over all 35 machine types pefile names plus three numbers it does not -- 36 rows identical between baseline and head, 2 differ, and they are 0x1f0 and 0x1f1 going from Iend_BE to Iend_LE. The tables are in the output comment
  • Fixture survey: 106 of the 1,834 blobs tracked in angr/binaries carry a valid PE\0\0 signature at their e_lfanew offset; machine types 0x8664 x54, 0x14c x44, 0xaa64 x6, 0x1c4 x1, 0x5064 x1, and no PowerPC
  • Lint/type: the repository's own pinned pre-commit, run --all-files -- exit 0, 22 hooks passed, 2 skipped for having no files to check, and no file modified; merge-base pylint 10.00 -> 10.00 and pyright errors 52 -> 52 on cle/backends/pe/pe.py, 10.00 -> 10.00 and 4 -> 4 on tests/test_pe.py
  • Test inputs: the workspace check-test-inputs gate -- the branch adds no binary and no manufactured input outside angr/binaries

Caveats: the gate was run at cle scope -- this repository's full suite, its full hook set over every file, and the merge-base lint and type comparison. The angr, pyvex, claripy, archinfo and GUI suites were not run: the change is two call sites in one loader plus a table test, and it removes no API. The Windows, macOS and Pyodide legs run on hosted CI only. Running pylint without the branch on PYTHONPATH reports a spurious E0611: No name 'arch_from_machine_type' in module 'cle.backends.pe.pe', because cle then resolves to an installed copy at master; with the tree under test on the path both files score 10.00 on both sides.

@angr-bot

angr-bot commented Sep 1, 2026

Copy link
Copy Markdown
Member

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

Windows NT on PowerPC was little-endian. Both PE call sites that turn a
file header's machine type into an Arch called archinfo.arch_from_id with
no endness, so IMAGE_FILE_MACHINE_POWERPC (0x1f0) and
IMAGE_FILE_MACHINE_POWERPCFP (0x1f1) came back with whatever ArchPPC32
defaults to. angr/archinfo#380 made that default big-endian, so cle reads
a Windows NT PowerPC PE with its bytes the wrong way round today.

Route both sites through one helper that passes Endness.LE for those two
machine types and changes nothing else. Of the 35 machine types pefile
names, plus three numbers it does not, 36 resolve to exactly the same Arch
as before; the two that change are 0x1f0 and 0x1f1, from big-endian to
little-endian.

angr/binaries has no PowerPC PE: of the 1,834 blobs tracked at 3de2c41a,
106 carry a valid PE signature and their machine types are 0x8664 (54),
0x14c (44), 0xaa64 (6), 0x1c4 and 0x5064. The test therefore asserts the
machine type mapping directly instead of loading a fixture. Take the
endness argument back out and it fails, because archinfo answers
big-endian.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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