Add build identity reads for PE, ELF and Mach-O - #142
Conversation
fe899df to
7fedd82
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The PE, ELF, and Mach-O readers have unresolved correctness and validation issues.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds build identity readers for PE, ELF, and Mach-O modules to support exact per-build offset profiles.
Changes:
- Reads PE CodeView GUID/age, ELF GNU build IDs, and Mach-O UUIDs.
- Adds identity formatting, an in-memory test host, and unit tests.
File summaries
| File | Description |
|---|---|
src/runtime/mod.rs |
Exposes the test-only mock module. |
src/runtime/mock.rs |
Implements an in-memory process test host. |
src/lib.rs |
Enables std for native tests. |
src/file_format/pe.rs |
Adds PE identity parsing; directory-length and CodeView-size validation require fixes. |
src/file_format/macho.rs |
Adds UUID parsing; big-endian Mach-O handling requires correction. |
src/file_format/elf.rs |
Adds GNU build-ID parsing; ELF load-bias handling requires correction. |
Review details
Suppressed comments (3)
src/file_format/elf.rs:1246
- A
PT_NOTEsegment may legally contain more than 16 notes, and the build-ID note has no required position, so this cap silently misses valid IDs later in the segment. Sincep_fileszalready supplies the segment boundary, walk until that boundary while validating each padded size and ensuring the offset advances.
// The walk is bounded so corrupt sizes can't turn it into a scan.
for _ in 0..0x10 {
src/file_format/macho.rs:140
- This traversal ignores
sizeofcmds(the new fixture even leaves it at zero), so malformed or truncated images can be read past the declared load-command table and yield a UUID from unrelated bytes. The fixed 64-command cap can also miss a validLC_UUID, whose ordering is unconstrained. Iteratencmdswhile requiring eachcmdsizeto fit withinsizeofcmdsand to be large enough for its command; update the fixture to declare its command-table size.
// The walk is bounded so a corrupt command count can't turn it into a scan.
for _ in 0..header.ncmds.min(0x40) {
let command = process.read::<LoadCommand>(commands + offset).ok()?;
src/file_format/pe.rs:653
- The fixed 16-entry cutoff can report
Noneeven when a valid debug directory contains a CodeView entry later in the table. The directory's declared size already gives a finite entry count; validate it against the image bounds and inspect the complete table instead of imposing an unrelated format limit.
// The walk is bounded so a corrupt entry count can't turn it into a scan.
(0..entries.min(0x10)).find_map(|i| {
- Files reviewed: 6/6 changed files
- Comments generated: 4
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
7fedd82 to
dd7bde1
Compare
CryZe
left a comment
There was a problem hiding this comment.
Two ELF validation and compatibility issues remain. The program-header validation should be addressed before this identity is used to select per-build offsets; the build-ID length limit needs either support or an explicit documented contract.
CryZe
left a comment
There was a problem hiding this comment.
The requested ELF program-header validation and build-ID length contract are both addressed, with boundary and malformed-input tests. All checks pass.
First piece of #141: reading a module's build identity out of mapped memory, so offsets can later be keyed to exact builds instead of version heuristics.
pe::DebugId: GUID and age from the CodeView debug directory entry, the pair symbol servers key on.elf::build_id: theNT_GNU_BUILD_IDnote. It accounts for the load address, so non-PIE executables work the same as shared objects.macho::uuid: theLC_UUIDload command.Also introduces the first unit tests in the crate: a small mock host implementing the
envprocess imports over in-memory buffers, so the readers run against hand-laid images on the host.elf::build_idandmacho::uuidtake the module's range. Each reader checks that its table fits inside the module before reading it.pe::DebugIdusessize_of_imagefor the same check.I used
mem::offset_of!andOption::is_none_or. Neither is used elsewhere in the crate yet, andis_none_orneeds Rust 1.82.Since there's no precedent for tests here, happy to restructure if you'd prefer a different shape, like tests split into their own files or another home for the mock.