Compile patch code for the target architecture, not the build host - #40
Merged
DennyDai merged 1 commit intoAug 15, 2026
Merged
Conversation
xintenseapple
force-pushed
the
upstream/issue-39-target-triple
branch
from
August 5, 2026 17:20
10edce4 to
e3e957c
Compare
* 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
force-pushed
the
upstream/issue-39-target-triple
branch
from
August 5, 2026 17:29
e3e957c to
70296fc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
ElfAmd64LinuxandElfX86Linuxpass no--targettriple to clang, soInsertFunctionPatch/ModifyFunctionPatchcompile 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.-m32onElfX86Linuxis 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:
elf_amd64_linux(clang,clang19)-target x86_64-linux-gnuelf_x86_linux-target i386-linux-gnu(replaces-m32)elf_amd64_linux_recomp-target x86_64-linux-gnuelf_arm_linux_recomp-target arm-linux-gnueabihfThe two
_recompvariants had the same gap. Their-emit-llvmstep embeds the triple in the.ll, so pinning the compiler flag fixes the downstreamllcinvocation too.2. Check the compiled object's machine against the target's, and raise on a mismatch.
Compiler.check_object_arch()compares the object'se_machine,EI_CLASSandEI_DATAagainst the target's expectation, raisingObjectArchMismatchError. It is called from bothCompiler.compileandLLVMRecomp.compileat the point where theELFFilehandle is already open for the.rodatasection 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_recomptargets inherit theirs from their base class.IHexRiscv32BareandBinPpcPegasos2Barealready 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:
EM_AARCH64✗-target x86_64-linux-gnuEM_X86_64✓-m32EM_ARM✗-target i386-linux-gnuEM_386✓With the triple reverted, the new check raises instead of emitting a silently broken patch:
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 stubprather than a fullPatcherex, so they do not depend on a binary analyzer.test_every_registered_target_declares_expectationguards 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 --checkandruff checkclean.This touches no files that
d9106f4(#37) touched —utils.pyis untouched here.Note
IHexRiscv32Bareis absent fromtargets/__init__.py, so it never registers inTarget.target_classes; the new coverage test cannot see it andTarget.detect_targetcannot 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