Skip to content

[AUTOMATED] fix(p2): explicit-function-boundary-aborts — a declared extent clips flow instead of killing the function - #430

Merged
mahaloz merged 1 commit into
mainfrom
feat/re-explicit-function-boundary-aborts
Sep 6, 2026
Merged

[AUTOMATED] fix(p2): explicit-function-boundary-aborts — a declared extent clips flow instead of killing the function#430
mahaloz merged 1 commit into
mainfrom
feat/re-explicit-function-boundary-aborts

Conversation

@mahaloz

@mahaloz mahaloz commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

The problem

--define-function START-END is how an agent tells kuna where a function really
is on a packed or obfuscated image. Declaring an end that any branch crosses
killed the whole function instead of clipping it:

$ kuna decompile decompiler/crates/kuna-analysis/tests/fixtures/aif_gap_x86_64 \
      0x1070 --addr --define-function 0x1070-0x1098=deregister
error: Could not find op at target address: (ram,0x00001098)
$ echo $?
1

0x1098 is the declared exclusive end, and 0x1081 is JZ 0x1098 — an
ordinary forward conditional over the tail. --mode reliable and --mode fast
fail identically. docs/cli.md already promised the opposite ("a declared end
that cuts real control flow is reported rather than silently truncating the
body"), and the warning it promises exists; nothing could reach it.

The second half is worse and shows up on correct boundaries. Declaring the
extent kuna itself derived should be free; it deleted the last instruction:

$ kuna decompile ...aif_gap_x86_64 sub_1129                 # derived extent, undeclared
int sub_1129(int a0)
{
  return (a0 + 10) * 2;
}
$ kuna decompile ...aif_gap_x86_64 sub_1129 --define-function 0x1129-0x1141
void sub_1129(void) // warn: Function flows out of bounds
{ // warn: Function flow out of bounds: r0x00001140 flows to r0x00001140
}

The fix

Both live in FlowInfo (p2_lift/flow.rs); both are inert unless a caller
declares an extent, because set_range is the only thing that narrows the flow
range and only a declared extent calls it.

  • A branch target outside the extent resolves to its stub.
    fillin_branch_stubs already plants a missing artificial halt at every
    referenced-but-undecoded address; it is now also registered in visited as
    the instruction there, so collect_edges hangs the cut edge on it and the
    body ends at the boundary under the Function flows out of bounds header.
  • Only the out-of-extent subset gets that. The addresses
    handle_out_of_bounds recorded are tracked separately. An unprocessed
    address inside the extent means an op that should exist does not, and
    resolving that to a halt would shorten a function instead of reporting the
    defect — it keeps upstream's throw. (That is the distinct open need
    default-decompilation-fails-despite, whose missing target is an in-extent
    NOP; its acceptance still fails, deliberately.)
  • eaddr is the last in-body byte, so an instruction starting on it is in
    range.
    The fall-through bound tested bound <= addrlist.back() with
    bound == eaddr, which upstream can only hit at the top of memory but which
    under a declared extent is every function's last instruction. It now decodes
    that instruction and catches the fall-through past it on the next lap, where
    the address really is above eaddr.

The tests

tests/cli/explicit-function-boundary-aborts.json is the promoted acceptance,
re-aimed at the in-repo fixture above (the dataset witness is a 7 MB PE CI has
no copy of; same shape, same clauses). Plus three cargo tests, each failing
without the change: the branch-cut case and the declared-derived-extent oracle
in kuna-console/tests/verify_funcbounds.rs, and the in-extent/out-of-extent
split in kuna-decomp/tests/verify_w3_ir_flow.rs.

Measured over the 112 binary fixtures under kuna-analysis/tests/fixtures,
declaring every discovered function at its derived extent (2,765 functions):
bodies differing from the undeclared decompile 403 → 138, functions that
hard-error under the declaration 142 → 0. With nothing declared,
decompile-all --json over the same 112 binaries is 112/112 byte-identical.

Gates: make test 675/675 PARITY OK, make test-stages 635/635 PARITY OK,
make rust-test green, make check-spec OK (lenient + strict), make test-cli
35/35, kuna catalog --check OK. No new option, no catalog counter, no stages
XML.

🤖 Generated with Claude Code


Before / after — None in ?

Real captured kuna output on the function this feature was built for, with option None flipped. Ported from angr.

metric None off (default) None on
gotos 0 0
labels 0 0
loc 1 1
switches 0 0
kuna — before (option None off)
// kuna decompilation unavailable (no name/address)
kuna — after (option None on)
// kuna decompilation unavailable (no name/address)

…xtent clips flow instead of killing the function

`--define-function START-END` declares where a function is on a packed or
obfuscated image. Declaring an end that any branch crossed produced no C at all:
the walk deliberately never decodes an out-of-extent address, `fillin_branch_stubs`
plants a `missing` artificial halt there but nothing registered it in `visited`,
and `FlowInfo::target` resolves through `visited` alone — so `collect_edges` threw
`Could not find op at target address` on the first cut edge, in every mode. The
halt is now registered as the instruction at its address, so the edge lands on it
and the body ends at the boundary under the `Function flows out of bounds` header
that was already being emitted. Restricted to the addresses `handle_out_of_bounds`
recorded: an unprocessed address INSIDE the extent means an op that should exist
does not, and clipping there would shorten a correct function instead of reporting
the defect.

The collateral sweep found a second, larger defect in the same mechanism. `eaddr`
is the last IN-BODY byte, but `FlowInfo::fallthru` reported out of bounds when the
next address equalled it — upstream can only hit that at the top of memory, since
its `eaddr` is the space's highest address, but under a declared extent it is every
function's last instruction. So a CORRECT extent never decoded the closing `ret`:
`aif_gap_x86_64 sub_1129` came out as an empty `void sub_1129(void)` under a bogus
warning instead of `int sub_1129(int a0) { return (a0 + 10) * 2; }`.

Both are inert without a declared extent — `set_range` is the only narrowing of the
flow range and only a declared extent calls it (DIV-121, no option). Over 112 in-repo
fixtures with every discovered function declared at its derived extent (2,765
functions), bodies differing from the undeclared decompile go 403 -> 138 and hard
errors 142 -> 0; with nothing declared, `decompile-all --json` is 112/112
byte-identical.

Closes the round-3 RE-friction need `explicit-function-boundary-aborts`; the
acceptance probe is promoted to tests/cli/ against an in-repo fixture. The sibling
need `default-decompilation-fails-despite`, whose missing target is in-extent, is
deliberately untouched and still fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mahaloz mahaloz added the full-ci Run the full cargo workspace suite on this PR before merge (internal PRs skip it by default) label Sep 6, 2026
@mahaloz
mahaloz merged commit d8899cb into main Sep 6, 2026
10 of 11 checks passed
@mahaloz
mahaloz deleted the feat/re-explicit-function-boundary-aborts branch September 6, 2026 00:20
mahaloz added a commit that referenced this pull request Sep 6, 2026
#431 (B_DONE)

Round 3 stopped at B_VERIFY, so the three PRs its builders merged never made it
back into the backlog: corrupt-elf-section-table, explicit-function-boundary-aborts
and arm-literal-pool-string were all still `status: open` with `closing_pr: null`
while their fixes were in main and their probes already promoted into tests/cli/.

`verify --acceptance-suite --all` on a freshly built 81013ec: 51 needs, 30 pass,
13 fail, 8 indeterminate, 0 regressed — and exactly those three flipped from the
FAIL they were filed at. Closed via `needs apply-acceptance`, one PR each.

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

full-ci Run the full cargo workspace suite on this PR before merge (internal PRs skip it by default)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant