Skip to content

[AUTOMATED] fix(cli): zero-function-sizes-make — a sectionless ELF reports a real extent per function - #452

Merged
mahaloz merged 1 commit into
mainfrom
feat/re-zero-function-sizes-make
Sep 6, 2026
Merged

[AUTOMATED] fix(cli): zero-function-sizes-make — a sectionless ELF reports a real extent per function#452
mahaloz merged 1 commit into
mainfrom
feat/re-zero-function-sizes-make

Conversation

@mahaloz

@mahaloz mahaloz commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

The problem

On a binary with no ELF section table, every function kuna finds reports a size
of 0, so size-based triage throws the whole binary away. Take any ELF and zero
its three section-header fields — nothing else changes, every mapped byte is
identical:

$ cp /bin/true ./noshdr && python3 -c "
import struct; b=bytearray(open('noshdr','rb').read())
struct.pack_into('<Q',b,0x28,0); struct.pack_into('<H',b,0x3c,0); struct.pack_into('<H',b,0x3e,0)
open('noshdr','wb').write(bytes(b))"

$ kuna functions ./noshdr --json | jq '[.functions[].size]'
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

$ kuna functions ./noshdr --min-size 1 --json | jq '{count, total, error}'
{ "count": 0, "total": 11, "error": null }

--min-size 1 is documented triage, and it discards all eleven with no error to
distinguish that from a binary that really holds nothing. --summary reports
code_bytes 0 and a largest list of zeroes, so the "which function is the big
one" question a stripped binary is opened with cannot be asked at all.

The fix

  • funcextent clips each entry against the loader's CODE sections; an image
    with no section table publishes none, so every entry took the "outside every
    CODE section" answer that exists for import pointer slots. When the section
    table yields no CODE span at all, the clip now runs against the executable
    load segments instead — still an upper bound clipped at the next entry,
    just a coarser container for the last entry of a segment.
  • The loader reports its PT_LOADs through a new LoadImage::get_segments,
    separate from the section walk. Teaching the loader to synthesize sections
    from segments instead would have silently changed which entries
    whole-binary decompilation selects.
  • The fallback is whole-table, never per-entry. An entry that misses the CODE
    spans an image does publish is a pointer slot or an undefined external, and
    choosing a segment for it would hand a body to exactly those.

The tests

tests/cli/zero-function-sizes-make.json and a four-case console gate run on
noshdr_x86_64, a new 304-byte ELF64 PIE with no section table whose two
functions report 0 and 0 before the change and 16 and 6 after — the
neighbour clip and the segment-end clip. Two loader unit tests cover the
permission-to-flag translation. Sweeping 135 fixture images and 29 system
binaries through kuna functions --json, exactly two change, both sectionless,
both from 0 to a real extent; kuna functions /bin/bash stays at a 0.32 s
median.

🤖 Generated with Claude Code

…ports a real extent per function

Zero an ELF's three section-header fields and every function kuna finds
reports size 0, so `--min-size 1` discards the whole binary:

  $ kuna functions ./noshdr --min-size 1 --json | jq '{count, total, error}'
  { "count": 0, "total": 11, "error": null }

`funcextent` clips each entry against the loader's CODE sections, and an
image with no section table publishes none, so every entry took the
"outside every CODE section" answer that exists for import pointer slots.
When the section table yields no CODE span at all the clip now runs
against the executable load segments instead, which the loader reports
through a new `LoadImage::get_segments` — separate from the section walk,
so which entries whole-binary decompilation selects is unchanged. The
fallback is whole-table, never per-entry: an entry that misses the CODE
spans an image does publish is a pointer slot, and choosing a segment for
it would hand a body to exactly those.

On the filed witness the summary goes from code_bytes 0 to 4821 and
`largest` ranks the VM interpreter first at 4101 bytes. Sweeping 135
fixture images and 29 system binaries, exactly two change — both
sectionless, both from 0 to a real extent.

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 329a714 into main Sep 6, 2026
10 of 11 checks passed
@mahaloz
mahaloz deleted the feat/re-zero-function-sizes-make branch September 6, 2026 08:01
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