* feat(dotnet): expose field constant defaults * test(dotnet): guard field default comparison warning Compare actual ordinary and constant-bearing observations independently of the limitation producer. Preserve warning delivery and unchanged matching assertions while allowing harmless wording changes. Refs #5150 * test(mcp): retain structured bundle path diagnostics Keep absolute-path refusal and all rejected parameter facts independent of legacy flat or current structured SDK diagnostic rendering. Retain relative CLI, absolute MCP and no-Evidence controls. Refs #5150 * Revert "fix(ci): integrate main and repair inherited static checks" This reverts commitafd4d571f6, reversing changes made to317bf3fe23. * fix(ci): replay reviewed field-default integration * fix(analysis-view): keep inferred confidence on views of inferred Evidence (#8099) createAnalysisViewEvidence chose view confidence from the view result kind only, so every summary, page or item view of an inferred parent (compare_source_to_bundle, inspect_evm_interface, partially resolved inspect_il2cpp_metadata) was sealed as derived while keeping the parent's authority. A projection never makes its facts more certain: an inferred parent now yields inferred views through CLI and MCP alike. Observed and derived parents keep their existing view confidence and identities. Fixes #8098 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(apple): read flattened framework Info.plist from Resources (#8101) A macOS framework without Versions/ keeps its binary, _CodeSignature/ and Resources/Info.plist directly in the .framework, as Electron and Gecko apps ship it. Bundle anatomy looked for a shallow bundle's plist only at its root and reported info_plist_path: null under complete coverage. Resolve a shallow bundle's root Info.plist first, then Resources/Info.plist, the same layout a framework version directory uses. Keep the iOS platform rule tied to a root-level Info.plist so a Resources/-only application root is not classified as iOS. Fixes #8100 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(desktop): charge retained rows at their measured representation (#8105) * fix(desktop): charge retained rows at their measured representation The desktop scan reserved 1024 + 24 bytes per path character for every row, omission and the root against its 32 MiB representation budget. Real rows cost 800-940 heap and 490-640 JSON bytes, so the charge overstated them 6-13 times and large macOS bundles stopped at 3,700-7,200 entries, far below max_entries. Charge each path at the larger of its UTF-16 heap size and its escaped JSON size, plus the existing fixed row and omission overheads, in one helper. The bound still covers the retained heap and the serialized report, including control-character paths that expand six times in JSON. Fixes #8104 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(desktop): charge every retained copy of a path The tighter row charge counted each path once, but rows repeat it in convention-fact values and the next probes those facts offer, and host failure issues name the absolute path. Control-character paths under a convention such as Content/Paks or resources/app.asar therefore serialized 47-80 MiB of reports within the 32 MiB budget. Reserve each copy through the same text helper where it is stored: path convention facts with their row, the probes any fact offers (at most two, pinned by a projection test) with that fact, retained read ranges before their read, and probe failure issues before they are recorded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(source-maps): resolve empty sourceRoot sources against the map URL (#8107) webpack 5 and tsc write "sourceRoot": "" for map-relative sources. Since5f702dd6cthe shared join added "/" for any non-null root, so these sources resolved from the URL root: rxjs's ../../../src/internal/Observable.ts under /npm/rxjs@7.8.1/dist/cjs/internal/ became /src/internal/Observable.ts, and xterm's webpack://xterm/./src/... became https://host/webpack://xterm/.... Treat an empty root like a null root in sourceMapReference, the single join used by trace-web-source-location, analyze-javascript-application and the web source-map fetcher. Chrome, Firefox and trace-mapping skip an empty root, and tc39/ecma426#183 wrote the current steps to match them. Non-empty roots, including "/" and repeated slashes (#6726), keep their existing behavior. Fixes #8106 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(javascript): stop reporting self-defaulted receivers as module loads (#8109) The CommonJS loader search treated any repeated state on its own ancestor chain as an unknown loader. A self-default such as `session.messages = session.messages || []` forms that cycle, because a member read inside the assigned value was attributed to the later call's read site and selected the same write again. Each `push` on the value was then reported as an unresolved `require` load, and graph coverage became partial. Record a member read inside a flowed value as a depth-1 captured selection at its own position, as member mutations already do for flowed origins. Treat a revisited state as adding no source; the cycle's other entries are still expanded once each. Remove the superseded ancestor tracking. Fixes #8102 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(captures): admit browser-serialized HAR request URLs (#8112) * fix(captures): admit browser-serialized HAR request URLs har-schema@2.0.0 checks request.url with ajv-formats' strict RFC 3986 `uri` format. Browsers record and send URLs as WHATWG serializes them, which keeps characters such as `|` and `{` unescaped in the query, so one Google Fonts or GraphQL GET entry made inspect-web-network-capture and environment-build reject the whole HAR. HAR 1.2 defines request.url only as the absolute request URL. Own that correction in the HAR validator: every URL the RFC 3986 check accepts stays accepted, and a URL that WHATWG URL serialization reproduces unchanged is accepted too. Spellings WHATWG would rewrite (a backslash authority, a tab, a space) and relative references still fail. Fixes #8110 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(captures): state that RFC 3986 request URLs stay admitted The HAR request URL rule is "passes RFC 3986, or WHATWG serialization reproduces it unchanged". A spelling WHATWG would rewrite therefore fails only when it also fails the RFC 3986 check; an uppercase host still passes. Say so in the docs and the validator comment, and pin the accepted uppercase-host case in the CLI test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(apple): charge the XML plist decoder's DOM before decoding (#8115) * fix(apple): charge the XML plist decoder's DOM before decoding The XML preflight charged 4 bytes per character and 256 bytes per start or end tag. The decoder builds an @xmldom/xmldom DOM that measured 1,000-1,160 bytes per element and up to 240 bytes per text, comment or CDATA node, which were not charged at all. A 1.75 MB plist of 230,000 <true/> lines was admitted under the 64 MiB decode budget and peaked at 249 MiB of heap; shipped Apple plists peaked at 80-104 MiB. Charge each element and each text, comment, CDATA or DOCTYPE node at a measured upper bound, keeping the character and number-projection terms. The binary estimator already bounds its decoder and is unchanged. Fixes #8113 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(apple): charge every XML element, attribute and reference The XML preflight recognized only element names starting with an ASCII letter, so names such as é, _, a CJK or astral character escaped the element charge: 100,000 <é/> elements (0.5 MB) built an 84 MiB DOM before the plist decoder rejected the tag. Attributes and entity or character references were also uncharged; 20 attributes per element measured about 210 bytes each, and each reference about 26 bytes beyond its characters. Classify markup by the character after "<" instead of a name pattern, so anything other than an end tag, comment, CDATA, PI or declaration counts as an element (saxes rejects malformed markup before the DOM is built). Charge each unquoted "=" in a start tag as an attribute node and every "&" as a reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(apple): charge XML references only where the parser expands them The previous commit charged every "&" in the document as a reference, including literal ampersands inside CDATA sections and comments. A valid plist with one CDATA string of 1,000,000 ampersands was refused although it is a single node the decoder holds as one string. Count "&" during the existing token walk, only in text runs and in the attribute values of start tags, so comments, CDATA, PIs and declarations add no reference charge and the scan stays a single pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(fit): preserve leading U+FEFF in property strings (#8083) * fix(environment): retain conflicting scenario MIME uncertainty (#8082) * fix(environment): retain conflicting scenario MIME uncertainty * test(mcp): assert structured absolute-path diagnostics * test(mcp): assert typed selector and stdio diagnostics * fix(electron): keep literal IPC sends ambiguous beside unresolved handlers (#8120) * fix(electron): keep literal IPC sends ambiguous beside unresolved handlers A main handler on an unresolved channel can receive any literal renderer send of a compatible mode, but pairing ignored it unless it had a scoped receiver. The send was then reported unpaired, or paired with another handler. Treat an unresolved-channel handler as a competitor that never pairs. The wrapper pass seeds only calls with a literal first argument, so it now records which invocations of a dynamic site it traced. The syntax finding gains `invocation_channels: "literal"` only when every invocation is a traced direct call of an unexported local function supplying a literal channel. Such a site is already represented by its per-invocation findings and does not compete. Fixes #8118 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(electron): keep guarded wrapper registrations competing in IPC pairing A guarded wrapper registration keeps its predicate on a call path and has no IPC finding. It still counted toward `invocation_channels: "literal"`, so pairing lost it: a send on that channel was reported unpaired, or paired uniquely with a direct handler. The enumerated site now carries these channels as `guarded_invocation_channels`, and each one competes for its own channel without pairing. Index each binding's direct call sites by appending in place. Spreading the earlier calls on every reference was quadratic in a file's call count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): remove unused exports that fail deep static checks (#8095) * fix(ci): remove unused exports that fail deep static checks Knip on main reports three analysis-view hub re-exports, two type re-exports, an unused artifact-format guard and an alias constant. Drop the dead re-exports and guard, and give the debugger control-reply budget its own literal: it is a separate budget from the caller command-output default, as its comment already states. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(config): read the Rizin control budget from its own constant The Rizin control-reply admission limit still took its value from the caller command-output default, while GDB used DEBUGGER_CONTROL_REPLY_BYTES. Read the dedicated constant in both so the control budget has one owner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * perf(javascript): keep one member-effect record per stored fact (#8103) Member-mutation indexing appended a binding write or escape record for every traversal state that reached it. States that differ only in coverage fields the record never stores (origin, owner initializer, getter widening) produced identical records, and each later receiver query rescanned all of them. Record each stored fact once, alongside the existing required-module effect deduplication. Propagation is unchanged. Effect ordering also re-walked the same top-level statement ancestry for every candidate record. Cache it with the other initializer positions, and test the selected path before the costlier receiver comparison. On the #7774 reproducer the semantic IR and the complete analyze-javascript-application Evidence are byte-identical, while the CLI run drops from 146 s to 40 s. Refs #7774 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(native): open extension, framework, XPC and plug-in bundle directories (#8117) * feat(native): open extension, framework, XPC and plug-in bundle directories open_binary and the CLI inspection commands admitted only .app directories and pointed every other Apple bundle at the JavaScript analyzer. Resolve .appex, .framework, .xpc, .systemextension, .bundle and .plugin directories through their Info.plist CFBundleExecutable, in the Contents or flat root layout, plus the versioned framework layout under Versions/Current. Exactly one layout may declare the bundle and every resolved path must stay inside it. Unresolvable bundles fail closed with typed constraints, probed paths and the Info.plist that was read. Other Apple bundle types are refused as unsupported instead of being offered the JavaScript route. Resource-root discovery shares the same layout rule, so inspect_plist reads a versioned framework's Info.plist. The recognized bundle vocabulary now has one owner in the domain. Refs #4310 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(native): refuse kernel extensions and state bundle error fields Name the Apple bundle types binary routes recognize but do not open (.dext, .kext, .qlgenerator, .mdimporter) explicitly, so a kernel extension is refused as unsupported instead of receiving the JavaScript route. Document which bundle refusals carry probed_paths and info_plist, and make success tests throw the typed admission error before asserting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(native): open flattened frameworks through Resources/Info.plist Electron and Gecko apps ship frameworks without Versions/: the program file sits at the framework root beside Resources/Info.plist, the layout #8101 taught bundle anatomy. Read that layout for a framework without Versions/, and keep the versioned layout for one with it, so a versioned framework's root Resources link is never read as a second declaration. The resource root reports it as flattened-framework. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * chore(security): configure dependency and ownership reviews * perf(javascript): index member-effect paths for projected reads (#8108) * perf(javascript): index member-effect paths for projected reads Each projected read scanned every member-write record of its binding and tested the record path against the selection. On the #7774 reproducer only 3.7% of scanned records pass that test. Index each binding's append-only records by path in a lazily built trie that extends itself with newly appended records, and visit only records whose exact keys a selected choice names or whose keys are wildcards. Candidates come back in record order and still pass the exact predicate, now owned next to the index as semanticEffectPathMatches. The semantic IR and the complete analyze-javascript-application Evidence are byte-identical. On the full reproducer each worker phase drops from about 80 s to 29 s, and to 8 s together with #8103. Refs #7774 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(javascript): stop escape checks at the first indexed match The escape check only asks whether any matching escape is unsuperseded, so it does not need ordered candidates. Share one trie traversal between the ordered write candidates and an early-exit visitor for this check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(hopper): distinguish an absent demo dialog from publisher rejection (#8074) * fix(hopper): distinguish an absent demo dialog from publisher rejection The Linux demo watcher collapsed an admitted launcher with no first-run dialog into unsupported_demo_dialog. Report that post-authentication timeout as demo_dialog_absent, keep the unsupported dialog code for an observed but unrecognized Hopper window, and allow REA_HOPPER_DEMO_DIALOG_TIMEOUT_MS to widen the fixed wait. Closes #8008. * fix(hopper): tolerate callers without the dialog timeout argument * fix(hopper): report an invalid demo dialog timeout as its own failure An invalid REA_HOPPER_DEMO_DIALOG_TIMEOUT_MS exited 73 and was shown as invalid_launch_command. Exit 82 is invalid_dialog_timeout. The public error names the setting, shows the rejected value and source, lists the allowed range, and says to restart REA. Drive the demo watcher with controlled windows and time. No window exits 81, a Hopper window without a supported dialog exits 76, and a dialog that arrives after the old 10 second limit is still accepted. * fix(hopper): keep startup stderr capture on AdapterFailure DemoWatchStop was inserted above with_stderr, so launch failures called a method they no longer had and became unexpected_failure. Restore with_stderr on AdapterFailure and reject a watcher fixture that cannot capture stderr. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> * test(ci): require provider checks and reduce test startup (#8145) * test(ci): require provider coverage and reuse test sessions * fix(ci): prepare the runtime before Python producer tests The fast Python job ran the suite on a fresh checkout, so analysis-efficiency checks failed without compiled process modules. Install locked dependencies and prepare the runtime before the suite. * fix(ci): keep provider test reports inside the checkout The provider-report reader accepted an arbitrary path from argv. Resolve the path and reject anything outside the checkout before reading it. The packet and signal workflows write those reports under .cache. * fix(ci): require a test module in each named Python bridge family Discovery still runs every bridge test. The mitmproxy, native, and pwndbg families must each keep a test module, so deleting one cannot pass through the others. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> * fix(javascript): emit no http request for a target that is not a URL (#8147) * fix(javascript): emit no http request for a target that is not a URL `node:http` parses a string target with `new URL(...)` before a request object exists, so a literal that is not an absolute URL throws `ERR_INVALID_URL` synchronously. The analysis reported `endpoint` at `resolution: "complete"` for those calls, and the graph then projected `constructs-request` as `resolved`. Measured on Node 24.22.1: http.get("nope") -> THREW ERR_INVALID_URL http.get("example.test/api") -> THREW ERR_INVALID_URL http.get("/api/items") -> THREW ERR_INVALID_URL http.get("/api/items", { host }) -> THREW ERR_INVALID_URL http.get("") -> THREW ERR_INVALID_URL http.get("//example.test/x") -> THREW ERR_INVALID_URL http.get("http://") -> THREW ERR_INVALID_URL http.request("nope") -> THREW ERR_INVALID_URL https.get("/api/items") -> THREW ERR_INVALID_URL --- accepted --- http.get("http://example.test/x") -> OK ClientRequest http.get("HTTP://example.test/x") -> OK ClientRequest http.get("http:/x") -> OK ClientRequest http.get("http:///x") -> OK ClientRequest http.get("http:x") -> OK ClientRequest `URL.canParse` is the exact predictor of that branch, so a relative spelling such as `"http:x"` keeps its operation. It already has precedent in this codebase (`javascriptPackageImportAliases.ts`). Two boundaries are deliberate. `fetch` and `new WebSocket` are host-supplied globals: a browser resolves a relative target against the document URL, so their spellings are not judged by Node's absolute-URL rule. A protocol mismatch such as `http.get("ftp://...")` is also left alone because an options object can rescue it, so it is not a provable throw. The guard is scoped to `method === "get" | "request"`, which only `requestMethod`'s `http`/`https` builtin branch can produce. * chore: retrigger title and readiness checks after the title was corrected * refactor(ci): unify lane ownership and guard Python tests (#8150) * refactor(ci): unify lane ownership and guard Python tests * fix(ci): reject entirely skipped bridge subtests * docs: correct the raw ARM example and document a Cortex-M language override (#7483) * docs(raw-binary): replace the STM32-style ARM example The headline raw example opened ecu.bin at 0x8000000, an STM32 flash address, while the arm lane decodes in ARM state. Map a 32-bit ARM-state image at 0x8000 instead, and state beside the example that Thumb-only images, including Cortex-M firmware, are not supported. * docs(ghidra): document an interim Cortex-M language override Ghidra imports little-endian ARM ELF files as ARM:LE:32:v8, which defaults to ARM state and marks Thumb code only from $t mapping symbols and odd function-symbol addresses. Unpinned arm ELF already admits REA_GHIDRA_LANGUAGE_ID, so document ARM:LE:32:Cortex (ARMv6-M, ARMv7-M, ARMv7E-M) and ARM:LE:32:v8-m (ARMv8-M) as an unverified interim. State the override's full effect while it is set: raw-binary, DOS, DOL, PSP and language-pinned lanes such as RISC-V refuse it, and REA asks Ghidra to import every other target in the ARM language, because it does not check the override's processor against the target. * fix(ida): admit architecture families by allowlist (#7485) IDA refused MIPS, RISC-V, PowerPC, Hexagon and arm64_32 by denylist, so any family nobody excluded was reported supported, and the label ternary printed "Hexagon" for any other metadata family. Admit exactly the families IDA admits today (x86, x86_64, arm, arm64) and name every other family from its own architecture id. Families that Ghidra admits keep the pointer to a Ghidra profile. Refusal text now uses architecture ids, for example "no verified mips lane" instead of "no verified MIPS lane". * ci(ghidra): probe M-profile ARM and Xtensa decoding (#7482) * ci(ghidra): probe M-profile ARM and Xtensa decoding The embedded plan relies on ARM:LE:32:Cortex, ARM:LE:32:v8-m and Xtensa:LE:32:default, but CI only exercised ARM:LE:32:v7. Import source-owned bytes for each language at 0x1000 with -noanalysis and check how they decode from the language's default context: Cortex and v8-m must start in Thumb state, v8-m must decode the ARMv8-M MSPLIM register, and Xtensa must decode entry and retw.n. Add the probe scripts the job runs, including the existing ARM context probe, to the ghidra_bridge change scope. * ci(ghidra): match decode-probe operands as complete operands The operand expectation searched the instruction text for a substring, so mrs/4/msp also accepted mrs r0,msplim. Compare it with each operand Ghidra reports for the instruction, so only a complete operand matches. The Cortex (msp, lr), v8-M (msplim, lr) and Xtensa (a1) expectations are unchanged. * test: guard target format vocabularies and DOL snapshot saves (#7481) * test(mcp): save and reopen a GameCube DOL snapshot #7290 reported that DOL Evidence could not be bundled or saved;50822e1effixed the bundle manifest and added bundle export/import coverage, but no test saved a DOL session through close_binary. Save a DOL snapshot with recorded Evidence, check its target and artifact manifest, then reopen it and replay the read without running the provider again. * test(binary): require every target format in each target vocabulary A new target format must be added by hand to the Evidence subject, bundle manifest, snapshot target and live summary vocabularies; missing one caused #7290. Give every BinaryTarget format a fixture through a mapped type, so a new format fails compilation until it has one, and push each fixture through the producers that persist or project targets. Caller-selected format hints must name executable target formats. This is a guard until target formats have a single owner. * test(mcp): assert the replayed DOL result and the owned replay limitation The DOL snapshot replay test checked the subject and the provider-call count but not the answer, so a replay returning null or another value would pass. Compare the replayed normalized result with the original. Name the replay limitation as SNAPSHOT_REPLAY_LIMITATION in the snapshot cache that adds it, and assert that value instead of repeating the sentence. * test: list Intel HEX and S-record targets in the vocabulary guard Upstream added the intel-hex and srec formats (#7136). The guard requires a fixture for every target format, so after merging main it no longer typechecked; both formats now have archive fixtures and pass the same persistence and projection checks. * test: list UnityFS among the target format fixtures Upstream #7699 added the unityfs target format, so the vocabulary test's per-format fixtures must name it; without one the test no longer typechecks against main. * test: list uImage among the target format fixtures Upstream #7234 added the uimage target format, so the vocabulary test's per-format fixtures must name it; without one the test no longer typechecks against main. * test: list the ESP image among the target format fixtures Upstream #7498 added the esp-image target format, so the vocabulary test's per-format fixtures must name it; without one the test no longer typechecks against main. * fix: correct stale provider refusal and diagnostic text (#7484) * fix(ghidra): name every admitted family in the architecture refusal The refusal listed x86, x86_64, arm, arm64, MIPS, RISC-V, raw 6502 and raw m68k, but the admission set also holds powerpc and hexagon. Build the text from SUPPORTED_ARCHITECTURES, marking raw-only families, so a newly admitted family cannot be left out again, and name the refused family. * fix(hopper): stop calling the Ghidra Hexagon lane verified Hopper declined Hexagon targets with "Select a verified Ghidra Hexagon profile instead", but REA has no verified Hexagon lane. Record whether each declined family's Ghidra lane is verified, and say that Ghidra admits Hexagon through an unverified lane. Name declined families by architecture id, as the Ghidra and IDA refusals do. * fix(ghidra): name the pinned language in profile readback refusals The session readback said "MIPS" whenever a provider-pinned profile conflicted with the target or with REA_GHIDRA_LANGUAGE_ID, although the RISC-V, PowerPC and DOL lanes are pinned too. The readback helper was MIPS-named and re-parsed the language slot that ghidraProfileLanguageOverride already owns. Move it beside that owner as a family-neutral readback and name the pinned language id instead. * fix(ghidra): list DOL among independently verified load images GameCube DOL load images are independently verified, but the declined format reason, the inspect_native_load_image capability note and the Windows P0 guide listed only DOS MZ, DOS COM and raw-binary. * fix(ghidra): count RV32E's sixteen integer registers The EF_RISCV_RVE refusal described a 15-register ABI. RV32E has 16 integer registers, x0-x15, as docs/ghidra-riscv.md already says. * fix(elf): name the refused e_machine "unsupported ELF architecture" did not say which machine was refused. Include the numeric e_machine, for example EM_XTENSA 94. * test(hopper): assert declined-lane facts and owned refusal reasons The MIPS case banned the phrase "not verified" instead of checking facts, so wrong advice would still pass. Expose each declined family's Ghidra lane status, assert those statuses, check that the advice depends on the status, and compare Hopper's refusals with hopperDeclinedArchitectureReason. The RISC-V refusal table now compares the provider's reason with ghidraRiscvUnsupportedReason and expects the EF_RISCV_RVE declaration name rather than fixed wording. * test(ghidra): assert verified load-image formats as owned facts The declined-format test required display phrases such as "DOS MZ" and "GameCube DOL", so harmless rewording failed it while wrong guidance containing those names passed. The format list was also spelled three times: in the attestation's membership check, its reason and the inspect_native_load_image capability note. Give the verified formats one owner with the formatters for both texts, assert the list as a fact, and compare the delivered reason and capability note with those formatters. * fix(hopper): stop restating which Ghidra lanes REA has verified Hopper's refusal for families it declines carried its own table of which Ghidra lanes REA has verified (mips, riscv and powerpc verified, hexagon not). That restated the Ghidra provider's facts in Hopper and goes stale whenever they change, the drift that made the Hexagon refusal wrong in the first place. Reading Ghidra's facts from Hopper would couple the providers instead; no module in src/hopper imports from src/ghidra. The table and hopperDeclinedGhidraLane are removed. hopperDeclinedArchitectureReason now states only what Hopper owns: it does not admit the family, and the Ghidra provider's lane reports whether REA has verified it. The arm64_32 sentence is unchanged. The test checks every declined family against the formatter: each reason names the family and Ghidra, and apart from the name all reasons are the same text, so none states a per-family Ghidra status. * fix(hopper): route declined families to Ghidra without promising its lane facts The refusal said the Ghidra lane reports whether REA has verified it, but Ghidra admits Hexagon with no such note, so a caller could not find the promised status. Hopper now only states that it does not admit the family and routes the caller to the Ghidra provider. --------- Co-authored-by: hhang <169027046+xu91102@users.noreply.github.com> Co-authored-by: Kaoru0822 <kanadekitauji@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ShuaixinHuang <1836656514@qq.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> Co-authored-by: Henglie <90338155+Henglie@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: 吴佳浩Alben <35291372+XiaoBinGan@users.noreply.github.com> Co-authored-by: Entrpi <133157614+Entrpi@users.noreply.github.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
56 KiB
Managed-code analysis
REA inspects .NET PE/CLI artifacts without loading or executing their code. It can identify an assembly, inspect metadata and CIL, compare members across builds, and connect declared native calls to supplied native-analysis evidence. CIL is Common Intermediate Language, the instruction format used by managed assemblies.
The seven shipped tools are:
| Task | MCP tool |
|---|---|
| Identify an artifact and its managed deployment form | inspect_managed_artifact |
| Inspect types, signatures, method bodies, calls, and field access | inspect_managed_members |
| List declared native calls and native implementation indicators | inspect_managed_native_boundaries |
| Compare members across builds and map build-local tokens | compare_managed_members |
| Check native call declarations against supplied export or function evidence | verify_managed_native_boundaries |
| Import decompiled code against verified static member identities | import_managed_reconstruction |
| Add managed findings to an application graph | project_managed_application_graph |
Each has a matching CLI command: replace underscores with hyphens and prefix
the name with rea, for example rea inspect-managed-artifact.
Native-body bridge mapping and managed runtime execution are outside the
current tool set.
The canonical tool inventory is the build-generated catalog.
Inspect an assembly
rea inspect-managed-artifact /absolute/path/Assembly.dll --json
rea inspect-managed-members /absolute/path/Assembly.dll --json
Use the artifact result to identify the assembly and its metadata coverage.
Member results retain build-local tokens, signatures, CIL bodies, and decoded
relationships. Use an exact reported full_name with --type when a focused
inspection is needed; see selected member inspection.
Compare separate builds with member comparison, preserving
their paths, digests, and selection scope.
Field constant defaults
inspect_managed_members reads field-owned values from the ECMA-335 Constant
table without loading the assembly. Each selected field's constant contains
the declaration's row offset, element-type code, blob index and exact raw_hex,
plus its decoded element_type and value. For example, an i4 declaration
with bytes 865d0000 returns 23942.
Boolean, character, signed/unsigned integers, floating-point, UTF-16 strings
and class-null declarations are supported. i8 and u8 values are decimal
strings to avoid JSON integer precision loss. Floating-point negative zero,
NaN and infinities are the strings -0, NaN, Infinity or -Infinity;
raw_hex preserves their original bits. UTF-16 code units, including embedded
NULs and unpaired surrogates, are preserved.
constant: null means neither a Constant declaration nor HasDefault was
observed. Historical Evidence can omit the field entirely; omission does not
prove that the artifact has no default. Missing or duplicate declarations,
inconsistent flags, malformed values and unsupported element types return
state: "unavailable", a limitation and partial coverage while retaining other
member facts. This decoding shares the existing metadata representation budget
and only decodes blobs for the selected fields.
Retained managed-members field pages can select fields: ["constant"] while
keeping original field identity and source offsets. These values are metadata
declarations, including enum members' encoded underlying constants, not observed
runtime state or inferred enum-name mappings. Parameter/property defaults are
not exposed by this field-focused reader. Member comparison still covers field
names, flags and signatures; it does not compare constant defaults.
The encoding follows ECMA-335, Partition II, sections 22.9 and 23.1.16.
Metadata overview and retained rows
The existing CLI and omitted/complete MCP detail return complete metadata
inventories. For identity and classification questions, select
inspect_managed_artifact with
{"path":"/absolute/path/Assembly.dll","detail":"summary"} on the first call.
This keeps the complete source in the session and returns a distinct linked
inspect_analysis_view overview with complete PE/CLI, classification and its
evidence, metadata status, module/MVID, assembly, frameworks, returned-row counts,
coverage/issues and limitations. No assembly is loaded or executed.
ECMA-335 permits empty metadata streams to be omitted. An absent #Blob
stream therefore does not prevent module or assembly identity from being read
when all blob indexes are zero; a nonzero reference to that missing heap is
reported as a missing-stream issue.
Use normalized_result.parent_evidence_id to page original rows:
{
"source": {
"kind": "retained-evidence",
"evidence_id": "<parent_evidence_id>"
},
"view": {
"kind": "managed",
"collection": "attributes",
"offset": 0,
"limit": 16
}
}
Collections are references, resources and attributes; rows retain their
original values, nulls, order, metadata tokens and offsets. Pages retain source
metadata status and coverage. Exhaustion covers the returned parent collection,
not all possible metadata, and empty rows under partial/unavailable metadata do
not establish absence. Tokens are build-local and require the recorded artifact
SHA-256 and known module MVID for cross-observation matching.
Retained views do not repeat the parser or require the source file to remain
available. Portable views use the authenticated original Evidence inline,
including the existing inspect-analysis-view CLI. Session record lifetime and
export/import semantics remain unchanged. A server without Evidence retention
rejects selected summary before analysis; complete delivery remains available.
Member summary and retained member rows
Complete inspect_managed_members delivery always includes the assembly-wide
member_refs and exported_types, which type_names does not narrow, so even
one selected or unmatched type can return megabytes. In MCP, select
{"path":"/absolute/path/Assembly.dll","type_names":["Namespace.Type"],"detail":"summary"}
to keep the complete source in the session and receive identity, metadata
status, the selection with its unmatched names, coverage/issues, the separate
user-string coverage, limitations and per-collection counts. Then page original
rows with view kind managed-members:
{
"source": {
"kind": "retained-evidence",
"evidence_id": "<parent_evidence_id>"
},
"view": {
"kind": "managed-members",
"collection": "call_edges",
"offset": 0,
"limit": 64
}
}
Collections are types, exported_types, fields, methods, member_refs,
call_edges, field_accesses and user_strings (the literals). Each page names
its collection, retains original rows, order, tokens, signatures and body facts,
and carries the source selection and coverage; user_strings pages carry the
literal pool's own coverage. Older Evidence that never examined exported types
or user strings reports a null count and refuses those pages rather than
returning an empty one.
To find specific retained members without returning their IL anchors, select exact names and defining types, then project columns:
{
"source": {
"kind": "retained-evidence",
"evidence_id": "<parent_evidence_id>"
},
"view": {
"kind": "managed-members",
"collection": "methods",
"names": ["ParseNumber", "ParseReadNumber"],
"declaring_types": ["Newtonsoft.Json.JsonTextReader"],
"fields": ["name", "declaring_type", "signature"],
"offset": 0,
"limit": 64
}
}
Filters are exact and case-sensitive; lists are alternatives and filters
intersect. An empty list matches nothing. Names select name in types,
exported types, fields, methods and member references; in call edges they select
target_name, and in field accesses field_name. Declaring-type filters select
declaring_type in fields/methods, parent_name in member references,
target_declaring_type in call edges and field_declaring_type in field accesses.
Types, exported types and user strings do not accept a declaring-type filter;
user strings do not accept a name filter. No display strings are reparsed.
fields accepts only columns of that collection. Omitting it preserves full
rows, including method bodies. Selecting body explicitly returns the full
recorded body and anchors; selecting no columns ([]) returns identity fields.
Projected or filtered rows add their original source_index; tokens and source
byte offsets remain, with defining/parent tokens or IL-site endpoints where
applicable. The page retains module/MVID, artifact digest, parent Evidence,
producer selection and source coverage. Full facts remain in the parent.
Paging offsets and totals count known matches in original order. source_count
counts the producer-returned collection; unresolved_filter_rows counts rows
that could match but lack a requested name/type. Those candidates remain unknown
and are excluded from the matched total. Exhausting the known matches does not
prove absence among unresolved or unexamined source rows.
CLI summary and saved member rows
The CLI accepts the same complete and summary detail choices. Omitted or
--detail complete returns complete Evidence. To receive a summary, choose a
new Evidence bundle destination so its original complete parent remains
available after the command exits:
rea inspect-managed-members /absolute/path/Assembly.dll --type Namespace.Type \
--detail summary --evidence-bundle /absolute/path/evidence/members.json --json
--evidence-bundle writes the complete original Evidence in REA's canonical
bundle format. Its parent directory must exist, and it never replaces an
existing path. Relative paths resolve against the command's working directory.
The option also works with complete delivery when a saved bundle is useful.
Summary without a destination is rejected before analysis. A publication failure
returns a typed error with the completed Evidence under
details.partial_observation; it does not report successful summary delivery.
Cancellation before publication prevents the new output. Once atomic
publication has started, it can complete despite cancellation; a cancelled
delivery then retains the committed path and byte count under
details.diagnostics.evidence_bundle.
Copy normalized_result.parent_evidence_id from the summary into the
source.evidence_id field of the managed-members request above, then save that
small request as members-page.json and run:
rea inspect-analysis-view /absolute/path/evidence/members-page.json \
--evidence-bundle /absolute/path/evidence/members.json --json
The view command authenticates the selected bundle and resolves that exact
parent ID. It does not reopen the assembly, so the original target may have
been moved or removed. Subsequent calls can select another collection or use
the returned coverage.next_offset to continue the same collection. An inline
source already includes its parent and cannot also select --evidence-bundle.
Summary changes delivery, while type selection still controls decoding and the same inspection resource limits apply. Each separate CLI page reads and authenticates the saved bundle; the full source is retained on disk.
Shipped scope
The canonical parser admits PE/CLI bytes and their metadata/CIL without loading
an assembly. It reports observed implementation markers and unavailable facts;
it does not itself unpack .NET single-file hosts or infer a NativeAOT identity
from an ordinary native PE. It does not decode IL2CPP; use
IL2CPP metadata for the image, type and method names in a
Unity global-metadata.dat. Inputs without admitted CLI metadata do not
become managed assemblies through naming or routing guesses.
Inspect separately obtained components explicitly and preserve their identities.
For ordinary .NET single-file hosts, use the existing artifact tools to inventory and extract bundle members, then inspect the extracted assembly explicitly with the managed tools.
A valid implementation marker establishes a candidate native boundary, not recovered native semantics or runtime behavior.
An assembly targeting .NET Standard
declares API compatibility across multiple .NET implementations. REA preserves
that target in target_frameworks and leaves runtime_family as unknown
unless independent markers identify a runtime family, such as a ReadyToRun
signature.
When ReadyToRun is observed, runtime_family remains modern-dotnet even if
Unity or target-framework hints are also present; the separate hint evidence
is retained in the classification evidence list.
Resource admission
The static provider admits regular input files up to 128 MiB before retaining their bytes. Reads remain bounded if a file grows after admission. Metadata, CIL decoding, and projected inspection records have a separate conservative 64 MiB representation budget; shared heap strings are cached, but repeated output occurrences still consume that budget. These are inspection resource policies, not PE format limits or a hard bound on total V8 heap use.
Path-based member comparison uses the same per-input snapshot policy and inspects its two selected paths sequentially, retaining each inspection rather than both full byte snapshots. The limit therefore remains a per-file resource admission rule, not an aggregate comparison limit. Comparison Evidence keeps the caller-selected paths as its parameters and artifact locations.
Input overflow returns a resource constraint. Decode or projection overflow
reports resource-limit with unavailable or partial coverage; omitted facts do
not become empty observations. Ambiguous PE section mappings are rejected
rather than interpreted according to section order.
Minimal Edit-and-Continue metadata marked by #JTD is not a standalone
assembly inventory. The pinned .NET 9 reader
uses different reference widths for this mode, and delta handles require the
baseline and prior generations. The released
metadata serializer
emits this marker with #-; the
PE writer
can embed that metadata root. REA reports unsupported-metadata at the marker
header before interpreting table rows, preserving the selected path, artifact
digest, parsed metadata version and stream names, and admitted PE/CLI facts.
Metadata status is partial; member and native
declaration inventories are unavailable. Empty collections do not establish
absence, and a requested type name is not declared unmatched. Ordinary
standalone #~ and #- metadata keep their existing interpretation. This does
not add raw .dmeta input, baseline attachment or delta reconstruction.
Regression coverage uses authored metadata in a generated PE shell; it does
not establish naturally emitted application PEs, CLR loadability or a real
Edit-and-Continue execution.
Selected member inspection
Member inspection admits type names, tokens, list ranges/counts, nesting facts,
module identity and their output representation before decoding members. If
later decoding or projection reaches the 64 MiB representation budget, the
result retains this type inventory with coverage.state: "partial" and the
original resource-limit coordinate. It does not return partially admitted
member collections: empty fields, methods, references and edges are unexamined,
not proof of absence; exported types, user strings and signature-resolution
attestation remain null. Summary and retained pages keep this partial coverage.
The capacity limit is unchanged. If metadata or the type inventory itself cannot
be admitted, the result remains unavailable.
Use the returned types[].full_name values to select declaring types before
their signatures and CIL bodies are decoded:
rea inspect-managed-members /absolute/path/Assembly.dll --type Example.Parser
MCP uses inspect_managed_members with
{"path":"/absolute/path/Assembly.dll","type_names":["Example.Parser"]}.
Repeat --type or supply multiple unique type_names for several types.
Names match the reported full_name exactly. Top-level names require that
namespace-qualified spelling. Nested declarations also accept their bare
name, including when their own namespace is nonempty; this can select several
types with that name. Selection uses the parsed TypeDef facts and comparison
uses the returned declaring types, rather than reconstructing scope from the
caller's spelling. Each type's enclosing_type_token records its enclosing
TypeDef. nesting_read distinguishes complete source rows (true), incomplete
current rows (false), and historical Evidence (null). A null enclosing token
alone does not establish that a type is top-level.
Each selected type also retains name_sha256, a framed digest of its namespace,
name and enclosing names. It uses the same bounded, cached owner as signature
type operands and remains available when selection omits the enclosing types.
It adds one fixed-size digest per returned type rather than repeated name paths.
The result preserves the requested names in selection.type_names and reports
unmatched names separately. If metadata names are unreadable, limitations retain
that uncertainty. With no selection, selection is null and whole-assembly
inspection keeps its existing resource policy. Selection restricts types,
fields, methods, call edges, field accesses and user strings; member references
remain an assembly-wide inventory, so output size is not guaranteed to be small.
Coverage describes the selected members. Comparisons leave members outside the
opposite observation's selection unknown, and application graphs report their
selected scope as partial. Neither makes an absence claim about the whole
assembly. Older member Evidence without selection remains readable. The
comparison resolves a member's declaring TypeDef token in its own source, then
checks that nominal name_sha256 against the opposite selected type set.
Pairing, absence and aggregate selected-scope agreement share these retained
facts, including enclosing names, rather than requested spellings or display
labels. Equivalent requests can cover the same types, while the same display
name can select different nested owners. Such disjoint selected owners cannot
establish a member pair or added/removed status even when both inventories are
complete. Missing nominal identities cannot establish selected membership,
including historical selected Evidence that retains only display names;
the explicitly limited historical whole-inventory comparison remains available.
Incomplete selected inventories or missing identities leave aggregate agreement
unknown and coverage partial; known observed member pairs remain available.
Complete empty selected inventories agree even if their unmatched request names
differ. Whole-assembly and selected
inventories remain distinct scopes, and each source retains its exact request.
The method_rows interval records the examined zero-based MethodDef offset, row
count and full table total before type selection. A complete observation can
cover only that interval. Comparison keeps omissions from a method page unknown:
build-local row positions cannot prove member absence across builds. Historical
Evidence without this interval also cannot establish method absence; observed
pairs and field inventory comparisons remain available.
For MCP callers, request detail: "summary" and page the types collection
using the returned parent Evidence ID. Select one or more returned full names
in a subsequent inspect_managed_members request. Selection is a recovery path,
not a guarantee: a single type or the assembly-wide member references can still
exhaust the representation budget. A successful command exit means the Evidence
was returned; inspect coverage.state and coverage.issues before treating any
collection as complete.
User strings
ldstr anchors keep their build-local 0x70 tokens. user_strings.literals
lists each distinct token loaded by an inspected method body once, in token
order, with its #US file offset and decoded value. Values keep their stored
UTF-16 code units, including unpaired surrogates. Token 0x70000000 names the
heap's empty entry at offset 0 and reads as ""; any other item there is
malformed. The terminal flag is checked against ECMA-335 II.24.2.4, and the
length prefix against the width II.23.2 assigns. A difference is reported with
its byte offset as a user-string issue while preserving the readable literal.
Toolchain conventions can differ; this conformance observation does not
establish runtime rejection. A missing #US stream is one stream-level issue;
a malformed item is reported against its token. User strings carry their own
coverage: any of these makes user_strings.state partial and lists the issue
in user_strings.issues, while the member coverage and metadata.status are
unaffected, and readable literals remain. The state is also partial when an
inspected method's CIL was not fully decoded (a malformed exception section
alone does not count) or a method that may belong to the inspected types was
not inspected (its row could not be read, or its type's name could not be read
under a type_names selection), because ldstr instructions there were not
observed. Normalized CIL digests do not include decoded values.
compare_managed_members does not decode or compare literals: an unchanged
method does not establish unchanged strings, and the comparison says so in its
limitations. Older member Evidence without user_strings reads as null: not
examined.
Edge target declaring types
member_refs[].parent_name, call_edges[].target_declaring_type, and
field_accesses[].field_declaring_type name the type that declares each
target. Referenced spec signatures validate encoded type coordinates against
the metadata tables, including nested type arguments and custom modifiers.
TypeRef resolution scopes are checked transitively with cycle detection;
readable names survive malformed scopes as partial evidence.
TypeDef and TypeRef names use the namespace-qualified full_name
form, and nested types keep bare names. A generic-instantiation TypeSpec
parent names its generic type, such as System.Collections.Generic.List`1.
Other well-formed TypeSpec forms and ModuleRef or MethodDef parents stay
null. A method-spec call edge reports the name and declaring type of the
generic method it instantiates. A field access is named only by a field or a
field-signature MemberRef. TypeSpec and MethodSpec blobs are decoded with the
member-signature grammar, and a MethodSpec must instantiate a generic method
with as many type arguments as it declares. Malformed or unsupported rows and
blobs, and arity mismatches, make coverage partial. A readable MethodSpec
method column that names a method, or a TypeRef whose resolution scope is
invalid, keeps its name.
Older member Evidence without these fields reads them as null: unknown.
Analysis objective
Start by identifying the artifact with inspect_managed_artifact, then inspect
its types and CIL with inspect_managed_members. Use member comparison
for another build, and managed/native boundary rules
when matching declared native calls to supplied native Evidence. Static inspection
does not load or execute the assembly. Reconstructed structure and unresolved
native/runtime questions retain their own authority and coverage.
When the selected source changes during acquisition or no longer matches its
admitted digest, inspection and comparison report retryable artifact_changed;
any completed hash retains the observed digest and byte count. Retry against a
stable source. This differs from corrupted retained Evidence, which remains an
Evidence-integrity failure. Malformed selected PE input has the same input-format
classification in inspection and comparison. Its issue identifies the selected
filename and argument: path for inspection, or leftPath / rightPath for CLI
comparison. A failed right-side comparison retains the completed left observation.
Member comparison
Call edges and field accesses retain il_offset as their instruction site within
the caller's build-local method token and artifact identity. Their
target_identity_sha256 combines the existing enclosing-type nominal digest,
member name and resolved decoded signature. Comparison joins edges to their
instruction sites rather than array positions and compares these identities;
display labels alone cannot establish equal targets. Same-named nested types
under different enclosing types and different overload signatures stay distinct.
This is nominal metadata identity, not assembly resolution or runtime binding.
Historical edges without a site or identity, unresolved/cyclic type scopes,
TypeSpec owners and unresolved MethodSpec instantiations retain unknown target
equivalence. Valid MethodSpecs combine the base member identity with the
resolved instantiation signature, preserving generic argument distinctions.
Names and semantic labels do not establish durable member identity. Comparison preserves observed differences, structural inferences, and unknowns separately.
Cross-version matching first pairs members of the same declared type and name.
A current declared type uses its observed name_sha256, so same-named nested
types in different enclosing types stay distinct even in selected inventories.
Malformed, missing, contradictory, cyclic or over-deep enclosing identity remains unknown;
the comparison preserves display names without using them to repair that gap.
Historical observations retain an explicitly limited bare-name mode when either
side predates the identity facts. A known current identity gap still remains
unknown beside a historical observation. Matching then pairs
methods by exact CIL/signature identity, then by signature, then by
structural body shape; fields by signature. Exact CIL/signature identity
requires equal raw CIL digests and decoded fingerprints. Equal decoded
fingerprints alone cannot establish an exact match: they omit switch targets
and collapse distinct NaN encodings. Only then may a remap pair a
renamed or moved member across declared types or names: a method by exact
CIL/signature identity or structural body shape, a field by exact decoded
signature. A remap never takes a member whose own declared type and name remain
unpaired on the other side, so a member is never taken from its own changed
counterpart. A member whose declared identity is unresolved could belong to any type. It
never makes a declared-identity pair, and any remap group it joins stays
ambiguous. Unresolved same-named counterparts prevent definitive added/removed
claims, while observed names and tokens remain available in the result.
The exact-signature key uses names only as part of that
full tuple; names alone never select a pair. Duplicate tuples remain ambiguous
rather than being paired by token order. Every round compares signatures by
resolved_sha256: the raw signature with each TypeDef or TypeRef operand
replaced by a digest of that type's namespace and name, hashed as separate
values so A + B.C differs from A.B + C, after its enclosing types for a
nested type (Outer/Inner). The defining assembly is not part of the name:
a TypeDef and a TypeRef with the same namespace and name compare as the
same nominal name, and a TypeRef's ResolutionScope assembly is ignored, so a type
forwarded from one assembly to another still pairs. Two different types with
the same full name in different assemblies therefore have equal name digests.
Comparison reports this scope limitation inline; it does not establish CLR
type identity or runtime binding equivalence.
Nesting deeper than 64 levels is not named, and such a signature compares by
its raw digest. Comparison keys keep resolved and raw digests apart, so a raw
digest never pairs with an equal resolved one.
Raw signature bytes name types by metadata row, and rows are build-local, so
the same signature usually has different raw bytes in two builds. A signature
that names no types resolves to its raw digest. When an operand cannot be
resolved, such as a TypeSpec, or when REA could not parse the signature, the
raw digest stands in. Missing or unreadable TypeDef and TypeRef operands are
reported as coverage issues. Invalid NestedClass rows retain their failure
reasons and withhold TypeDef signature identities; type names and nesting are
parsed once by their shared owner. Each current inspection records
signature_resolution: "type-names-v2", even when none of its operands can be
resolved. Version 2 also appends readable names to signature operands, for
example class:7772 (AnalysisJob). Coded indexes remain build-local references;
names are display text and do not change raw or resolved signature digests.
Nested names use Outer/Inner. Generic instantiations keep their decoded
arguments, for example class:9 (System.Collections.Generic.List`1)<string>;
custom modifiers are named too. Valid but unresolved TypeSpecs, invalid scopes,
unreadable type names, cycles and nesting beyond the cap keep their raw operand
spelling. Invalid or missing metadata coordinates retain the existing malformed
signature result and source issue; rendering does not bypass validation.
Version 1 Evidence remains readable and compares using the same digest policy.
Historical Evidence defaults this attestation to null. When either
observation predates resolution, both sides compare raw digests, which pair only where type rows agree,
and the comparison reports that limitation. The signature is exact whether or
not REA decoded it, so undecoded signatures pair by that tuple too, but they
never enter structural rounds. An unmatched member is therefore reported as
unknown, not added or removed, when its own signature was not decoded or when
the other side has an unpaired member, one-sided or ambiguous, with the same
declared type and name whose signature was not decoded. When both observations
attest type-name resolution, a decoded signature that still compares by its raw
digest is treated the same way: an unmatched member with one, or beside an
unpaired same-named member with one, is unknown, because its raw bytes name
types by build-local row. A matched unresolved signature also remains unknown
unless another observed dimension establishes a change; identical row bytes
can reference different TypeSpec contents. Its resolution limitation is retained
even when another dimension changes.
For a matched method, an unavailable or partial body makes body-shape facets
unknown while preserving observed signature differences. For complete bodies,
a difference in either the raw CIL digest or the decoded fingerprint is reported
in the cil dimension, including reordered switch targets with equal decoded
fingerprints. Structural identity compares resolved signatures, the limited
decoded-CIL tuple fingerprint,
constants, and bounded shape context. It reports all candidates at the winning
score and remains ambiguous when the evidence does not distinguish them. The
digest does not itself remap metadata tokens.
Matched methods also compare aligned references. call-shape and
field-shape report opcode and operand-kind differences; call-target and
field-target report differences in the observed target declaring type or
name from call_edges and field_accesses. Target tokens remain build-local
and are not compared across artifacts. Missing or unaligned edges and missing
target names leave that facet unknown; equal names do not establish overload
identity or runtime dispatch. exception-shape compares clause flags and
ranges. exception-target compares the nested nominal catch-type name digest;
for resolved TypeDefs it also compares the enclosing assembly identity. A
different known name or assembly scope establishes a change. Equal TypeRef
names remain unknown because their resolution scope is not resolved here.
Exception tokens remain in the source facts but are not cross-build identity;
an unresolved catch type leaves that facet unknown. The method body retains the
display class_type, nested class_type_name_sha256, and whether its local
token identifies a TypeDef. A TypeDef digest alone does not prove identity
without matching assembly identity. Missing assembly identity and equal TypeRef
names leave the comparison unknown.
Tokens are always remapped through observed structure. A caller cannot carry
0x06001234 into a new MVID and assume it names the same method.
NativeAOT metadata recovery
NativeAOT removes the ordinary CIL method bodies, so a CIL decompiler cannot reconstruct those bodies as C# source. The executable remains a native target for Hopper or Ghidra, where decompilation produces pseudocode and analyst inference. This is a different route from ordinary managed member inspection.
NativeAOT is not limited to Windows .exe files. The native image can be PE,
ELF, or Mach-O, and NativeAOT can also publish shared libraries (PE DLL, ELF
.so, or Mach-O .dylib) with explicitly exported entry points. Identify the
image from its bytes and loader metadata, not its filename extension. When the
image is inside an application bundle, inventory or extract the bundle first
and carry the component identity into native analysis. Matching PDB, ELF debug,
or dSYM sidecars can add symbol evidence when the selected provider supports
them; verify their identity and keep them separate from observations made from
the executable itself. Microsoft's NativeAOT overview
lists supported OS/architecture targets and deployment limitations, while its
native library guide
describes exported shared-library entry points.
REA offers a built-in read-only snapshot parser for x64 PE/RTR 9.1 metadata and
an optional Linux headless Ghidra adapter for annotations and broader recovery.
The adapter requires a locally built REA_GHIDRA_NATIVEAOT_JAR; REA does not
install it or its toolchain. The NativeAOT guide defines
these routes, their layout/host restrictions, derived-memory provenance, and
verification coverage.
REA's experimental Windows Ghidra boundary admits native x86, x86-64, ARMNT (Thumb-2) and ARM64 PE applications and DLLs for static analysis. For x64 PE NativeAOT, the read-only parser also runs there; the NativeAOT guide lists what a Windows host covers.
For NativeAOT analysis, preserve the selected native artifact's path and digest and report recovered type data with its returned provenance. Linking those types to native function addresses requires verified provider evidence; names or vtable similarity alone do not prove that a native function implements a specific managed method.
Managed/native boundary rules
verify_managed_native_boundaries compares declared P/Invoke module and entry
point identities with supplied native export or function evidence. Name/module
matches establish declaration links; they do not establish that a call executed.
When either side lacks a usable module identity, a matching name remains
unresolved and retains the candidate evidence; REA reports a module mismatch
only when both identities are available and their format-specific comparison
establishes a difference. ELF module names retain case, PE module names compare
case-insensitively, and a case-only match for Mach-O or an unknown format stays
unresolved because the available Evidence does not establish case behavior.
For PE imports, .dll, .exe, a trailing period, and absolute paths suppress
appending .dll; other names may also be tried with .dll. Unix loaders use
the platform suffix (.so or .dylib) and lib prefix variations. A suffix
already present at the end of the name or before a period still gets an
appended-suffix variation, so Linux can match nativedep.so.6 with
libnativedep.so.6. The lib prefix is omitted when the name contains /, and
absolute paths are used as supplied. POSIX paths keep backslashes as literal
characters. These candidates follow Microsoft's native library loading rules.
These filename rules compare declared and observed identities; they do not
establish which library the runtime loaded.
Signatures retain their decoded fields, but a Field or MethodDef row whose
signature has the other member kind is malformed and cannot establish an
unchanged signature. Comparison retains exact byte-based pairs for malformed
signatures, including empty, truncated, or invalid headers, but reports them as
unknown with a signature-malformed limitation when no change is observed.
This applies to historical Evidence without signature-resolution attestation as
well. Runtime-provided methods are counted separately in
native_implementations.runtime_provided; they have no file-backed native body
for this workflow to map and do not increase native_body_unresolved. Historical
verification Evidence without this separate count leaves it unknown (null).
An empty declared import name remains unchanged in verification Evidence and
produces no native-name candidates. With a supplied PE native context and
no_mangle: false, an unspecified charset includes the ANSI A candidate;
auto includes the Windows Unicode W candidate. Other native formats do not
inherit these Windows defaults. Known ELF and Mach-O contexts use the declared
entry point without A/W suffixes or Windows stdcall decoration, including when
the metadata declares an explicit ANSI or Unicode marshalling charset. The
original charset and calling convention remain unchanged. This follows the
.NET runtime's platform-specific entry-point lookup.
Windows name decorations are static inferred candidates;
exact spelling still suppresses decoration and no runtime binding is proven.
The mapping follows Microsoft's charset guidance.
Partial input coverage records a residual question even when no unresolved
individual boundary was produced. Selected literal inventories likewise remain
partial when unknown MethodDef ownership can hide a selected body.
Metadata tokens are not native addresses. Native body mapping for C++/CLI, ReadyToRun, NativeAOT, and IL2CPP requires separately verified format and provider support. The managed static tools do not recover those mappings. The optional Ghidra NativeAOT extension has its own supported boundary.
Declared import inventory supports a bounded positive claim. Its absence does not exclude dynamic resolution, generated code, protected code, native helpers, or server-side behavior.
Application graph relations
project_managed_application_graph turns member Evidence into contains
edges for declarations, imports edges for P/Invoke declarations, and two
relation kinds read from method bodies:
callslinks twomanaged-methodnodes when acall,callvirt,newobjorjmpsite in the caller targets a MethodDef in the inspected module.accesseslinks amanaged-methodnode to amanaged-fieldnode for each field it loads, stores or takes the address of.
Each edge stands for every site between one source and one target. Its
properties carry site_count and per-opcode opcodes counts; accesses
edges also list access as read, write and/or address. Edge evidence
links the member Evidence.
Some sites are counted rather than projected: calls to MemberRefs (members of
other assemblies or of generic type instantiations), generic method
instantiations (MethodSpec), unknown targets, ldftn/ldvirtftn function
pointer loads, recursive self-calls (the graph has no self-loops), MemberRef
fields, and sites whose method or field node is absent, for example under a
type selection. Limitations name each counted class, and graph coverage is
partial with omitted_count equal to the omitted sites when nothing else is
incomplete. A missing calls edge is therefore not evidence that a method is
never called.
Evidence record shape
Member comparison, application graph projection, and managed/native verification authenticate supplied inspection Evidence and check that any subject SHA-256 matches the normalized artifact SHA-256. A valid Evidence ID alone does not establish that those two identities agree. Evidence without a subject remains usable, with no subject digest available to cross-check.
Artifact inspection returns the selected path, byte length and SHA-256, observed PE/CLI classification, assembly and module identities, and metadata coverage. Member inspection binds build-local tokens, signatures, row offsets, method bodies, and decoded relationships to that artifact and MVID. Missing or malformed facts remain explicit in coverage and per-member status.
Metadata table streams may carry the CLR-compatible ExtraData extension
(HeapSizes flag 0x40), which Microsoft readers honor although it is not
specified by ECMA-335. REA skips its fixed four-byte word after the row counts
before decoding table rows; the word's value is not a length. An incomplete
word is reported as a metadata coverage failure at its file offset, without
reading beyond the declared table stream.
For unoptimized #- metadata, nonempty FieldPtr and MethodPtr tables
translate TypeDef list positions to physical member definitions. REA resolves
these indirections for member ownership and local attribute-constructor types;
field and method tokens still identify the original definition rows.
field_list and method_list retain the metadata's list coordinates, which
refer to the pointer table when present rather than necessarily contiguous
definition rows. Invalid or nonmonotonic list coordinates and malformed or
ambiguous pointers leave affected owners unknown with partial coverage while
preserving readable member and attribute facts.
The retained inverse indexes are admitted against the existing representation
budget before allocation.
Custom-attribute type_name uses Outer+Nested when its local TypeDef or
TypeRef enclosing chain is readable. It remains null when the constructor's
type or required enclosing facts cannot be resolved.
exported_types reports ExportedType rows as declared. Facade assemblies such
as .NET System.Runtime.dll provide their types this way: forwarder: true
marks a type forwarder, and implementation_token names the AssemblyRef, File,
or enclosing ExportedType row that implements it. REA does not load the
implementing assembly to resolve the forwarded definition, and
compare_managed_members does not compare exported types. Member Evidence
recorded before this field existed reads back with exported_types: null,
which means the table was not examined.
CLI #GUID heap values retain their exact 16-byte GUID text. REA does not impose
RFC 4122 UUID version or variant bits on MVID, EncId, or EncBaseId because
ECMA-335 metadata does not require those bit patterns. Tokens and RVAs are
build-local coordinates, not durable cross-build identities or native addresses.
Field and method signatures validate embedded TypeDefOrRefOrSpec operands
against the artifact's metadata tables. An invalid operand leaves the member
name and raw signature digest available while marking that signature malformed
and coverage partial.
A fat method header's nonzero LocalVarSigTok must name an existing StandAloneSig
row whose blob is a structurally valid LocalVarSig. REA validates its local
types and exact blob coverage, retaining local_var_signature with the raw blob
digest and whether named types, modifiers, or generic variables require metadata
or declaring context. Historical Evidence defaults this field to null. An invalid
token or malformed signature leaves decoded CIL anchors intact but marks the
body malformed and coverage partial. Unsupported local type encodings retain
the readable body with partial coverage. Zero continues to mean no locals
signature. Raw table tokens are not cross-build identities. Comparison uses
validated context-free signature digests; signatures requiring metadata or
declaring context remain unknown across artifacts.
Decoded-CIL fingerprint
inspect_managed_members exposes two method CIL hashes with different byte
boundaries:
| Field | Input | Excluded from the digest |
|---|---|---|
il_sha256 |
The exact il_size raw CIL bytes after the tiny or fat method header |
Method header, locals signature blob, alignment, and extra sections including exception clauses |
normalized_il_sha256 |
UTF-8 JSON.stringify output for the instruction-order array of [opcode, operand_kind, operand] tuples |
Instruction offsets except projected scalar branch targets, switch targets, resolved metadata/string identities, method header, locals, and exception regions |
The normalized field is available only when the entire CIL stream decodes with
status present. A malformed or absent body reports null. The tuple format
is defined here.
Tuple operands use these exact projections:
- operand-free instructions:
noneandnull; - metadata and user-string operands: the build-local token as lowercase
0x-prefixed eight-digit hexadecimal; - scalar branches: the target CIL byte offset as a decimal string;
switch: the case count as a decimal string, without target offsets;- signed integer and variable operands: decimal strings; floating-point
operands: JavaScript's
Stringrepresentation of the decoded IEEE value.
For ldtoken MemberRef operands, operand_kind follows the parsed member
signature (field or method); if that signature does not establish the kind,
the anchor keeps the generic token classification.
Opcode names retain short/long forms and prefix opcodes. This makes the value a reproducible decoded-tuple fingerprint, not a canonical semantic CIL identity: it does not resolve tokens across MVIDs, does not commit full control flow or exception semantics, and does not replace the raw CIL digest. Cross-build comparison requires equal raw CIL digests as well for exact CIL/signature matches and reports ambiguity when structural evidence is insufficient.
For example, a present body (including any valid declared locals signature)
whose IL is nop; ret serializes exactly as
[["nop","none",null],["ret","none",null]] and has digest
5e5fad7741cb44bca3a4f045546b7449990da343f612f5f34c0ca30e9eee0636.
This vector specifies the tuple order, null handling, absence of whitespace,
UTF-8 encoding, and lowercase hexadecimal output.
Tool and packaging boundary
The production parser is REA-owned TypeScript and ships with the existing Node application. Deterministic conformance uses source-owned byte-built PE/CLI fixtures and expected semantic facts. An optional real ILSpy lane runs only when its executable is explicitly supplied; the default lane does not establish independent pinned-oracle parity. Production inspection needs none of those tools.
ICSharpCode.Decompiler/ilspycmdcan supply reconstruction inference. A BYO reconstruction import records the supplied version and remains non-canonical. WhenREA_ILSPY_CMD_PATHpoints to an absolute runnableilspycmd,verify:managedruns a source-owned real ILSpy oracle and imports its C# output as reconstruction inference against exact static member Evidence.- The package contains no .NET runtime, SDK, ILSpy installation, proprietary assembly, or compiled conformance fixture.
- Setup never installs or upgrades those tools. Doctor may inspect an explicit optional path without changing it.
Real-tool downloads used in development or CI require exact package coordinates and SHA-256 lock entries. They are cached outside the source tree and restored into isolated directories. Updating any coordinate requires review of license, runtime, supported host, output contract, and conformance results.
Target platform is not host support. The canonical parser must inspect Windows-produced PE/CLI bytes on REA's supported Linux and macOS hosts with the same result. Pinned Windows oracle jobs may provide additional conformance, but do not establish that the REA managed analysis workflow has been verified on a Windows host. The experimental Windows Ghidra boundary has its own host and provider verification scope. Native ReadyToRun, C++/CLI, NativeAOT, or IL2CPP coverage is additionally constrained by the selected Hopper/Ghidra host and format matrix.
Conformance verification
The current lane uses source-owned byte-built fixtures and malformed-input regressions, with optional operator-supplied applications and ILSpy verification.
Run the deterministic lane with npm run verify:managed. See
testing for prerequisites and
coverage. Generated outputs belong outside tracked fixture directories and must
not remain after verification or packaging.
Verification compares semantic facts, not decompiler text. It checks resource ceilings, deterministic ordering, complete cleanup, CLI/MCP parity, Evidence commitments, packed-package behavior, and target-free MCP startup. A parser and an oracle disagreeing on malformed input is investigated explicitly; one tool's acceptance does not automatically make the bytes valid.
Operator-local osu! benchmark
The optional osu! benchmark accepts only paths and expectations supplied by the operator. Run it with:
REA_MANAGED_APP_MANIFEST_PATH=/absolute/managed-app-manifest.json npm run verify:managed
verify:managed always runs a source-owned manifest-verifier self-test first.
If REA_ILSPY_CMD_PATH is set to an absolute ilspycmd path, it also runs a
source-owned ILSpy oracle: version discovery, class listing, bounded C# output
for a pinned fixture type, and import through import_managed_reconstruction.
The verifier prints only command/version, executable and output digests,
Evidence IDs, method locks, and compact fixture identity; it does not print
decompiled C# text. A failing ILSpy oracle fails the verifier because it is an
explicit real-tool claim, but leaving REA_ILSPY_CMD_PATH unset keeps the
oracle disabled.
When REA_MANAGED_APP_MANIFEST_PATH is set, the manifest's target path may be
absolute or relative to the manifest file. A compact manifest contains:
{
"label": "operator-local osu!stable semantic slice",
"target": {
"path": "./osu!.exe",
"sha256": "<exact target digest>",
"mvid": "<exact module MVID>",
"assembly_name": "<expected simple name>",
"runtime_family": "dotnet-framework",
"managed_architecture": "x86"
},
"methods": [
{
"label": "operator-local semantic label",
"token": "0x06000000",
"signature_sha256": "<raw signature blob digest>",
"il_size": 0,
"il_sha256": "<raw CIL byte digest>",
"normalized_il_sha256": "<digest>"
}
],
"application_graph": {
"expected_node_kinds": [
"artifact",
"managed-assembly",
"managed-module",
"managed-type",
"managed-method"
],
"feature_traces": [
{
"label": "operator-local feature label",
"method_token": "0x06000000",
"seed": "<method name, API name, string, or digest to trace>",
"match": "exact",
"case_sensitive": true,
"min_matched_seeds": 1
}
]
}
}
Before evaluating methods, the verifier fails closed on target SHA-256, MVID,
assembly name, runtime-family, and managed-architecture mismatches whenever the
manifest supplies those fields. It then pages each declared MethodDef token
directly by row, so selected methods do not need to appear in the first member
page of a large application. Optional il_sha256 locks the exact raw CIL bytes
in addition to REA's decoded-instruction-tuple digest. That normalized field
has the byte boundaries documented above and is not a complete semantic CIL
identity.
The optional application_graph block reuses those exact-build method
commitments. For each referenced MethodDef token, the verifier builds a bounded
managed application-graph projection from the authenticated artifact and the
single selected member page, then checks requested node kinds and feature-trace
seed matches. This proves only that the selected static facts enter REA's graph
and tracing vocabulary for that exact local build; it does not execute the
application, inspect arbitrary unlisted methods, or infer runtime behavior.
Output contains assertion status and compact identities only: file name, target SHA-256, MVID, assembly name, runtime-family, managed architecture, method tokens, method names, signature digests, IL sizes, normalized IL digests, graph Evidence IDs, node-kind summaries, and trace seed hit counts. It does not print method bodies, reconstructed C#, application data, runtime telemetry, account/service details, full filesystem inventories, or the target's absolute path. The manifest and target remain outside git.
The benchmark may prove that REA reproduces selected facts for that exact local build. It cannot establish general support on its own; source-built conformance remains the admission requirement.
Runtime behavior boundary
Managed inspection does not execute assemblies or observe CLR internals. Questions about actual behavior remain unknown until supported runtime evidence is collected. A direct process capture can retain declared inputs, outputs, filesystem and protocol activity for a target run; it does not prove which managed methods executed or how the CLR resolved them.
Method metadata during build comparison
Managed member comparisons report a metadata dimension when an observed
MethodDef flags or implementation flags value differs, including accessibility
or synchronization changes. Observed method-body init_locals and max_stack
differences also contribute to metadata, even when CIL decoding is incomplete.
For complete bodies, presence or absence of a locals signature also contributes;
the locals table token is build-local and is not compared. Different validated
context-free LocalVarSig digests report a metadata encoding difference without
asserting a semantic type change. Equal digests establish unchanged bytes only
when neither signature requires metadata or declaring context. Historical
unrecorded signatures and context-dependent signatures keep local identity
unknown, preserving any independently observed changes. The same method token
in an artifact with the same authenticated SHA-256 identifies the same local
signature bytes, including for retained historical Evidence.
An unavailable header value keeps body coverage unknown rather than establishing
equality or a difference. These comparisons are separate from the CIL/signature
matching tiers: identical instructions do not establish unchanged method metadata.
The reported difference is static metadata evidence, not proof of runtime behavior.