Skip to content

feat(native): inspect PE and ELF export tables without an engine - #6278

Merged
kaoru0822-kitauji merged 5 commits into
mainfrom
feat/4057-native-export-inventory
Oct 22, 2026
Merged

kaoru0822-kitauji merged 5 commits into
mainfrom
feat/4057-native-export-inventory

Conversation

@kaoru0822-kitauji

@kaoru0822-kitauji kaoru0822-kitauji commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

What broke: REA had no host-independent way to read a PE or ELF export table. Checking a .NET app's 369 P/Invokes against libgit2 required 369 analyze_function dossiers (88–146 s each with a fresh Ghidra import, per #4057), and inspect_macho is macOS-only.
Fix: new inspect_native_exports / inspect-native-exports reads PE export directories and ELF .dynsym tables in-process on any host and returns observed Evidence bound to the file's SHA-256.
Risk: low. Read-only static parsing; adds one MCP tool, which adds its description to the catalog. No existing tool's behavior changes.

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_boundaries accept this Evidence, which closes #4057. CI only runs on PRs that target main, 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/*/native builds. The answer needs to be one call, no engine, and must not flatten format-specific distinctions.

  • PE: one row per used export-address-table slot and name.
    • Fields: {name|null, ordinal, address|null, forwarder|null}, plus the export directory's DLL name, ordinal base and timestamp.
    • Aliases share an ordinal. Ordinal-only exports have name: null. Forwarders keep their "DLL.Func" text and have no address. Unused slots are not exports.
    • An image without an export directory is a complete empty inventory.
  • ELF: defined .dynsym symbols with global, weak or GNU-unique binding and default or protected visibility.
    • Fields: {name, value, size, type, binding, version}.
    • version is null (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 that dlsym selects.
    • value is st_value as stored; on 32-bit ARM, bit 0 marks Thumb code.
  • Incomplete reads stay visible:
    • Unreadable names, out-of-range name ordinals, unreadable forwarders and unresolved version indexes go into issues (one per unresolved version index), and coverage becomes partial.
    • An ELF without section headers is unsupported; enumerating symbols from the dynamic segment is not implemented.
    • Mach-O gets a typed unsupported_target that points to inspect_macho.

Change and scope

The change is three commits, one per layer:

  1. read PE and ELF export tables adds the parser on the shared src/native/metadata container 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 for sh_link/sh_info/sh_entsize.
  2. inspect native export tables through CLI and MCP adds the provider, service, contract, CLI and MCP registration. It mirrors inspect_pe_resources: one stable digest-checked read with a caller-selected max_file_bytes (default 256 MiB, max 512 MiB), and typed failures for malformed, unsupported, oversized, symlinked, missing and cancelled inputs.
  3. Docs: 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:

  • verification integration (follow-up)
  • dynamic-segment-only ELF enumeration
  • Mach-O
  • following PE forwarders into other DLLs
  • analysis-view paging; the payload is about 30 tokens per export

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 new src/native/exports reader.

  • 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, effects evidence, 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

  • Tests added:
    • src/native/exports/NativeExportParser.test.ts (13 tests), using synthetic PE32/PE32+ and ELF64-LE/ELF32-BE images. It covers:
      • PE named, aliased, ordinal-only, forwarded and unused slots; unreadable and out-of-range names; no export directory; the row-budget and 16-bit ordinal refusals
      • ELF default, hidden, unversioned and undefined-index versions; a truncated version table ("unknown"); weak, IFUNC, object and protected rows; hidden, local, undefined and VER_NDX_LOCAL exclusions; an unreadable name; no .dynsym versus no section headers
      • Mach-O refusal
    • tests/boundary/filesystem/nativeExports.test.ts runs 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.ts runs the public MCP workflow: advertised schemas validate, a relative path is refused, and Evidence is recorded in the session.
    • Path-contract lists: absoluteLocalPathSchema, localToolContracts.
  • Real data, the public artifacts from [Feature] Accept a PE/ELF export inventory as native Evidence in verify_managed_native_boundaries #4057:
    • Packages: libgit2sharp.0.31.0.nupkg (02fbec03…c890) and libgit2sharp.nativebinaries.2.0.323.nupkg (d2a16ac8…eeb1). The linux-x64 .so (b1891c25…e020) and win-x64 .dll (44d849a1…ede5) match the issue.
    • Oracle comparison: rows were compared with nm -D --defined-only (ELF, address and name) and llvm-objdump -p (PE, ordinal and name). All matched exactly on 9 libraries:
      • libgit2 linux-x64, linux-arm (Thumb bit cleared as nm does), linux-arm64, linux-musl-arm, linux-ppc64le, win-x64, win-x86 and win-arm64
      • a third-party PE32 freeglut32.vc10.dll
      • The counts were 944, 944, 944, 946, 944, 944, 944, 944 and 165.
    • The git2-3f4182d P/Invokes: inspect-managed-native-boundaries LibGit2Sharp.dll lists 371 P/Invokes. All 369 that name git2-3f4182d are present in both the linux-x64 ELF and win-x64 PE inventories. The other 2 target libdl/kernel32.

Cost

Measured with rea inspect-native-exports <lib> --format json on macOS arm64, 3 warm runs each:

Library Rows Wall s User CPU s Peak RSS MB Output tokens
win-x64/git2-3f4182d.dll 944 0.37–0.41 0.55–0.56 293 27,661
linux-x64/libgit2-3f4182d.so 944 0.38–0.40 0.52–0.60 289–295 36,200
linux-arm64/libgit2-3f4182d.so 944 0.38 0.55–0.57 292–295 36,199

Parsing 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_function with 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:

    • No real-world ELF with symbol versioning was available on this host; version handling is covered by synthetic fixtures.
    • No real PE with forwarders or ordinal-only exports was available here; both are covered by synthetic fixtures.
  • 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 onto c8ed32853, which added inspect_byte_entropy to the same files.

  • Review follow-up (8716138) fixes all seven Greptile findings:

    • shared 262144-row and 16 MiB retained-text allowances, charged before each row is stored
    • PE export directories stored in headers are now read
    • an out-of-file .gnu.version keeps exports as "unknown"
    • an empty version name is flagged as unresolved
    • a .dynsym without its null entry is rejected
    • the ELF section directory is decoded once
    • docs corrected

    There 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:check and docs:check passed; 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

  • Breaking changes or migration steps: none.
  • Real Hopper/Ghidra, browser, or OS coverage: no engine involved. Run on macOS arm64 against Linux and Windows binaries.
  • Package or release metadata impact: one new tool and CLI command.
  • Security, privacy, process, or containment review: reads one caller-selected regular file (symlinks refused), with no execution, loading, subprocess or network access. The samples are public NuGet packages (MIT / GPLv2 with linking exception), used locally and not committed.

Author readiness

Human owner: @kaoru0822-kitauji

  • I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
  • I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
  • I declare this PR ready for initial maintainer review.

Before merge

  • This PR addresses a concrete problem or an agreed enhancement.
  • Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

🤖 Generated with Claude Code

kaoru0822-kitauji and others added 3 commits October 22, 2026 06:58
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>
@kaoru0822-kitauji kaoru0822-kitauji added enhancement New feature or request track:feature Feature/enhancement automation track labels Oct 22, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-22T07:24:09.741609Z
We couldn’t complete this review. Comment @codex review to try again.
acc2940 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 3/5

[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.

Summary

Adds portable PE and ELF export inspection through CLI and MCP, with file hashes and observed Evidence.

  • PE and ELF files now show their declared export entries.
  • Callers can inspect export tables from the CLI or MCP.

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]
Loading

Reviews (2) · Last reviewed commit: "fix(native): bound export inventories an..." · Reviewed by Greptile

Comment thread src/native/exports/NativeExportParser.ts
Comment thread src/native/exports/NativeExportParser.ts Outdated
Comment thread src/native/exports/NativeExportParser.ts Outdated
Comment thread src/native/exports/NativeExportParser.ts Outdated
Comment thread src/native/exports/NativeExportParser.ts
Comment thread src/native/exports/NativeExportParser.ts Outdated
Comment thread docs/binary-diagnostics.md Outdated
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>
@kaoru0822-kitauji
kaoru0822-kitauji merged commit 933af60 into main Oct 22, 2026
46 checks passed
@kaoru0822-kitauji
kaoru0822-kitauji deleted the feat/4057-native-export-inventory branch October 22, 2026 08:30
@morluto morluto mentioned this pull request Oct 25, 2026
3 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request track:feature Feature/enhancement automation track

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Accept a PE/ELF export inventory as native Evidence in verify_managed_native_boundaries

1 participant