Skip to content

Compile patch code for the target architecture, not the build host - #40

Merged
DennyDai merged 1 commit into
purseclab:mainfrom
trail-of-forks:upstream/issue-39-target-triple
Aug 15, 2026
Merged

Compile patch code for the target architecture, not the build host#40
DennyDai merged 1 commit into
purseclab:mainfrom
trail-of-forks:upstream/issue-39-target-triple

Conversation

@xintenseapple

Copy link
Copy Markdown
Contributor

Addresses #39. (Deliberately not using a closing keyword — please close the issue yourself if you take this, or close it in favour of your own fix.)

Summary

ElfAmd64Linux and ElfX86Linux pass no --target triple to clang, so InsertFunctionPatch / ModifyFunctionPatch compile the patch body for the build host rather than the binary being patched. On a non-x86 host that writes a function in the wrong instruction set into the target at full size, with nothing raised anywhere. On an x86 host the omission is invisible because the host default happens to match.

-m32 on ElfX86Linux is worth calling out separately: it reads as architecture-selecting but only sets the width, so on an arm64 host it produces 32-bit ARM.

Changes

1. Pin the triple — matching the targets that already do:

Target Flag
elf_amd64_linux (clang, clang19) -target x86_64-linux-gnu
elf_x86_linux -target i386-linux-gnu (replaces -m32)
elf_amd64_linux_recomp -target x86_64-linux-gnu
elf_arm_linux_recomp -target arm-linux-gnueabihf

The two _recomp variants had the same gap. Their -emit-llvm step embeds the triple in the .ll, so pinning the compiler flag fixes the downstream llc invocation too.

2. Check the compiled object's machine against the target's, and raise on a mismatch.

Compiler.check_object_arch() compares the object's e_machine, EI_CLASS and EI_DATA against the target's expectation, raising ObjectArchMismatchError. It is called from both Compiler.compile and LLVMRecomp.compile at the point where the ELFFile handle is already open for the .rodata section walk, so the header read costs nothing.

This is the part that matters more than (1): a wrong-ISA patch that reports success is the worst failure mode available here, and it is not detectable from the patcherex2 API by a caller. The check also covers a target whose triple is wrong rather than missing, and any future host/target divergence.

Targets declare their expectation via a new Target.expected_object_arch; None (the default) skips the check, so nothing is imposed on targets that do not opt in. All targets now declare one — the ARM-derived (elf_arm_bare, elf_arm_mimxrt1052) and _recomp targets inherit theirs from their base class.

IHexRiscv32Bare and BinPpcPegasos2Bare already pinned their triples correctly and were never affected by the bug; they just had no expectation, so they got no check. Their declared values were verified against what clang actually emits for their real compiler flags rather than inferred from the triple.

Verification

Reproduced and fixed on a native arm64 Linux host (clang 15), the condition from the issue:

Flags Resulting machine
old amd64 (none) EM_AARCH64
new -target x86_64-linux-gnu EM_X86_64
old x86 -m32 EM_ARM
new -target i386-linux-gnu EM_386

With the triple reverted, the new check raises instead of emitting a silently broken patch:

Compiled patch code does not match the target architecture
(e_machine: expected EM_X86_64, got EM_AARCH64). This usually means the
compiler is missing or has the wrong target triple, and the patch would
not run on the binary being patched. Compiler flags: []

New tests in tests/test_compiler_arch.py (42 passing) cover wrong machine, wrong class, wrong endianness, the no-expectation passthrough, that every target declares an expectation consistent with the binaries it detects, and that each target's configured compiler emits an object for its own architecture. They drive the compiler with a stub p rather than a full Patcherex, so they do not depend on a binary analyzer.

test_every_registered_target_declares_expectation guards the gap that let the two newer targets slip through: a missing expectation silently disables the check rather than erroring. I confirmed it fails when a declaration is removed, rather than passing vacuously.

Full suite on this branch: 506 passed, 67 skipped, 0 failed (all architectures, both angr and ghidra). ruff format --check and ruff check clean.

This touches no files that d9106f4 (#37) touched — utils.py is untouched here.

Note

IHexRiscv32Bare is absent from targets/__init__.py, so it never registers in Target.target_classes; the new coverage test cannot see it and Target.detect_target cannot reach it either. Pre-existing and out of scope for this PR — its expectation is set regardless — but it likely wants a one-line fix separately.

🤖 Generated with Claude Code

* Compile patch code for the target arch, not the build host

ElfAmd64Linux and ElfX86Linux passed no --target triple to clang, so
InsertFunctionPatch / ModifyFunctionPatch compiled the patch body for the
build host. On a non-x86 host that yields a function in the wrong
instruction set, written into the target at full size, with nothing
raised. On an x86 host the omission is invisible because the host default
happens to match. ElfX86Linux's -m32 reads as architecture-selecting but
only sets the width, so on an arm64 host it produced 32-bit ARM.

Pin the triple on both, matching the eleven targets that already do, and
on the two _recomp variants which had the same gap.

Also check the compiled object's machine, class and data encoding against
the target's in Compiler.compile and LLVMRecomp.compile, raising
ObjectArchMismatchError on a mismatch. The ELFFile handle is already open
there for the .rodata walk, so the header read costs nothing. This turns
any future host/target divergence into an error rather than a corrupt
patch, including a target whose triple is wrong rather than missing --
a wrong-ISA patch reporting success is not detectable from the API by a
caller.

Targets declare their expectation via a new Target.expected_object_arch;
None (the default) skips the check.

Verified on a native arm64 Linux host: the old amd64 flags produced
EM_AARCH64 and the old -m32 produced EM_ARM, while the pinned triples
produce EM_X86_64 and EM_386. With a triple reverted, the check raises
rather than emitting a silently broken patch.

Fixes purseclab#39

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Declare arch expectation for the riscv32 and pegasos2 targets

Both targets already pin their triples correctly, so they were never
affected by the wrong-ISA bug; they just had no expected_object_arch, so
they fell back to the permissive default and got no mismatch check.

Declared values verified against what clang actually emits for each
target's real compiler flags, not inferred from the triple:

  riscv32-unknown-elf  -> EM_RISCV ELFCLASS32 ELFDATA2LSB
  powerpc-unknown-elf  -> EM_PPC   ELFCLASS32 ELFDATA2MSB
  (with -mcpu=7450 -mbig-endian -ffreestanding -nostdlib -fno-pic
   -fno-builtin, as get_compiler passes)

Add test_every_registered_target_declares_expectation, which fails if any
registered target lacks a declaration. These two slipped in precisely
because nothing enforced that, and a missing expectation is invisible --
it silently disables the check rather than erroring.

Note that IHexRiscv32Bare is absent from targets/__init__.py on main, so
it never registers in Target.target_classes and the new test cannot see
it. Pre-existing and out of scope here; its expectation is set regardless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@xintenseapple
xintenseapple force-pushed the upstream/issue-39-target-triple branch from e3e957c to 70296fc Compare August 5, 2026 17:29
@DennyDai
DennyDai merged commit 682b8fe into purseclab:main Aug 15, 2026
4 checks passed
DennyDai added a commit that referenced this pull request Aug 15, 2026
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