[AUTOMATED] fix(p2): explicit-function-boundary-aborts — a declared extent clips flow instead of killing the function - #430
Merged
Conversation
…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
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>
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.
The problem
--define-function START-ENDis how an agent tells kuna where a function reallyis on a packed or obfuscated image. Declaring an end that any branch crosses
killed the whole function instead of clipping it:
0x1098is the declared exclusive end, and0x1081isJZ 0x1098— anordinary forward conditional over the tail.
--mode reliableand--mode fastfail identically.
docs/cli.mdalready promised the opposite ("a declaredendthat 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:
The fix
Both live in
FlowInfo(p2_lift/flow.rs); both are inert unless a callerdeclares an extent, because
set_rangeis the only thing that narrows the flowrange and only a declared extent calls it.
fillin_branch_stubsalready plants amissingartificial halt at everyreferenced-but-undecoded address; it is now also registered in
visitedasthe instruction there, so
collect_edgeshangs the cut edge on it and thebody ends at the boundary under the
Function flows out of boundsheader.handle_out_of_boundsrecorded are tracked separately. An unprocessedaddress 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-extentNOP; its acceptance still fails, deliberately.)
eaddris the last in-body byte, so an instruction starting on it is inrange. The fall-through bound tested
bound <= addrlist.back()withbound == eaddr, which upstream can only hit at the top of memory but whichunder 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.jsonis 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-extentsplit 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 --jsonover the same 112 binaries is 112/112 byte-identical.Gates:
make test675/675 PARITY OK,make test-stages635/635 PARITY OK,make rust-testgreen,make check-specOK (lenient + strict),make test-cli35/35,
kuna catalog --checkOK. No new option, no catalog counter, no stagesXML.
🤖 Generated with Claude Code
Before / after —
Nonein?Real captured kuna output on the function this feature was built for, with
option Noneflipped. Ported from angr.None off(default)None onkuna — before (
option None off)// kuna decompilation unavailable (no name/address)kuna — after (
option None on)// kuna decompilation unavailable (no name/address)