[PR #3586] feat(jvm): inspect JVM source-line & local-variable debug metadata (#2377) #5840

Open
opened 2026-10-31 01:09:49 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/3586
Author: @FerroLx
Created: 10/14/2026
Status: 🔄 Open

Base: main ← Head: feat/2377-jvm-debug-metadata


📝 Commits (10+)

  • db89910 feat(jvm): add JVM debug-metadata domain core (#2377)
  • 258595a docs(jvm): complete the #2377 spec directory
  • a2d923b feat(jvm): inspect class-file debug metadata via CLI and MCP (#2377)
  • 2395d3d test(jvm): add javac/javap real lane and the analyst guide (#2377)
  • 0430d4e fix(jvm): address three-angle review findings (#2377)
  • 8a99aee feat(jvm): flag out-of-range local-variable scopes (#2377)
  • 7901488 feat(jvm): select a class entry from a JAR, with multi-release (#2377)
  • 92e4f25 fix(jvm): honor Multi-Release manifest and refuse ambiguous JAR entries (#2377)
  • 60840db fix(jvm): guide JAR selection, normalize entry paths, tighten UTF-8 (#2377)
  • baedbfc fix(jvm): classify archives by magic peek before the class-file budget (#2377)

📊 Changes

82 files changed (+11177 additions, -71 deletions)

View changed files

📝 .agents/skills/reverse-engineer-anything/SKILL.md (+6 -0)
📝 .agents/skills/reverse-engineer-anything/references/native-and-artifacts.md (+19 -0)
📝 README.md (+1 -0)
📝 docs/architecture.mermaid (+3 -0)
📝 docs/index.md (+14 -14)
➕ docs/jvm-debug-metadata.md (+293 -0)
➕ docs/specs/2377-jvm-debug-metadata/plan.md (+18 -0)
➕ docs/specs/2377-jvm-debug-metadata/spec.md (+85 -0)
➕ docs/specs/2377-jvm-debug-metadata/tasks.md (+19 -0)
➕ docs/specs/2377-jvm-debug-metadata/verification.md (+333 -0)
📝 experiments/analysis-efficiency/source-to-bundle.mjs (+1 -2)
📝 package.json (+1 -0)
📝 scripts/lib/catalog-builders.mjs (+5 -0)
📝 scripts/lib/catalog-core.mjs (+2 -0)
📝 scripts/lib/test-projects.mjs (+1 -1)
➕ scripts/verify-jvm-debug-metadata.mjs (+500 -0)
📝 src/application/WebSourceLocationPorts.ts (+4 -2)
📝 src/application/javascript/JavaScriptSemanticGraphConstruction.ts (+2 -2)
➕ src/application/jvm/JvmDebugMetadataPort.ts (+23 -0)
➕ src/application/jvm/JvmDebugMetadataService.test.ts (+291 -0)

...and 62 more files

📄 Description

Summary

Fixes #2377.

Adds inspect_jvm_debug_metadata / inspect-jvm-debug-metadata, a static, in-process reader of the compiler-emitted debug metadata in a local .class file or in a class entry of a .jar. Multi-release JARs are supported through an explicit release. It lets an analyst or agent judge debugging readiness before attaching a debugger: which methods carry line numbers and local-variable names, which were stripped, and which are malformed.

Problem and expected behavior

Before this change, an analyst had to run javap -l or open a decompiler, then read per-method tables by hand, to answer "can I debug this class with names and lines?". Decompiler line numbers are not compiler positions, so they cannot answer it.

Expected: one bounded call returns the following, as artifact-bound Evidence, without executing Java:

  • per-method readiness (present | partial | stripped | malformed | not-applicable);
  • source file;
  • line positions and local declarations;
  • out-of-range scopes.

Change and scope

  • src/domain/jvm/: a dependency-free JVMS §4.7 reader using Result and tagged errors, bounded before allocation.
    • It decodes SourceFile, SourceDebugExtension (kept opaque), LineNumberTable, LocalVariableTable and LocalVariableTypeTable.
    • It binds offsets, slots and scopes to each method's name and descriptor.
    • It checks max_locals, code length and constant-pool kinds.
    • Repeated or trailing attributes are reported as malformed, never silently merged.
  • src/jvm/: the provider and lifecycle.
    • JAR descriptor and cleanup: one owned JAR descriptor through withArtifactRegularFile, with cleanup leases and retained failed-close ownership, following the RustBinaryProvider pattern.
    • Classification: the archive is classified by a magic peek before the class-file budget is applied.
    • Multi-release selection: decided exactly as JDK 25's JarFile does (src/jvm/jarManifest.ts ports Attributes.read).
      • The manifest is matched by a case-insensitive stored name and follows the JDK's last-duplicate rule.
      • Manifests over 16,000,000 bytes count as not multi-release, mirroring the default jdk.jar.maxSignatureFileSize.
      • Version directories are limited to 9..2147483647.
      • Versions are never silently combined.
    • JAR reading: REA opens a JAR with its own reader (src/artifacts/JdkZipCentralDirectory.ts, src/artifacts/JdkZipFile.ts). It reproduces OpenJDK 25's ZipFile: findEND, initCEN, checkAndAddEntry, checkExtraFields, countCENHeaders and getInputStream, over raw bytes.
      • It refuses the whole archive exactly where new JarFile(...) does, quoting the JDK's message: encrypted records, bad compression methods, END and zip64 inconsistencies, and so on.
      • It reads members from the central directory alone, as the JDK does. It makes no CRC or local-header cross-check, and charges the decompression budget with the bytes actually produced.
      • The manifest is read as JarFile.getBytes reads it.
      • A shared zip.js-based reader could not be made exact, because zip.js reads END, zip64 END and local headers differently. The JVM tool therefore no longer uses ZipArtifactReader.
      • inspect_artifact still lists JARs through the shared reader, so on an unusual JAR the two tools can disagree. Each tool's docs state its own rule.
    • Member matching: JAR members are matched by their stored names, independent of ZIP attribute bits. ./-prefixed members are inert, as for the JDK, and META-INF/ entries are never versioned. Symlink-mode members are read as ZIP bytes, as JDK 25 reads them; no host link is followed.
  • Shared ZIP reader (src/artifacts/; #4473 carries the same changes, and the JVM tool no longer uses this reader):
    • storedPath: ArtifactEntry.storedPath is the raw central-directory name, never the Info-ZIP Unicode Path name. A limitation is recorded when the two differ.
    • Directory rule: a member is a directory only when its stored name ends in /.
    • Attribute-only directories: members marked as directories only by attribute bits are readable files with a limitation. This makes two shapes fail closed with explicit errors:
      • foo plus foo/bar is a prefix conflict;
      • . or an empty name is a non-normalized path.
    • Reused helper: peekArtifactMagic reuses readStableArtifact's no-follow, identity-checked open.
  • Wiring: contract, CLI, MCP, composition, effects, catalog provider identity, docs, skill routing, and npm run verify:jvm with its fixtures.

Not included: decompilation, source resolution, and any runtime or debugger attachment.

Scope: contribution scope. This is static format inspection of a caller-selected local file within existing offline-artifact authority. No Java executes, nothing is fetched, and nothing is mutated. No maintainer scope decision is required.

Contract and boundary impact

  • Semantic owner and earliest changed stage:
    • src/domain/jvm/javaDebugMetadata.ts (meaning of the attributes);
    • src/jvm/jarManifest.ts (multi-release decision);
    • src/artifacts/ZipArtifactReader.ts (stored name and directory kind).
  • Contribution scope fit and linked maintainer decision: static offline parsing; not applicable.
  • CLI and MCP/tool-catalog contract:
    • New tool inspect_jvm_debug_metadata / inspect-jvm-debug-metadata, with inputs path, optional entry, release and class_descriptor.
    • New catalog provider identity.
  • Provider, bridge, target-format, or platform compatibility: in-process and portable. Inputs are class files of any major version, plain JARs and multi-release JARs.
  • Evidence, artifact, provenance, or reconstruction contract:
    • The Evidence carries the archive path and SHA-256, plus the selected member's identity and SHA-256 (hashed after selection), at confidence observed.
    • Limitations are stated per answer.
  • Target selection, authority, upstream behavior, and cleanup details: one admitted no-follow descriptor per call, with failed-close ownership retained and retried. Cancellation carries the caller's reason.
  • Generated metadata, package, or installation impact: CI regenerates the catalog. There is no new dependency.
  • Shared files with #4473: both PRs change ArtifactReader.ts and ZipArtifactReader.ts identically except for one line. #4473 normalizes member paths with normalizeArchiveMemberPath; this PR uses the equivalent ./ strip. Whichever merges second keeps normalizeArchiveMemberPath. The other overlaps are tool-registration lines (CLI, server, catalog, docs) to union.

Expected effects:

  • Read existing files or captured data.
  • Launch or attach to a process.
  • Inject instrumentation or run scripts/code.
  • Change process memory, registers, files, or configuration.
  • Contact network services.
  • Recover protected code or work around protections.
  • Documentation or usability only; no target interaction.
  • Not sure yet; investigation or maintainer guidance needed.

Evidence and regression coverage

  • Tests added or updated:
    • Domain, provider, manifest, JAR-entry, service and ZIP-reader unit tests: 162 run in the latest JVM + artifacts pass.
    • tests/process-global/jvmClassFileLifetime.test.ts covers the lifecycle regressions: failed close, cancellation during final cleanup, and retry.
    • Each review fix added a test that fails on the previous code.
  • Real-toolchain lane: npm run verify:jvm (JDK 25), 16 cases.
    • It compiles owned javac fixtures with -g and -g:none and compares the decoded facts with javap.
    • It runs a real third-party JAR and a built multi-release JAR through the CLI and stdio MCP, checking that their Evidence matches.
  • Review round (head 252924008): all 7 automated review findings were confirmed against JDK 25 / HotSpot and fixed, each with a test that fails on the previous code. JAR member names are decoded as UTF-8 like JarFile (a ZIP-reader charset option; other consumers keep CP437). Local-variable slots past max_locals, out-of-code line pcs and scopes, and bytes after the class file are now malformed or refused, as HotSpot rejects them. Evidence is bound to the answered container, entry and release. ZIP limit failures report the bound that was actually hit. The Multi-Release matcher no longer buffers continuation lines. The verify lane uses the resolved jar tool. An independent audit of the round was clean: 0 javap mismatches over 2,726 real classes, 16/16 UTF-8 name cases match JDK 25 ZipFile, and 0 Multi-Release differences across about 35,000 manifests.
  • Second review round (head e271e022c): 7 more automated findings, which CassieFox23 confirmed against JDK 21, were fixed, each with a test that fails on 252924008. The fixes:
    • a constant-pool count of 0 and a long/double in the pool's last slot are refused;
    • attribute names that don't resolve to CONSTANT_Utf8 are refused, or make the method malformed inside Code;
    • code_length outside 1..65535 is malformed;
    • local names and descriptors are checked against HotSpot's grammar (new javaDescriptor.ts);
    • a LocalVariableTypeTable below class version 49 no longer changes a method's state;
    • a missing entry is reported not-found before the manifest is read.
  • Third review round (head f19116b77): 3 automated findings fixed:
    • the CLI keeps the caller's path through absoluteCallerPath, so link/../Example.class is resolved by the filesystem after following the link, not rewritten lexically (a POSIX acceptance test fails on bd515f2fc);
    • a loose .class whose stable read and hash completed before a failed close or a cancellation now keeps its SHA-256 and size as a file-snapshot-hash partial observation (two lifecycle tests fail on bd515f2fc);
    • the docs, tool description, skill reference and spec now say SourceDebugExtension is reported by presence and byte length only; its bytes were never returned (wording fix, no behaviour change).
      An independent audit of this round was clean.
  • Fourth review round (head 2ed202b0d): 4 more automated findings, each confirmed on JDK 21 and 25 and fixed with a test that fails on f19116b77. Three adversarial audits followed; the last one was clean.
    • Decompression budget: only the bytes REA decompresses are charged. An unrelated member that declares 300 MiB no longer refuses the read.
    • Whole-archive refusals: the JAR is refused wherever JarFile refuses to open it. This is what led to the JDK-faithful reader above.
    • META-INF/ entries are never taken from META-INF/versions/<N>/.
    • Local scopes vs instructions: from class version 51, a local scope must start, and end short of code_length, on an instruction start. This mirrors the HotSpot verifier, through a new one-pass bytecode decoder (src/domain/jvm/javaBytecode.ts). jsr/ret from version 51 are undecodable.
    • Type-table rows are matched to LocalVariableTable rows exactly as HotSpot's copy_localvariable_table matches them, including the duplicate rules.
    • Output change: local entries report scope (aligned | in-code | misaligned | out-of-range | unknown) instead of out_of_range. Line-number entries keep out_of_range.
    • Evidence:
      • 67/67 new hostile JARs match JDK 25 on verdict, message and member SHA-256. So do 142 of the 144 earlier crafted JARs; the 2 others are by design.
      • 75,634 classes from real JARs are byte-identical to JDK 25.
      • Against f19116b77, 309,969 classes in 634 real JARs show 0 new refusals and 0 changed verdicts, and 850,058 real classes show 0 new malformed.
      • Compared with the old zip.js path, the new reader takes 4× less CPU and memory goes from 1.3 to 0.3 GiB.
      • A 2.5 GB-declared inflate bomb stops at the 32 MiB class cap under a 256 MB heap.
    • Remaining by design:
      • A duplicate stored name is refused as ambiguous; the JDK reads the last record.
      • A JAR holding only versioned overrides needs release.
      • JAR signatures are not verified.
      • REA follows JDK 25 where JDK 21 opens a few more archives.
      • A method whose bytecode no local scope needs is not decoded.
  • Base reproduction or other evidence (head 634470298):
    • javap parity: 0 mismatches across 23,408 classes (javac 21/25, ecj, kotlinc, groovyc, r8, java.base, jdk.compiler) and across 4,590 classes from real multi-release JARs (bcprov 1.84, kotlin-stdlib 2.4.0, guava 33.3.1). The comparison covers 35,538 methods and 63,210 line / 49,396 local / 9,955 local-type entries.
    • Multi-release decision vs JDK 25 and 21 JarFile.isMultiRelease: 0 disagreements on 23,903 manifests. These are 20,000 random cases plus 3,903 at the 512/8192-byte buffer boundaries.
    • Crafted ZIP shapes: across 136 checks on 68 crafted JARs (with and without release), REA never states a different fact than the JDK. 91 agree outright. The rest are explicit refusals or unknown/unavailable answers:
      • non-multi-release JARs asked for a release;
      • symlink or encrypted manifests and classes;
      • files the JDK cannot open (CP437 names, encrypted entries);
      • malformed ZIPs (bad CRC, declared/actual size mismatch).
    • Adversarial audits: independent rounds after every fix round; the latest is clean. Fixed findings include:
      • an unbounded table materialization (about 1.5 GiB RSS on a crafted class), now refused before allocation;
      • a JAR digest/read TOCTOU;
      • lifecycle ownership of failed closes;
      • max_locals 0 readiness;
      • spec-invalid constant references;
      • JDK multi-release parity;
      • attribute-flagged and Unicode Path manifest decoys;
      • ./ decoys;
      • directory classification by the Unicode name.
  • User-visible CLI/MCP output: a per-method readiness summary with source file and counts. --class-descriptor adds per-method detail.
  • Remaining proof gaps:
    • Real-toolchain validation covers Linux with JDK 25; the JDK 21 manifest oracle agrees.
    • Peak memory and token usage of representative tasks are not measured.

For evidence-bearing changes:

  • 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

At head 2ed202b0d (the fourth review round; every gate run with TURBO_FORCE=true, 0 cached tasks):

  • npm run verify:jvm -- --require-tools: passed, 16 cases, none skipped.
  • npm run test:local: 316 tests in 16 files (src/jvm, src/domain/jvm, JdkZipCentralDirectory, JdkZipFile, ZipArtifactReader and its consumers).
  • npm run test:focused: 39 tests in 11 files (JVM acceptance, inventory and lifecycle; 6 ZIP acceptance files; apkZip64FramingCli; artifactExtractionCoverage).
  • npm run check:fast, docs:check, format:check, mcp-catalog:check, verify:module-boundaries: passed.

At head f19116b77 (the third review round on top of bd515f2fc):

  • npm run verify:jvm -- --require-tools: passed, 16 cases (JDK 25, javap, CLI and MCP), none skipped.
  • npm run test:local on src/jvm and src/domain/jvm: 161 passed.
  • npm run test:focused on the JVM acceptance, inventory and lifecycle tests: 19 passed.
  • npm run check:fast and docs:check: passed.

At head bd515f2fc (the second review round plus a merge of main at bc143125e):

  • Unit tests: JVM 169, artifacts 217, APK and ManagedCilDecoding 240, all passed.
  • npm run test:focused: 18 JVM acceptance and lifecycle tests, plus 183 tests across 10 ZIP and stable-artifact files: passed.
  • npm run check:fast, docs:check, format:check, mcp-catalog:check, verify:module-boundaries: passed.
  • An independent audit of the second round was clean:
    • 343,569 real class files (JDK 25 lib/modules plus 483 Gradle-cache JARs, including kotlinc and groovyc output, majors 46–69) had 0 new malformed verdicts compared with 252924008.
    • 139 crafted classes were loaded in JDK 25. REA is never stricter than HotSpot. Documented lenient cases at that head were non-ASCII characters in pre-49 names and a type row without a matching LVT row; the latter is modelled since d40c306d1.

Merge note: in the merge commit, the repository's pre-commit hook (oxlint --fix) merged duplicate imports in 14 upstream files this PR does not otherwise touch (+38/−32). Behaviour is unchanged. Undoing it would mean bypassing the hook.

Earlier, at head 634470298:

  • npm run test:local -- src/jvm/*.test.ts src/domain/jvm/*.test.ts src/artifacts/*.test.ts: 162 passed.
  • npm run test:focused on the JVM lifecycle, acceptance and artifact boundary tests: 56 passed. On 30 ZIP-consumer test files (inventory, extraction, Flutter, jadx, SAZ, ASAR, Harmony and others): 251 passed.
  • npm run check:fast, npm run docs:check, npm run format:check, npm run mcp-catalog:check, npm run verify:module-boundaries, npx knip: passed.
  • git merge-tree against current main: merges cleanly.

Compatibility, safety, and release

  • Breaking changes or migration steps: none for the new tool. The shared ZIP reader now treats an attribute-only directory member (no trailing /) as a readable file with a limitation. Two such shapes now fail closed with explicit errors:
    • an attribute-only foo next to foo/bar (prefix conflict);
    • an attribute-only . or empty name (non-normalized path).
  • Real Hopper/Ghidra, browser, or OS coverage: not applicable (engine-free); validated on Linux x64.
  • Package or release metadata impact: new tool and catalog entry; new verify:jvm script.
  • Security, privacy, process, or containment review:
    • read-only, with no execution;
    • one no-follow, identity-checked descriptor;
    • work bounded before allocation;
    • the selected member is hashed after selection;
    • symlink members are never read.

Author readiness

Human owner: @FerroLx

  • 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

https://claude.ai/code/session_0117t1LTnmt5nHnp1WyyJVFC


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/3586 **Author:** [@FerroLx](https://github.com/FerroLx) **Created:** 10/14/2026 **Status:** 🔄 Open **Base:** `main` ← **Head:** `feat/2377-jvm-debug-metadata` --- ### 📝 Commits (10+) - [`db89910`](https://github.com/morluto/rea/commit/db8991069ad4f5364c4056b431e86b7b267e7a3a) feat(jvm): add JVM debug-metadata domain core (#2377) - [`258595a`](https://github.com/morluto/rea/commit/258595a487470bf9bb3e307b3a433e125d8342af) docs(jvm): complete the #2377 spec directory - [`a2d923b`](https://github.com/morluto/rea/commit/a2d923b4c117a6df5c302ad083222d7de5653323) feat(jvm): inspect class-file debug metadata via CLI and MCP (#2377) - [`2395d3d`](https://github.com/morluto/rea/commit/2395d3d4b9cbacdbd72ac61b1fb21dbf1d202ac5) test(jvm): add javac/javap real lane and the analyst guide (#2377) - [`0430d4e`](https://github.com/morluto/rea/commit/0430d4e42c76265d41514e58f0dd5b2dd9cdc3fb) fix(jvm): address three-angle review findings (#2377) - [`8a99aee`](https://github.com/morluto/rea/commit/8a99aeee947f891db855acdde96fb3bb70e21fc3) feat(jvm): flag out-of-range local-variable scopes (#2377) - [`7901488`](https://github.com/morluto/rea/commit/790148827e9298e8032a0eec38fd5a9c43f8cc04) feat(jvm): select a class entry from a JAR, with multi-release (#2377) - [`92e4f25`](https://github.com/morluto/rea/commit/92e4f25978049de8e318a4733ebc0360b7cedf9d) fix(jvm): honor Multi-Release manifest and refuse ambiguous JAR entries (#2377) - [`60840db`](https://github.com/morluto/rea/commit/60840db992ae94548949094f684106ab86fd1783) fix(jvm): guide JAR selection, normalize entry paths, tighten UTF-8 (#2377) - [`baedbfc`](https://github.com/morluto/rea/commit/baedbfceb15299f87c793a01aa5ef7cedf0c357f) fix(jvm): classify archives by magic peek before the class-file budget (#2377) ### 📊 Changes **82 files changed** (+11177 additions, -71 deletions) <details> <summary>View changed files</summary> 📝 `.agents/skills/reverse-engineer-anything/SKILL.md` (+6 -0) 📝 `.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md` (+19 -0) 📝 `README.md` (+1 -0) 📝 `docs/architecture.mermaid` (+3 -0) 📝 `docs/index.md` (+14 -14) ➕ `docs/jvm-debug-metadata.md` (+293 -0) ➕ `docs/specs/2377-jvm-debug-metadata/plan.md` (+18 -0) ➕ `docs/specs/2377-jvm-debug-metadata/spec.md` (+85 -0) ➕ `docs/specs/2377-jvm-debug-metadata/tasks.md` (+19 -0) ➕ `docs/specs/2377-jvm-debug-metadata/verification.md` (+333 -0) 📝 `experiments/analysis-efficiency/source-to-bundle.mjs` (+1 -2) 📝 `package.json` (+1 -0) 📝 `scripts/lib/catalog-builders.mjs` (+5 -0) 📝 `scripts/lib/catalog-core.mjs` (+2 -0) 📝 `scripts/lib/test-projects.mjs` (+1 -1) ➕ `scripts/verify-jvm-debug-metadata.mjs` (+500 -0) 📝 `src/application/WebSourceLocationPorts.ts` (+4 -2) 📝 `src/application/javascript/JavaScriptSemanticGraphConstruction.ts` (+2 -2) ➕ `src/application/jvm/JvmDebugMetadataPort.ts` (+23 -0) ➕ `src/application/jvm/JvmDebugMetadataService.test.ts` (+291 -0) _...and 62 more files_ </details> ### 📄 Description ## Summary Fixes #2377. Adds `inspect_jvm_debug_metadata` / `inspect-jvm-debug-metadata`, a static, in-process reader of the compiler-emitted debug metadata in a local `.class` file or in a class entry of a `.jar`. Multi-release JARs are supported through an explicit `release`. It lets an analyst or agent judge **debugging readiness before attaching a debugger**: which methods carry line numbers and local-variable names, which were stripped, and which are malformed. ## Problem and expected behavior Before this change, an analyst had to run `javap -l` or open a decompiler, then read per-method tables by hand, to answer "can I debug this class with names and lines?". Decompiler line numbers are not compiler positions, so they cannot answer it. Expected: one bounded call returns the following, as artifact-bound Evidence, without executing Java: - per-method readiness (`present | partial | stripped | malformed | not-applicable`); - source file; - line positions and local declarations; - out-of-range scopes. ## Change and scope - `src/domain/jvm/`: a dependency-free JVMS §4.7 reader using `Result` and tagged errors, bounded before allocation. - It decodes `SourceFile`, `SourceDebugExtension` (kept opaque), `LineNumberTable`, `LocalVariableTable` and `LocalVariableTypeTable`. - It binds offsets, slots and scopes to each method's name and descriptor. - It checks `max_locals`, code length and constant-pool kinds. - Repeated or trailing attributes are reported as malformed, never silently merged. - `src/jvm/`: the provider and lifecycle. - **JAR descriptor and cleanup:** one owned JAR descriptor through `withArtifactRegularFile`, with cleanup leases and retained failed-close ownership, following the RustBinaryProvider pattern. - **Classification:** the archive is classified by a magic peek before the class-file budget is applied. - **Multi-release selection:** decided exactly as JDK 25's `JarFile` does (`src/jvm/jarManifest.ts` ports `Attributes.read`). - The manifest is matched by a case-insensitive stored name and follows the JDK's last-duplicate rule. - Manifests over 16,000,000 bytes count as not multi-release, mirroring the default `jdk.jar.maxSignatureFileSize`. - Version directories are limited to 9..2147483647. - Versions are never silently combined. - **JAR reading:** REA opens a JAR with its own reader (`src/artifacts/JdkZipCentralDirectory.ts`, `src/artifacts/JdkZipFile.ts`). It reproduces OpenJDK 25's `ZipFile`: `findEND`, `initCEN`, `checkAndAddEntry`, `checkExtraFields`, `countCENHeaders` and `getInputStream`, over raw bytes. - It refuses the whole archive exactly where `new JarFile(...)` does, quoting the JDK's message: encrypted records, bad compression methods, END and zip64 inconsistencies, and so on. - It reads members from the central directory alone, as the JDK does. It makes no CRC or local-header cross-check, and charges the decompression budget with the bytes actually produced. - The manifest is read as `JarFile.getBytes` reads it. - A shared zip.js-based reader could not be made exact, because zip.js reads END, zip64 END and local headers differently. The JVM tool therefore no longer uses `ZipArtifactReader`. - `inspect_artifact` still lists JARs through the shared reader, so on an unusual JAR the two tools can disagree. Each tool's docs state its own rule. - **Member matching:** JAR members are matched by their stored names, independent of ZIP attribute bits. `./`-prefixed members are inert, as for the JDK, and `META-INF/` entries are never versioned. Symlink-mode members are read as ZIP bytes, as JDK 25 reads them; no host link is followed. - Shared ZIP reader (`src/artifacts/`; #4473 carries the same changes, and the JVM tool no longer uses this reader): - **`storedPath`:** `ArtifactEntry.storedPath` is the raw central-directory name, never the Info-ZIP Unicode Path name. A limitation is recorded when the two differ. - **Directory rule:** a member is a directory only when its stored name ends in `/`. - **Attribute-only directories:** members marked as directories only by attribute bits are readable files with a limitation. This makes two shapes fail closed with explicit errors: - `foo` plus `foo/bar` is a prefix conflict; - `.` or an empty name is a non-normalized path. - **Reused helper:** `peekArtifactMagic` reuses `readStableArtifact`'s no-follow, identity-checked open. - Wiring: contract, CLI, MCP, composition, effects, catalog provider identity, docs, skill routing, and `npm run verify:jvm` with its fixtures. Not included: decompilation, source resolution, and any runtime or debugger attachment. Scope: [contribution scope](https://github.com/morluto/rea/blob/main/CONTRIBUTING.md#contribution-scope). This is static format inspection of a caller-selected local file within existing offline-artifact authority. No Java executes, nothing is fetched, and nothing is mutated. No maintainer scope decision is required. ## Contract and boundary impact - Semantic owner and earliest changed stage: - `src/domain/jvm/javaDebugMetadata.ts` (meaning of the attributes); - `src/jvm/jarManifest.ts` (multi-release decision); - `src/artifacts/ZipArtifactReader.ts` (stored name and directory kind). - Contribution scope fit and linked maintainer decision: static offline parsing; not applicable. - CLI and MCP/tool-catalog contract: - New tool `inspect_jvm_debug_metadata` / `inspect-jvm-debug-metadata`, with inputs `path`, optional `entry`, `release` and `class_descriptor`. - New catalog provider identity. - Provider, bridge, target-format, or platform compatibility: in-process and portable. Inputs are class files of any major version, plain JARs and multi-release JARs. - Evidence, artifact, provenance, or reconstruction contract: - The Evidence carries the archive path and SHA-256, plus the selected member's identity and SHA-256 (hashed after selection), at confidence `observed`. - Limitations are stated per answer. - Target selection, authority, upstream behavior, and cleanup details: one admitted no-follow descriptor per call, with failed-close ownership retained and retried. Cancellation carries the caller's reason. - Generated metadata, package, or installation impact: CI regenerates the catalog. There is no new dependency. - Shared files with #4473: both PRs change `ArtifactReader.ts` and `ZipArtifactReader.ts` identically except for one line. #4473 normalizes member paths with `normalizeArchiveMemberPath`; this PR uses the equivalent `./` strip. Whichever merges second keeps `normalizeArchiveMemberPath`. The other overlaps are tool-registration lines (CLI, server, catalog, docs) to union. Expected effects: - [x] Read existing files or captured data. - [ ] Launch or attach to a process. - [ ] Inject instrumentation or run scripts/code. - [ ] Change process memory, registers, files, or configuration. - [ ] Contact network services. - [ ] Recover protected code or work around protections. - [ ] Documentation or usability only; no target interaction. - [ ] Not sure yet; investigation or maintainer guidance needed. ## Evidence and regression coverage - **Tests added or updated:** - Domain, provider, manifest, JAR-entry, service and ZIP-reader unit tests: 162 run in the latest JVM + artifacts pass. - `tests/process-global/jvmClassFileLifetime.test.ts` covers the lifecycle regressions: failed close, cancellation during final cleanup, and retry. - Each review fix added a test that fails on the previous code. - **Real-toolchain lane:** `npm run verify:jvm` (JDK 25), 16 cases. - It compiles owned `javac` fixtures with `-g` and `-g:none` and compares the decoded facts with `javap`. - It runs a real third-party JAR and a built multi-release JAR through the CLI and stdio MCP, checking that their Evidence matches. - **Review round (head `252924008`):** all 7 automated review findings were confirmed against JDK 25 / HotSpot and fixed, each with a test that fails on the previous code. JAR member names are decoded as UTF-8 like `JarFile` (a ZIP-reader charset option; other consumers keep CP437). Local-variable slots past `max_locals`, out-of-code line pcs and scopes, and bytes after the class file are now `malformed` or refused, as HotSpot rejects them. Evidence is bound to the answered container, entry and release. ZIP limit failures report the bound that was actually hit. The Multi-Release matcher no longer buffers continuation lines. The verify lane uses the resolved `jar` tool. An independent audit of the round was clean: 0 `javap` mismatches over 2,726 real classes, 16/16 UTF-8 name cases match JDK 25 `ZipFile`, and 0 Multi-Release differences across about 35,000 manifests. - **Second review round (head `e271e022c`):** 7 more automated findings, which CassieFox23 confirmed against JDK 21, were fixed, each with a test that fails on `252924008`. The fixes: - a constant-pool count of 0 and a long/double in the pool's last slot are refused; - attribute names that don't resolve to `CONSTANT_Utf8` are refused, or make the method `malformed` inside `Code`; - `code_length` outside 1..65535 is `malformed`; - local names and descriptors are checked against HotSpot's grammar (new `javaDescriptor.ts`); - a `LocalVariableTypeTable` below class version 49 no longer changes a method's state; - a missing entry is reported not-found before the manifest is read. - **Third review round (head `f19116b77`):** 3 automated findings fixed: - the CLI keeps the caller's path through `absoluteCallerPath`, so `link/../Example.class` is resolved by the filesystem after following the link, not rewritten lexically (a POSIX acceptance test fails on `bd515f2fc`); - a loose `.class` whose stable read and hash completed before a failed close or a cancellation now keeps its SHA-256 and size as a `file-snapshot-hash` partial observation (two lifecycle tests fail on `bd515f2fc`); - the docs, tool description, skill reference and spec now say SourceDebugExtension is reported by presence and byte length only; its bytes were never returned (wording fix, no behaviour change). An independent audit of this round was clean. - **Fourth review round (head `2ed202b0d`):** 4 more automated findings, each confirmed on JDK 21 and 25 and fixed with a test that fails on `f19116b77`. Three adversarial audits followed; the last one was clean. - **Decompression budget:** only the bytes REA decompresses are charged. An unrelated member that declares 300 MiB no longer refuses the read. - **Whole-archive refusals:** the JAR is refused wherever `JarFile` refuses to open it. This is what led to the JDK-faithful reader above. - **`META-INF/` entries** are never taken from `META-INF/versions/<N>/`. - **Local scopes vs instructions:** from class version 51, a local scope must start, and end short of `code_length`, on an instruction start. This mirrors the HotSpot verifier, through a new one-pass bytecode decoder (`src/domain/jvm/javaBytecode.ts`). `jsr`/`ret` from version 51 are undecodable. - **Type-table rows** are matched to `LocalVariableTable` rows exactly as HotSpot's `copy_localvariable_table` matches them, including the duplicate rules. - **Output change:** local entries report `scope` (`aligned | in-code | misaligned | out-of-range | unknown`) instead of `out_of_range`. Line-number entries keep `out_of_range`. - **Evidence:** - 67/67 new hostile JARs match JDK 25 on verdict, message and member SHA-256. So do 142 of the 144 earlier crafted JARs; the 2 others are by design. - 75,634 classes from real JARs are byte-identical to JDK 25. - Against `f19116b77`, 309,969 classes in 634 real JARs show 0 new refusals and 0 changed verdicts, and 850,058 real classes show 0 new `malformed`. - Compared with the old zip.js path, the new reader takes 4× less CPU and memory goes from 1.3 to 0.3 GiB. - A 2.5 GB-declared inflate bomb stops at the 32 MiB class cap under a 256 MB heap. - **Remaining by design:** - A duplicate stored name is refused as ambiguous; the JDK reads the last record. - A JAR holding only versioned overrides needs `release`. - JAR signatures are not verified. - REA follows JDK 25 where JDK 21 opens a few more archives. - A method whose bytecode no local scope needs is not decoded. - **Base reproduction or other evidence** (head `634470298`): - **`javap` parity:** 0 mismatches across 23,408 classes (javac 21/25, ecj, kotlinc, groovyc, r8, `java.base`, `jdk.compiler`) and across 4,590 classes from real multi-release JARs (bcprov 1.84, kotlin-stdlib 2.4.0, guava 33.3.1). The comparison covers 35,538 methods and 63,210 line / 49,396 local / 9,955 local-type entries. - **Multi-release decision vs JDK 25 and 21 `JarFile.isMultiRelease`:** 0 disagreements on 23,903 manifests. These are 20,000 random cases plus 3,903 at the 512/8192-byte buffer boundaries. - **Crafted ZIP shapes:** across 136 checks on 68 crafted JARs (with and without `release`), REA never states a different fact than the JDK. 91 agree outright. The rest are explicit refusals or unknown/unavailable answers: - non-multi-release JARs asked for a release; - symlink or encrypted manifests and classes; - files the JDK cannot open (CP437 names, encrypted entries); - malformed ZIPs (bad CRC, declared/actual size mismatch). - **Adversarial audits:** independent rounds after every fix round; the latest is clean. Fixed findings include: - an unbounded table materialization (about 1.5 GiB RSS on a crafted class), now refused before allocation; - a JAR digest/read TOCTOU; - lifecycle ownership of failed closes; - `max_locals` 0 readiness; - spec-invalid constant references; - JDK multi-release parity; - attribute-flagged and Unicode Path manifest decoys; - `./` decoys; - directory classification by the Unicode name. - **User-visible CLI/MCP output:** a per-method readiness summary with source file and counts. `--class-descriptor` adds per-method detail. - **Remaining proof gaps:** - Real-toolchain validation covers Linux with JDK 25; the JDK 21 manifest oracle agrees. - Peak memory and token usage of representative tasks are not measured. For evidence-bearing changes: - [x] Observed, derived, and inferred claims remain distinguishable. - [x] Artifact identity, source provenance, and failed attempts remain preserved. - [x] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible. ## Validation performed At head `2ed202b0d` (the fourth review round; every gate run with `TURBO_FORCE=true`, 0 cached tasks): - `npm run verify:jvm -- --require-tools`: passed, 16 cases, none skipped. - `npm run test:local`: 316 tests in 16 files (src/jvm, src/domain/jvm, `JdkZipCentralDirectory`, `JdkZipFile`, `ZipArtifactReader` and its consumers). - `npm run test:focused`: 39 tests in 11 files (JVM acceptance, inventory and lifecycle; 6 ZIP acceptance files; `apkZip64FramingCli`; `artifactExtractionCoverage`). - `npm run check:fast`, `docs:check`, `format:check`, `mcp-catalog:check`, `verify:module-boundaries`: passed. At head `f19116b77` (the third review round on top of `bd515f2fc`): - `npm run verify:jvm -- --require-tools`: passed, 16 cases (JDK 25, `javap`, CLI and MCP), none skipped. - `npm run test:local` on `src/jvm` and `src/domain/jvm`: 161 passed. - `npm run test:focused` on the JVM acceptance, inventory and lifecycle tests: 19 passed. - `npm run check:fast` and `docs:check`: passed. At head `bd515f2fc` (the second review round plus a merge of `main` at `bc143125e`): - Unit tests: JVM 169, artifacts 217, APK and `ManagedCilDecoding` 240, all passed. - `npm run test:focused`: 18 JVM acceptance and lifecycle tests, plus 183 tests across 10 ZIP and stable-artifact files: passed. - `npm run check:fast`, `docs:check`, `format:check`, `mcp-catalog:check`, `verify:module-boundaries`: passed. - An independent audit of the second round was clean: - 343,569 real class files (JDK 25 `lib/modules` plus 483 Gradle-cache JARs, including kotlinc and groovyc output, majors 46–69) had 0 new `malformed` verdicts compared with `252924008`. - 139 crafted classes were loaded in JDK 25. REA is never stricter than HotSpot. Documented lenient cases at that head were non-ASCII characters in pre-49 names and a type row without a matching LVT row; the latter is modelled since `d40c306d1`. **Merge note:** in the merge commit, the repository's pre-commit hook (`oxlint --fix`) merged duplicate imports in 14 upstream files this PR does not otherwise touch (+38/−32). Behaviour is unchanged. Undoing it would mean bypassing the hook. Earlier, at head `634470298`: - `npm run test:local -- src/jvm/*.test.ts src/domain/jvm/*.test.ts src/artifacts/*.test.ts`: 162 passed. - `npm run test:focused` on the JVM lifecycle, acceptance and artifact boundary tests: 56 passed. On 30 ZIP-consumer test files (inventory, extraction, Flutter, jadx, SAZ, ASAR, Harmony and others): 251 passed. - `npm run check:fast`, `npm run docs:check`, `npm run format:check`, `npm run mcp-catalog:check`, `npm run verify:module-boundaries`, `npx knip`: passed. - `git merge-tree` against current `main`: merges cleanly. ## Compatibility, safety, and release - Breaking changes or migration steps: none for the new tool. The shared ZIP reader now treats an attribute-only directory member (no trailing `/`) as a readable file with a limitation. Two such shapes now fail closed with explicit errors: - an attribute-only `foo` next to `foo/bar` (prefix conflict); - an attribute-only `.` or empty name (non-normalized path). - Real Hopper/Ghidra, browser, or OS coverage: not applicable (engine-free); validated on Linux x64. - Package or release metadata impact: new tool and catalog entry; new `verify:jvm` script. - Security, privacy, process, or containment review: - read-only, with no execution; - one no-follow, identity-checked descriptor; - work bounded before allocation; - the selected member is hashed after selection; - symlink members are never read. ## Author readiness <!-- The human owner completes these declarations before requesting review. The PR author is the owner by default. A different owner must explicitly accept ownership on this PR. AI agents must leave these declarations to a human to dictate to them or there could be catastrophic consequences for many users of REA. Draft PRs may leave them unchecked. These declarations record initial readiness when opening the PR for review; later pushes do not require a new human sign-off. Keep the description and validation evidence current as the work changes. See CONTRIBUTING.md#pull-request-ownership-and-readiness. --> Human owner: @FerroLx - [x] 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. - [x] I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps. - [x] 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](https://claude.com/claude-code) https://claude.ai/code/session_0117t1LTnmt5nHnp1WyyJVFC --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
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#5840
No description provided.