Files
rea/docs/windows-ghidra-p0.md
T
Stefano RizzelloandClaude Opus 5 ab62ee9dcf fix(windows): preserve caller affinity only within one processor group (#2202)
* fix(windows): preserve caller affinity only within one processor group

`spawnProcess` read the caller's affinity mask and applied it to the
suspended child unconditionally. A mask names processors relative to one
processor group, and Windows assigns each new process to a group of its
own choosing, so on hosts with more than 64 logical processors the child
is often placed in another group. `SetProcessAffinityMask` then rejects
the mask with ERROR_INVALID_PARAMETER (87) and the spawn fails, making
every owned provider process start intermittently on such hosts.

Affinity is a nicety, not an authority or isolation control, so it is now
applied only when the caller and the suspended child both occupy the same
single processor group. A child placed in another group keeps that group's
default affinity and the spawn proceeds. Job Object membership,
kill-on-owner-close policy, owner normalization and priority preservation
are unchanged, and the mask failure remains fatal within a shared group,
where it is not expected.

Single-group hosts, including every host with 64 or fewer logical
processors, are unaffected: `GetProcessGroupAffinity` reports one group
for both processes and the same mask is applied as before.

The optional process-boundary fixture gains an `affinity` observation,
and the native verification lane asserts that an owned spawn succeeds and
that the child's mask matches the caller's whenever both share a single
group.

Fixes #2181

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(windows): refuse to widen a pinned caller's affinity across groups

Addresses review feedback on the first commit.

An unconstrained caller loses nothing when Windows places an owned child
in another processor group, so that spawn proceeds with the group's
default. A caller deliberately pinned to fewer processors than its group
holds is a different case: silently accepting the default would hand the
provider tree the whole group, which `docs/installation.md` tells
operators to rely on affinity to prevent. Such a spawn now fails naming
the constraint instead. `GetProcessAffinityMask` already reports the
group's active set, which distinguishes the two without another query.

Selecting the caller's group at creation through
PROC_THREAD_ATTRIBUTE_GROUP_AFFINITY was tried first and rejected:
`CreateProcessW` fails with ERROR_INVALID_PARAMETER on a host with a
single processor group, both alongside PROC_THREAD_ATTRIBUTE_JOB_LIST and
on its own, so it cannot carry the ordinary path.

The boundary fixture's affinity observation now reports a process id's
own mask rather than its own, because a separate probe process is
assigned its own group and cannot stand in for the process that spawns
the child. Masks and the group's active set cross as fixed-width
hexadecimal strings: a mask can set bits above the safe-integer range,
where `JSON.parse` would round two different masks to one value.

The verification lane asserts the mask is inherited exactly when caller
and child share a group, and otherwise that the caller was unconstrained.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(windows): record what owned-process affinity does not preserve

A mask covering its group's whole active set does not distinguish a
caller the system placed in that group from one that selected the group
deliberately, so an owned child can still be placed elsewhere with its
processor count bounded and its group placement lost. Treating that case
as constrained instead is not available: on a multi-group host an
ordinary process is assigned a group by the system and its mask is that
group's active set, so refusing it would fail every owned spawn there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-13 14:49:34 +08:00

9.9 KiB

Windows Ghidra P0

This guide describes repository main. npm 4.1.0 includes the Windows native bundle; check the release boundary before applying these instructions to an older published package. Native x86 (32-bit) PE support is available on repository main; npm releases through 5.0.0 admit x86-64 PE targets on Windows.

Status: experimental Windows x64 support for the read-only P0 boundary. Windows package builds bundle native process, filesystem, and DACL controls. REA uses them automatically when the analyst selects a Ghidra operation; no additional permission flag or degraded mode is required.

Supported boundary

  • Windows 10+ x64 with fixed local NTFS target and temporary volumes;
  • Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+;
  • an operator-installed Ghidra 12.1.x distribution (verified with 12.1.4);
  • a 64-bit full JDK inside that installation's declared Java range (JDK 21 or newer, with no maximum, for current 12.1 releases);
  • an explicit native, non-managed x86 or x86-64 PE application or DLL; and
  • the 25 read-only Ghidra inventory, memory, and function-analysis operations.

Loaded memory reads and file offsets preserve Ghidra's observed source mappings. PE load-image inspection returns an explicit unsupported attestation status. Independent verification covers admitted DOS MZ, explicit COM, and explicit raw-binary imports on their supported hosts. This does not extend Windows P0 admission beyond native PE. For x64 PE NativeAOT RTR 9.1, the read-only snapshot parser can report metadata through inspect_native_load_image and inspect_native_data_type without a JAR or database mutation. A real Windows-host NativeAOT Ghidra run is still required to establish the complete host workflow.

This boundary does not establish general Windows feature parity. Ghidra GUI state, annotations, other target architectures and formats, and Hopper-only operations remain unavailable through this provider. Managed PE/CLI inspection uses its separate execution-free provider.

REA does not install or upgrade Ghidra, Java, Python, npm, Node.js, Hopper, or a compiler. The adapter uses the packaged Java HeadlessScript; Python and PyGhidra are not prerequisites. Users do not build the native addon.

REA selects its packaged bridge scripts by explicit path. Same-named files or directories in the caller's working directory do not select a different script or require users to clean that directory before analysis.

Native DLL analysis preserves the PE shared-library role in the analysis profile. REA imports an immutable copy for static analysis; it does not load the library into the analyst process or execute its entry routine. The source-owned DLL fixtures have no entry routine or imports and expose two exported functions. The same native snapshot, path-admission and cleanup requirements apply to EXEs and DLLs.

Configuration

npm install --global rea-agents
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
$env:JAVA_HOME = "C:\tools\jdk-21"
$env:REA_ANALYSIS_PROVIDER = "ghidra"

rea doctor --json
rea providers --json
rea inspect "C:\fixtures\sample.exe" --provider ghidra --format json

rea doctor distinguishes installation prerequisites from native artifact or OS failures. It checks the Ghidra release, headless launcher, full JDK, x64 host, and native controls. A missing or mismatched addon requires the matching Windows x64 package; changing MCP registration cannot supply that artifact. A filesystem or access failure identifies the failed native constraint. Other doctor checks, including unconfigured agent integrations, can remain unready while Ghidra itself is available.

rea setup can register supported agent integrations and the bundled skill. Its existing preview and approval apply to configuration writes. Setup preserves valid Ghidra and Java settings even when a separate runtime prerequisite is unavailable.

For MCP, register the resolved Node entry point and the same provider settings:

{
  "mcpServers": {
    "rea": {
      "command": "C:\\tools\\node\\node.exe",
      "args": ["C:\\tools\\rea\\scripts\\rea.mjs", "mcp"],
      "env": {
        "REA_ANALYSIS_PROVIDER": "ghidra",
        "GHIDRA_INSTALL_DIR": "C:\\tools\\ghidra_12.1.4_PUBLIC",
        "JAVA_HOME": "C:\\tools\\jdk-21"
      }
    }
  }
}

These paths are examples. Use the actual package location and restart the MCP client after registration changes. Cold Ghidra import can exceed a client's short default request timeout; the real verification lane allows the existing 330-second provider startup deadline to complete.

Native authority and target identity

The verified boundary excludes SUBST aliases and changing drive-letter/DOS-device namespaces. REA does not claim a separate SUBST detector. Mounted-folder entries that expose reparse tags fail component admission; broader namespace variants have not been established by this lane. Requested paths, final handle paths, volume serials, and file IDs remain separate observations.

The loader validates package version, ABI, Node-API compatibility, PE machine, and artifact SHA-256 before loading the package-owned addon. Native failure reasons remain distinct from a valid installation and an unsupported target.

PE header classification and SHA-256 come from the same open file. The Windows provider admits native x86 and x86-64 PE applications and DLLs and rejects unsupported roles, architectures, managed images, and malformed headers. The original selected source coordinate is preserved for native admission.

Admission opens and checks every path component without following a reparse point. It retains handles, verifies local NTFS semantics, and observes volume serial, 128-bit file ID, final handle path, requested path, and size. Reparse paths, alternate streams, ambiguous coordinates, UNC paths, and other filesystems are rejected with the actual constraint.

The snapshot streams from the admitted source handle on a background worker. Its digest must match the parsed target digest. Ghidra imports that private, immutable snapshot, and the bridge requires the imported program SHA-256 to match before serving operations. Source and snapshot identities remain separate diagnostic observations. Snapshot cancellation is checked between 64 KiB reads and cleanup waits for worker settlement.

Private runtime, transport, and lifecycle

Runtime creation installs a protected DACL permitting exactly the current user and SYSTEM. Readback checks the actual owner and ACEs by handle. Descriptors and snapshots remain locked until owner cleanup. Child files inherit the private boundary. Analysis runs with the caller's authority; these controls are not a sandbox for the Ghidra engine or other processes using the same user.

Windows uses an authenticated listener bound to 127.0.0.1 on an ephemeral port. The endpoint record contains only the literal loopback host and port. The bridge writes a new record and closes its writer before the native reader can admit it. The reader refuses concurrent writers and verifies its opened object's private DACL. The random 256-bit bearer token stays in the private, immutable session descriptor and is redacted from diagnostic output.

The batch launcher uses an absolute cmd.exe, fixed switches, delayed expansion disabled, and quoted tokens. Command-interpreter metacharacters in launch paths are unsupported. JVM home and temporary properties use quoted JDK_JAVA_OPTIONS, preserving spaces. APPDATA and LOCALAPPDATA point into the private runtime, isolating Ghidra preferences, logs, and caches.

Process creation assigns a suspended child atomically to an owned Job Object. Membership and kill-on-owner-close policy are verified before resume. Breakaway is disabled. Only the child's default file owner is normalized; the caller token stays unchanged. Caller priority is preserved, and caller CPU affinity whenever the caller and the child share a processor group. A child that Windows places in another group keeps that group's default when the caller asked for no bound of its own; a caller pinned to fewer processors than its group holds is refused there rather than widened. Close, cancellation, timeout, startup failure, and provider failure terminate the job and verify all members have settled before runtime cleanup. Cleanup removes admitted objects by handle and unlinks reparse entries without following their targets. Abrupt owner death kills the job but can leave private runtime files; crash-time transactional deletion is not claimed.

Verification

See native controls for the separate artifact build lane and native boundary checks. Development tarballs without the built artifact report native controls unavailable; publishing requires the verified artifact and manifest.

npm run verify:windows-native
npm run verify:ghidra:windows
npm run verify:ghidra:windows -- --x86
npm run verify:ghidra:windows -- --dll
npm run verify:ghidra:windows -- --x86 --dll
npm run verify:ghidra:windows:package -- --x86
npm run verify:ghidra:windows:package -- --dll
npm run verify:ghidra:windows:package -- --x86 --dll
npm run verify:ghidra:windows:package -- C:\fixtures\installed-rea C:\fixtures\sample.exe

The package lane must run with an ordinary, non-elevated user token. It exercises CLI inspection and every admitted MCP operation, validates named canonical input/output contracts, checks the source digest stays unchanged, and verifies runtime cleanup. Use a source-owned fixture, one analysis process at a time, and a bounded heap for real-provider verification.

The controlled GitHub workflow runs only default-branch code on its protected self-hosted Windows environment. A separate Linux job builds the matching native artifact. Hosted Windows CI verifies native controls and packaged startup separately from real Ghidra acceptance. Broader hostile-target, concurrently mutable-path, extraction, and IPC-security claims require their own acceptance and are not implied by this P0 result.