Files
one_gadget/spec
david942jandClaude Opus 4.8 4732208300 Conditional-branch-aware gadget search (#321)
* fetcher: promote disasm index to Base

Move disasm_lines/disasm_index (full objdump disassembly + address→index
map) from the ARM fetcher into Base so any arch can walk the disassembly,
and extract the terminal-call regexp into a helper. Groundwork for
control-flow-aware candidate discovery; no behavior change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* Add conditional-branch-aware gadget search (aarch64)

Gadgets in newer glibc are often split across a conditional branch (e.g.
2.43's execvp materialises "/bin/sh" before `cmp x2, #1; b.ne`), which the
straight-line-only search could not find.

- fetcher: enumerate candidates by walking the CFG *backward* from each
  terminal call along its predecessors (fall-through + branch targets),
  forking at conditional edges up to MAX_FORKS(4). Gated per-arch by
  follow_branches? (aarch64 on; others keep slice_prefix for now).
- emulators: shared Conditional module models cmp/cmn/tst + b.<cond>/cbz/
  cbnz/tbz/tbnz, turning each crossed edge into a constraint via one-line-
  ahead resolution (full relational: ==,!=,signed/unsigned </>/<=/>=, bit
  tests). Sound: abort a path we can't express (e.g. no preceding compare).
- aarch64: inst_stp now handles non-sp stores like inst_str (needed to reach
  the argv-building block); resolve argv/envp fall back when a stack register
  has no tracked stack (frame pointer).
- gadget: score branch conditions.

2.43 now yields execve("/bin/sh", x4, x6) guarded by `x2 == 0x1`. Existing
aarch64 gadgets are unchanged or added to (fixtures updated); other arches
untouched.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* emulators: conditional-branch-aware search for arm

Enable follow_branches? for arm: model cmp/cmn/tst (previously nop-aliased)
plus bne/beq/... condition-suffix branches and Thumb cbz/cbnz, reusing the
shared Conditional module. Add arm branch classification (path_ends? covers
bx / pop{..pc} / mov pc returns).

Fixes surfaced by the new paths: guard inst_ldr/inst_str write-back against a
non-register base, and skip posix_spawn candidates whose pid/file_actions/attrp
is a concrete integer (a fixed address we can't constrain).

arm gadgets are unchanged or added to (2.27, 2.39 each gain one; fixtures
updated); e.g. 2.27 0x73f2c is now found guarded by `[r1] == 0`.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* Extend conditional-branch-aware search to x86 (amd64/i386)

Model cmp/test + the full jcc set (and jcxz) in the x86 emulator, reusing
the shared Conditional module; retire amd64's jmp_case_candidates (the CFG
walker subsumes it). With every arch now branch-aware, drop the dead
straight-line candidates path, slice_prefix and branch?.

Soundness/robustness fixes exposed by the richer x86 paths:
- drop gadgets whose branch condition is unsatisfiable (X != X) and strip
  always-true ones (X == X);
- dedup gadgets by offset (a branch can reach the call either way), keeping
  the easiest variant;
- treat a resolve-time eval error as "not a gadget";
- Lambda.parse: model absolute [addr]; keep xmm arrays / fall back only for a
  call-target string base;
- guard the SSE handlers, inst_sub, check_envp against non-Lambda operands.

amd64/i386 now find a superset of the previous gadgets (fixtures updated for
the force_file paths; prebuilt-DB assertions unchanged).

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* docs: note branch-aware gadget search in README

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* perf: speed up branch-aware gadget search

The backward CFG walk produces many overlapping candidate paths, and find()
re-emulates every start line in each; the extra work made the specs notably
slower. All behaviour-preserving (gadget output is unchanged):

- find(): memoize by candidate suffix so shared tails are emulated once
  (~57% of starts were duplicates).
- Processor#parse: build the instruction set (and a mnemonic index) once
  instead of re-allocating it per line.
- CFG walk: precompute each line's branch classification and address once
  (line_kinds/disasm_addrs/predecessors_map) rather than re-running the
  per-arch regexes on every visit; walk with a backtracking Set instead of
  copying visited/path each step; precompile the terminal-call regex.
- ARM emulator: cache the libc bytes instead of re-reading the file for every
  emulator instance.

~37% faster on the integration specs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* perf: cut branch-aware setup cost on large libcs

The previous pass eagerly classified every disassembly line (line_kinds /
predecessors_map) — costly on big libcs (aarch64 2.43 has 310k lines). Rework
so the whole-binary scan is cheap and the per-line classification only runs
where the walk actually goes. Behaviour-preserving.

- predecessors: computed lazily and memoized (the walk only visits lines near
  terminal calls), instead of precomputing for all lines.
- branch_pred_map: reject the ~99% non-branch lines with one precompiled
  per-arch regex (branch_lead_regex) before the full test, and drop the
  mnemonic memoization that was allocating an entry per line during this scan.
- disasm_cache: cache objdump output by command so re-analysing the same file
  (the specs load some libcs several times; the CLI reads a file once) skips
  the disassembly.

~45% faster on the full suite (60.7s -> 33.6s here).

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* perf: windowed disassembly for arm (byte-scan the calls)

A full objdump of a Thumb-2 arm libc is ~4x slower than amd64/aarch64
(~1.1s vs ~0.3s), yet the branch-aware search only ever touches ~0.35% of
the disassembly — everything it needs sits within ~0x1246 bytes of an
exec*/posix_spawn* call (measured max: 0x98b backward span + 0x8bb branch-in
across real amd64/aarch64/arm libcs).

So for arm we locate the calls *without* disassembling: take the
exec*/posix_spawn* addresses from the symbol table and scan .text for
A32/Thumb `BL` instructions branching to them, then objdump only a 0x2000
window around each (merging overlaps). The scan is exact for the direct
`bl <exec*>` calls the search cares about — validated to miss none of the
objdump-grep sites on the 2.23/2.27/2.39 fixtures (a rare false positive just
adds an empty window); if it finds nothing we fall back to a full disassembly.

Behaviour-preserving: gadget output is identical (the arm integration specs
assert exact lists). Single-libc time drops ~52-57% (2.39: 2.84s -> 1.32s;
2.23: 1.70s -> 0.73s). Only arm opts in — the fast-to-objdump arches keep the
exhaustive full scan.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* refactor: one branch_kind per arch instead of four predicates

A new architecture had to implement four overlapping methods to classify a
line for the CFG walk (conditional_branch?, unconditional_branch?, path_ends?,
branch_lead_regex). Collapse them into one intuitive classifier:

  branch_kind(line) -> :conditional | :unconditional | :terminator | nil

Base derives the predicates it needs from it and builds branch_lead_regex from
a tiny branch_lead_chars hint. No behavior change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* refactor: share compare/operand helpers across emulators

handle_compare, operand_str and mnemonic were byte-for-byte identical in the
aarch64/arm/x86 emulators (x86's `test ? :test : :cmp` is just `mnem.to_sym`).
Move them into the shared Conditional module so a new arch only supplies the
genuinely arch-specific parts: which mnemonics are compares/branches
(branch_mnem?), how to split operands (operands), and how a branch maps to a
queued decision (handle_branch). No behavior change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* docs: guide for adding a new architecture

The arch surface is spread over a Fetcher and an Emulator subclass; collect the
full contract in one place (docs/adding-an-architecture.md) and point the two
base classes at it, so it's clear what a new arch must implement.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* docs: add ruby lang hint to code block

* style: keep lib comments ASCII, enforce via rubocop

* docs: add @example usage to Conditional methods

* docs: prefer example/param tags over inline e.g.

* refactor: name branch conditions by predicate, adapters per arch

* refactor: share ARM-family emulator logic via ArmFamily module

* emulators: model each compare's ALU op, not just cmp

Keys compares by ALU op (:sub/:add/:and) via each arch's COMPARES, dropping the
:cmp special case so a new arch just declares its compare mnemonics. Fixes the
test/tst mis-rendering this exposed: test r,r; jne rendered r != r (a false
contradiction that dropped valid gadgets), test r,r; jz an unsound r == r
tautology, and cmn now allows ordered conditions. Recovers gadgets on amd64
2.19/2.31.

* emulators: tighten Conditional per review

Rename COMPARE -> COMPARE_OPS so it no longer collides with the arch COMPARES
mnemonic map; co-locate each op's renderer in the table and dispatch via fetch,
so an unknown op raises loudly instead of yielding a nil constraint. Document
Conditional's required host interface and branch_lead_chars' regex-safety contract.

* docs: add class + pipeline diagrams to the arch guide

* refactor: rename queue_* branch helpers to branch_on_*

queue_cond_branch -> branch_on_compare, queue_cbz -> branch_on_zero,
queue_tbz -> branch_on_bit. The names now describe what the branch tests
(a recorded compare / a register being zero / a single bit) instead of the
ARM mnemonic, and drop the misleading 'queue' (there is only one pending
branch, not a queue).

* docs: keep Conditional statements arch-neutral

Move arch mnemonics (cmp/cbz/b.ne/jcc/tst/...) out of the descriptive prose,
param and return docs into the @example blocks; the statements now say what each
method does and why in arch-independent terms, and the render_* / COMPARE_OPS
comments are labelled by ALU op rather than mnemonic.

---------

Co-authored-by: Claude Opus 4.8 <[email protected]>
2026-07-25 10:20:47 +08:00
..
2024-10-05 01:15:02 +08:00
2026-07-19 16:15:41 +08:00