[GH-ISSUE #6161] [Bug] A truncated ELF is analyzed as a complete program: Evidence reports fewer procedures with no truncation notice #3765

Closed
opened 2026-10-31 00:43:48 +00:00 by bsakaguchi · 4 comments
Owner

Originally created by @Nitin13-git on GitHub (Oct 21, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6161

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

analyze accepts an ELF cut short anywhere after its 64-byte header and returns successful Evidence for what Ghidra loaded, with the same generic limitation notes as the complete file and no mention that the file is shorter than its headers declare. Only a cut inside the 64-byte header is rejected (truncated ELF header, src/domain/elfFixedHeader.ts); a cut at exactly 64 bytes fails later as a generic execution_failure ("Run rea doctor and review the reported details...").

Reproduction

Executed on macOS arm64, Node 24.18.0, source 3dbe314ad, Ghidra 12.1.4 (GHIDRA_INSTALL_DIR and JAVA_HOME set). The ELF is /usr/bin/true from Ubuntu 22.04 (26,936 bytes, e_phoff=64, e_phnum=13, e_shoff=25016, e_shnum=30, e_shentsize=64), copied out of a container.

head -c <N> true > cut_<N>
node scripts/rea.mjs analyze cut_<N> --format json
N (bytes kept) Result
64 exit 1, execution_failure
120, 500, 2000 exit 0, procedure_count 0
8000 exit 0, procedure_count 24, segment_count 2
13468 exit 0, procedure_count 27, segment_count 2
26935 (one byte short) exit 0, 135 procedures, 32 segments (same as the whole file)
26936 (whole file) exit 0, 135 procedures, 32 segments

For N=8000 the section header table (bytes 25,016 to 26,936) is beyond the end of the file. The Evidence for N=8000 and for the whole file each lists the same 9 limitation entries, and normalized_result (document, image_base, segments, segment_count, procedure_count, string_count) has no truncation field. For comparison, a Mach-O cut inside its load commands is refused (truncated Mach-O load commands: the header declares ... bytes ...; the file has ...).

Expected behavior

Evidence for a file shorter than its program or section headers declare says so (a limitation or unknown naming the declared and actual sizes), or the file is refused as target_unavailable as truncated Mach-O load commands already are. I do not know which the maintainers prefer: a hard refusal could reject deliberately malformed samples that reverse engineers want to analyze, so a recorded limitation may be the better fit. Either way, a cut at 64 bytes should not report an environment failure.

Impact and scope

Observed on one ELF on one host. Real-world frequency, other ELF architectures, PE and fat Mach-O files, and non-Ghidra providers: NOT_ENOUGH_INFO. Effect: an incomplete download or copy produces plausible but incomplete Evidence that looks like a smaller program.

REA version

Source 3dbe314ad (package 6.7.0)

Runtime and platform

macOS arm64 (26.6.2), Node 24.18.0

Target and artifact context

One ELF x86-64 from a stock Ubuntu 22.04 image, truncated with head -c; no user binary.

Interface

CLI

Earliest failure stage

Setup or provider admission

Relevant output or evidence

Commands and results are in the table above. I also ran 25 header corruptions of an ELF and a PE: bad class, byte order, machine, PE signature, PE header offset, and truncated PE headers are refused with specific target_unavailable messages; zero or oversized program-header and section counts and a section table beyond the file are loaded by Ghidra with changed counts, which I do not consider a defect.

Analysis or suspected cause

Source-derived, not verified by a fix: parseElf in src/domain/binaryTarget.ts uses only the fixed header (readElfFixedHeader, src/domain/elfFixedHeader.ts); it does not compare e_phoff/e_shoff and their entry counts and sizes against the file size. Related: #6154 (corrupt Mach-O load commands, same generic execution_failure outcome) and #1189 (truncated Mach-O header, closed).

Originally created by @Nitin13-git on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6161 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior `analyze` accepts an ELF cut short anywhere after its 64-byte header and returns successful Evidence for what Ghidra loaded, with the same generic limitation notes as the complete file and no mention that the file is shorter than its headers declare. Only a cut inside the 64-byte header is rejected (`truncated ELF header`, `src/domain/elfFixedHeader.ts`); a cut at exactly 64 bytes fails later as a generic `execution_failure` ("Run `rea doctor` and review the reported details..."). ### Reproduction Executed on macOS arm64, Node 24.18.0, source `3dbe314ad`, Ghidra 12.1.4 (`GHIDRA_INSTALL_DIR` and `JAVA_HOME` set). The ELF is `/usr/bin/true` from Ubuntu 22.04 (26,936 bytes, `e_phoff=64`, `e_phnum=13`, `e_shoff=25016`, `e_shnum=30`, `e_shentsize=64`), copied out of a container. ```sh head -c <N> true > cut_<N> node scripts/rea.mjs analyze cut_<N> --format json ``` | N (bytes kept) | Result | | --- | --- | | 64 | exit 1, `execution_failure` | | 120, 500, 2000 | exit 0, `procedure_count` 0 | | 8000 | exit 0, `procedure_count` 24, `segment_count` 2 | | 13468 | exit 0, `procedure_count` 27, `segment_count` 2 | | 26935 (one byte short) | exit 0, 135 procedures, 32 segments (same as the whole file) | | 26936 (whole file) | exit 0, 135 procedures, 32 segments | For N=8000 the section header table (bytes 25,016 to 26,936) is beyond the end of the file. The Evidence for N=8000 and for the whole file each lists the same 9 limitation entries, and `normalized_result` (`document`, `image_base`, `segments`, `segment_count`, `procedure_count`, `string_count`) has no truncation field. For comparison, a Mach-O cut inside its load commands is refused (`truncated Mach-O load commands: the header declares ... bytes ...; the file has ...`). ### Expected behavior Evidence for a file shorter than its program or section headers declare says so (a limitation or unknown naming the declared and actual sizes), or the file is refused as `target_unavailable` as truncated Mach-O load commands already are. I do not know which the maintainers prefer: a hard refusal could reject deliberately malformed samples that reverse engineers want to analyze, so a recorded limitation may be the better fit. Either way, a cut at 64 bytes should not report an environment failure. ### Impact and scope Observed on one ELF on one host. Real-world frequency, other ELF architectures, PE and fat Mach-O files, and non-Ghidra providers: NOT_ENOUGH_INFO. Effect: an incomplete download or copy produces plausible but incomplete Evidence that looks like a smaller program. ### REA version Source `3dbe314ad` (package 6.7.0) ### Runtime and platform macOS arm64 (26.6.2), Node 24.18.0 ### Target and artifact context One ELF x86-64 from a stock Ubuntu 22.04 image, truncated with `head -c`; no user binary. ### Interface CLI ### Earliest failure stage Setup or provider admission ### Relevant output or evidence Commands and results are in the table above. I also ran 25 header corruptions of an ELF and a PE: bad class, byte order, machine, PE signature, PE header offset, and truncated PE headers are refused with specific `target_unavailable` messages; zero or oversized program-header and section counts and a section table beyond the file are loaded by Ghidra with changed counts, which I do not consider a defect. ### Analysis or suspected cause Source-derived, not verified by a fix: `parseElf` in `src/domain/binaryTarget.ts` uses only the fixed header (`readElfFixedHeader`, `src/domain/elfFixedHeader.ts`); it does not compare `e_phoff`/`e_shoff` and their entry counts and sizes against the file size. Related: #6154 (corrupt Mach-O load commands, same generic `execution_failure` outcome) and #1189 (truncated Mach-O header, closed).
Author
Owner

@XinwangTao commented on GitHub (Oct 22, 2026):

确认,这是静默不完整分析,比显式报错更危险——调用方会以为 135 个 procedure 就是全貌。除了 #6165 里"program header table 越界即拒绝"之外,建议对中间态(头能解析但文件长度小于头声明的各 section/symbol 范围)统一输出显式的不完整/截断标记,而不是静默降低 procedure_count 后照常返回成功 Evidence。另外 head -c 26935(差 1 字节)与完整文件输出同样的 135 procedures/32 segments 这一点很可疑,建议在 #6165 的验收里加一条:任意 N 截断要么非成功退出、要么结果带截断警告,杜绝"差一个字节结果完全相同"的假阳性。

<!-- gh-comment-id:6330395468 --> @XinwangTao commented on GitHub (Oct 22, 2026): 确认,这是静默不完整分析,比显式报错更危险——调用方会以为 135 个 procedure 就是全貌。除了 #6165 里"program header table 越界即拒绝"之外,建议对中间态(头能解析但文件长度小于头声明的各 section/symbol 范围)统一输出显式的不完整/截断标记,而不是静默降低 procedure_count 后照常返回成功 Evidence。另外 head -c 26935(差 1 字节)与完整文件输出同样的 135 procedures/32 segments 这一点很可疑,建议在 #6165 的验收里加一条:任意 N 截断要么非成功退出、要么结果带截断警告,杜绝"差一个字节结果完全相同"的假阳性。
Author
Owner

@Yoh5 commented on GitHub (Oct 22, 2026):

Source-only observation — I have not reproduced the table above on my own machine yet, so nothing here is a runtime result.

Reading src/domain/elfFixedHeader.ts: the validator checks e_ehsize against the containing file size, but it never reads the two extents the same fixed header also declares — e_phoff/e_phnum/e_phentsize and e_shoff/e_shnum/e_shentsize.

For the N=8000 row, the header declares a section table spanning 25,016–26,936 while the file stops at 8,000. That discrepancy is computable from the first 64 bytes, before any loader runs, which would explain why a cut inside the 64-byte header is caught and every longer cut is not.

A question on the intended contract, because it decides the shape of a fix rather than its size: should a declared extent beyond EOF reject the file, or should the file be admitted with a named truncation note carried into the Evidence?

The title ("no truncation notice") reads like the second. A rejection would also break legitimate inputs — an ELF carved out of a container, a relocatable object, an embedded image — and the second reading is consistent with #6154, #6123 and #6120, where the complaint is that a degraded input is mislabelled, not that it is accepted.

Glad to take this on if the annotate direction is the right one.

<!-- gh-comment-id:6338931997 --> @Yoh5 commented on GitHub (Oct 22, 2026): Source-only observation — I have **not** reproduced the table above on my own machine yet, so nothing here is a runtime result. Reading `src/domain/elfFixedHeader.ts`: the validator checks `e_ehsize` against the containing file size, but it never reads the two extents the same fixed header also declares — `e_phoff`/`e_phnum`/`e_phentsize` and `e_shoff`/`e_shnum`/`e_shentsize`. For the N=8000 row, the header declares a section table spanning 25,016–26,936 while the file stops at 8,000. That discrepancy is computable from the first 64 bytes, before any loader runs, which would explain why a cut inside the 64-byte header is caught and every longer cut is not. A question on the intended contract, because it decides the shape of a fix rather than its size: should a declared extent beyond EOF **reject** the file, or should the file be **admitted with a named truncation note** carried into the Evidence? The title ("no truncation notice") reads like the second. A rejection would also break legitimate inputs — an ELF carved out of a container, a relocatable object, an embedded image — and the second reading is consistent with #6154, #6123 and #6120, where the complaint is that a degraded input is *mislabelled*, not that it is accepted. Glad to take this on if the annotate direction is the right one.
Author
Owner

@XinwangTao commented on GitHub (Oct 23, 2026):

Agree with the annotate direction: accepting the file with an explicit truncation notice is the better contract, both for the carved-out/embedded ELF cases you list and because a hard refusal would reject deliberately malformed samples that reverse engineers want to load. To keep it consistent with #6165: keep the reject there scoped to program-header-table overflow (the case that genuinely breaks parsing), and carry a named truncation note into Evidence for section/symbol extents that merely run past EOF. It would also be worth pinning #6165's acceptance criterion to "any N-byte cut either exits non-zero or returns Evidence with a truncation notice", so the 26935-vs-26936 identical-result case cannot silently come back.

<!-- gh-comment-id:6354990769 --> @XinwangTao commented on GitHub (Oct 23, 2026): Agree with the annotate direction: accepting the file with an explicit truncation notice is the better contract, both for the carved-out/embedded ELF cases you list and because a hard refusal would reject deliberately malformed samples that reverse engineers want to load. To keep it consistent with #6165: keep the reject there scoped to program-header-table overflow (the case that genuinely breaks parsing), and carry a named truncation note into Evidence for section/symbol extents that merely run past EOF. It would also be worth pinning #6165's acceptance criterion to "any N-byte cut either exits non-zero or returns Evidence with a truncation notice", so the 26935-vs-26936 identical-result case cannot silently come back.
Author
Owner

@morluto commented on GitHub (Oct 23, 2026):

Resolved on main in 651afa445. Before provider selection, the shared target resolver validates complete ELF program/section tables and file-backed payload extents using one exact declaration owner. Executed ELF32/64 LE/BE controls refuse truncation and retain valid absent-table/NOBITS/zero-sized declarations. Validation: tests/boundary/filesystem/elfDeclaredRanges.test.ts — refuses declared table and payload truncation before provider admission; recorded execution at eac392d08. The exact ELF declared-range file passed; enclosing group exited1 on unrelated MIPS expectations later repaired. No Ghidra engine run; hard refusal occurs before provider launch, so this does not claim loader completeness.

<!-- gh-comment-id:6376131368 --> @morluto commented on GitHub (Oct 23, 2026): Resolved on main in [651afa445](https://github.com/morluto/rea/commit/651afa445571017be461d92f7a9cd263de6d4248). Before provider selection, the shared target resolver validates complete ELF program/section tables and file-backed payload extents using one exact declaration owner. Executed ELF32/64 LE/BE controls refuse truncation and retain valid absent-table/NOBITS/zero-sized declarations. Validation: `tests/boundary/filesystem/elfDeclaredRanges.test.ts` — refuses declared table and payload truncation before provider admission; recorded execution at `eac392d08`. The exact ELF declared-range file passed; enclosing group exited1 on unrelated MIPS expectations later repaired. No Ghidra engine run; hard refusal occurs before provider launch, so this does not claim loader completeness.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3765
No description provided.