Repository navigation
feat(native): inspect PE and ELF export tables without an engine - #6278
Conversation
Add a host-independent export-table reader on the shared native container layer. PE images report each used export-address-table slot with its ordinal, one row per name, its address or forwarder text, and the export directory identity; the PE image reader now exposes its data directories. ELF images report defined default/protected global, weak and unique .dynsym symbols with type, binding and .gnu.version_d version (hidden versions kept distinct, unreadable version tables "unknown"). Unreadable entries are counted in issues and make coverage partial; an ELF without section headers is unsupported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Expose the reader as inspect_native_exports / inspect-native-exports: one stable, digest-bound read of an explicit file with a caller-selected input budget, typed failures for malformed, unsupported, oversized and cancelled reads, and observed Evidence bound to the file path and SHA-256. Mach-O inputs point to inspect_macho. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Low impact] The change adds a contained local-file inspection workflow. The PR should not merge until PE names are bounded before they are stored. SummaryAdds portable PE and ELF export inspection through CLI and MCP, with file hashes and observed Evidence.
Reviewed statically; tests were not rerun. Diagram%%{init: {'theme': 'neutral'}}%%
flowchart LR
Caller[CLI or MCP] --> Service[NativeExportsService]
Service --> Read[Stable file read and SHA-256]
Read --> Format{Image format}
Format --> PE[PE export directory]
Format --> ELF[ELF dynamic symbols and versions]
PE --> Report[Exports, coverage and issues]
ELF --> Report
Report --> Evidence[Observed Evidence]
Reviews (2) · Last reviewed commit: "fix(native): bound export inventories an..." · Reviewed by Greptile |
Address review of the export-table reader: - Charge every retained row and its name, forwarder and version text against shared 262144-row and 16 MiB allowances before storing it, so repeated long names or PE aliases cannot exhaust memory. - Map PE RVAs through the headers as well as sections; the PE image reader now exposes its SizeOfHeaders mapping. - Keep ELF exports with version "unknown" when .gnu.version lies outside the file, and report an empty version-definition name as unresolved. - Reject a .dynsym without its mandatory null entry as malformed. - Decode the ELF section directory once. - Correct the docs on which gaps drop rows and which keep them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Part of #4057, the producer half of the proposal posted on the issue. A follow-up after this merges will make
verify_managed_native_boundariesaccept this Evidence, which closes #4057. CI only runs on PRs that targetmain, so I didn't stack that half here, where it would get no CI.Problem and expected behavior
The analyst question is "which entry points does this native library export?", asked on any host and for any of a package's
runtimes/*/nativebuilds. The answer needs to be one call, no engine, and must not flatten format-specific distinctions.{name|null, ordinal, address|null, forwarder|null}, plus the export directory's DLL name, ordinal base and timestamp.name: null. Forwarders keep their"DLL.Func"text and have no address. Unused slots are not exports..dynsymsymbols with global, weak or GNU-unique binding and default or protected visibility.{name, value, size, type, binding, version}.versionisnull(unversioned),{index, name, hidden}from.gnu.version_d, or"unknown"when the version table can't be read. A hidden version is not the default thatdlsymselects.valueisst_valueas stored; on 32-bit ARM, bit 0 marks Thumb code.issues(one per unresolved version index), and coverage becomespartial.unsupported; enumerating symbols from the dynamic segment is not implemented.unsupported_targetthat points toinspect_macho.Change and scope
The change is three commits, one per layer:
read PE and ELF export tablesadds the parser on the sharedsrc/native/metadatacontainer layer.readPeImage(the owner of PE header parsing) now also exposes its data directories, rather than the parser reparsing the optional header. ELF reads the raw section directory forsh_link/sh_info/sh_entsize.inspect native export tables through CLI and MCPadds the provider, service, contract, CLI and MCP registration. It mirrorsinspect_pe_resources: one stable digest-checked read with a caller-selectedmax_file_bytes(default 256 MiB, max 512 MiB), and typed failures for malformed, unsupported, oversized, symlinked, missing and cancelled inputs.docs/binary-diagnostics.md, plus the authored skill and reference.The diff is about 1,790 lines. About 660 of those are tests and fixtures, and about 480 are the parser (after
oxfmt).Not included:
Remaining cleanup: the read-failure translation follows the same pattern Go, Rust and IL2CPP each duplicate. Extracting a shared helper would touch those providers, so I left it out of this PR.
Contract and boundary impact
Semantic owner and earliest changed stage:
src/native/metadata(PE data directories) and the newsrc/native/exportsreader.Contribution scope fit and linked maintainer decision: static read of a caller-selected file, the same authority as
inspect_pe_resources. The proposal is on [Feature] Accept a PE/ELF export inventory as native Evidence in verify_managed_native_boundaries #4057 and the direction was endorsed in the thread.CLI and MCP/tool-catalog contract: one new tool,
inspect_native_exports(kind: native-provider, effectsevidence, always available). No existing contract changes.Provider, bridge, target-format, or platform compatibility: portable. PE32/PE32+ and ELF32/64 in both byte orders.
Evidence, artifact, provenance, or reconstruction contract: observed Evidence with the subject
{path, sha256, format: pe|elf}.Generated metadata: the catalog builder maps the tool to its own provider identity (
rea-native-exports). Nothing generated is committed.Read existing files or captured data.
Evidence and regression coverage
src/native/exports/NativeExportParser.test.ts(13 tests), using synthetic PE32/PE32+ and ELF64-LE/ELF32-BE images. It covers:"unknown"); weak, IFUNC, object and protected rows; hidden, local, undefined andVER_NDX_LOCALexclusions; an unreadable name; no.dynsymversus no section headerstests/boundary/filesystem/nativeExports.test.tsruns the real provider over temp files. It checks Evidence binding for PE and ELF, plus typed symlink, malformed, missing, Mach-O, budget and cancellation failures.tests/boundary/mcp/binaryDiagnosticsMcp.test.tsruns the public MCP workflow: advertised schemas validate, a relative path is refused, and Evidence is recorded in the session.absoluteLocalPathSchema,localToolContracts.libgit2sharp.0.31.0.nupkg(02fbec03…c890) andlibgit2sharp.nativebinaries.2.0.323.nupkg(d2a16ac8…eeb1). Thelinux-x64.so(b1891c25…e020) andwin-x64.dll(44d849a1…ede5) match the issue.nm -D --defined-only(ELF, address and name) andllvm-objdump -p(PE, ordinal and name). All matched exactly on 9 libraries:linux-x64,linux-arm(Thumb bit cleared as nm does),linux-arm64,linux-musl-arm,linux-ppc64le,win-x64,win-x86andwin-arm64freeglut32.vc10.dllgit2-3f4182dP/Invokes:inspect-managed-native-boundaries LibGit2Sharp.dlllists 371 P/Invokes. All 369 that namegit2-3f4182dare present in both thelinux-x64ELF andwin-x64PE inventories. The other 2 targetlibdl/kernel32.Cost
Measured with
rea inspect-native-exports <lib> --format jsonon macOS arm64, 3 warm runs each:win-x64/git2-3f4182d.dlllinux-x64/libgit2-3f4182d.solinux-arm64/libgit2-3f4182d.soParsing alone takes 1–4 ms per library; the rest is CLI process start-up, which accounts for most of the RSS. For comparison, #4057 reports 88 s (ELF) and 146 s (PE) for a single
analyze_functionwith a fresh Ghidra import, and 369 of them are needed today. I did not rerun Ghidra; those numbers are quoted from the issue, not measured here.Remaining proof gaps:
Observed, derived, and inferred claims remain distinguishable.
Artifact identity, source provenance, and failed attempts remain preserved.
Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.
Validation performed
npm run test:local -- src/native/exports/NativeExportParser.test.ts src/contracts/toolEffects.test.ts src/native/metadata/PeImage.test.ts: 24 passed.npm run test:focused -- tests/boundary/filesystem/nativeExports.test.ts tests/boundary/mcp/binaryDiagnosticsMcp.test.ts tests/boundary/mcp/absoluteLocalPathSchema.test.ts tests/acceptance/tools/localToolContracts.test.ts tests/boundary/mcp/toolSchemaValidity.test.ts tests/boundary/filesystem/peResources.test.ts: 31 passed, after rebasing ontoc8ed32853, which addedinspect_byte_entropyto the same files.Review follow-up (8716138) fixes all seven Greptile findings:
.gnu.versionkeeps exports as"unknown".dynsymwithout its null entry is rejectedThere are 5 new regression tests, and all 5 fail on the previous commit. After the follow-up: the parser suite passed 18 tests; the focused boundary, MCP and contract suites passed 31; the Go, Rust and IL2CPP container suites passed 24;
check:fast,format:checkanddocs:checkpassed; and the oracle still matches on 6 libgit2 libraries.npm run check:fast: passed.npm run format:check: passed.npm run docs:check: passed.npm run metadata:check: passed.Real-data oracle comparison and timing: see above.
Compatibility, safety, and release
Author readiness
Human owner: @kaoru0822-kitauji
Before merge
type(scope): outcome.🤖 Generated with Claude Code