[GH-ISSUE #2181] [Bug] Windows: owned process spawn fails intermittently with Win32 87 (ERROR_INVALID_PARAMETER) on multi-processor-group systems #727

Closed
opened 2026-10-15 18:27:44 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @hzh666kevin-hue on GitHub (Oct 12, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2181

Area

Other 鈥?owned provider process spawn (Windows native process ownership layer)

Observed behavior

rea analyze-javascript-application <dir> --json (and the same operation through MCP) fails intermittently on Windows with:

Preserve caller process affinity failed [C:\...\node.exe] (Win32 87): The parameter is incorrect.

The error appears in details.diagnostics of an execution_failure result with provider_id: javascript-static-analysis and phase: startup. The artifact inventory and application graph succeed; the static parse scope records the adapter failure.

Over 10 consecutive runs of the same command on the machine below, 6 succeeded and 4 failed. The outcome is random per invocation (not alternating deterministically).

Reproduction

  1. Use a Windows x64 host whose logical processors span more than one processor group (more than 64 logical CPUs). Here: 2脳 Xeon E5-2696 v4 = 88 logical processors, split into 2 groups of 44.
  2. Create a directory containing one small ES module file.
  3. Run rea analyze-javascript-application <dir> --json repeatedly (10 times).
  4. Observe random failures with the message above.

Expected behavior

Owned provider processes should start reliably. Preserving the caller's CPU affinity is a nicety; when the mask cannot be applied to the child, the spawn should proceed with the child's default affinity rather than failing the whole provider startup.

Impact and scope

Affects any Windows owned-process spawn that goes through the native addon's spawnProcess (the JavaScript analysis worker; other owned helpers) on multi-processor-group systems. The failure happens before the child starts, so evidence integrity and isolation are not affected 鈥?the operation simply fails. Hosts with a single processor group (64 or fewer logical processors) are unaffected.

REA version

6.4.0 (npm rea-agents)

Runtime and platform

Node.js 22.23.1; Windows 10 19045 x64; 2脳 Intel Xeon E5-2696 v4 (88 logical processors, 2 processor groups); no Ghidra/Hopper/IDA configured.

Target and artifact context

Plain directory containing a single ES module .js file (97 bytes).

Interface

CLI (the MCP path uses the same spawn and is equally affected)

Earliest failure stage

Setup or provider admission

Relevant output or evidence

{
  "error": "JavaScript application analysis failed",
  "code": "execution_failure",
  "category": "execution_failure",
  "message": "Analysis could not complete. Retry once; if it continues, run `rea doctor`.",
  "retryable": false,
  "details": {
    "provider_id": "javascript-static-analysis",
    "operation": "analyze_javascript_application",
    "diagnostics": {
      "source_path": "app.js",
      "phase": "startup",
      "message": "Preserve caller process affinity failed [C:\\...\\node.exe] (Win32 87): The parameter is incorrect.\r\n"
    },
    "captured_output": { "stdout": "", "stderr": "", "exit_code": null, "signal": null }
  }
}

Analysis or suspected cause

Source-derived reasoning, verified experimentally on the affected host:

native/windows/src/process.cc reads the caller's affinity with GetProcessAffinityMask and applies it to the suspended child with SetProcessAffinityMask. On systems with more than 64 logical processors, Windows splits CPUs into processor groups and assigns each new process to a group (Processor Groups 鈥?Win32 API), which the docs describe as round-robin for processes that do not request a group. The mask returned by GetProcessAffinityMask is relative to the caller's group. When the suspended child has been assigned to a different group, the caller's mask is not a subset of that group's active processors, so SetProcessAffinityMask fails with ERROR_INVALID_PARAMETER (87), and the current require(...) treats that as fatal, killing the spawn.

This matches the observed randomness: each spawn lands in a random group, and the mask is only valid for the caller's group.

Proposed fix 鈥?treat affinity preservation as best effort (a mask that cannot be applied cross-group should not fail the spawn):

   DWORD_PTR affinity = 0, systemAffinity = 0;
   require(GetProcessAffinityMask(GetCurrentProcess(), &affinity, &systemAffinity),
           "Read caller process affinity failed", command);
-  if (affinity != 0)
-    require(SetProcessAffinityMask(result->process.get(), affinity), "Preserve caller process affinity failed", command);
+  require(affinity == 0 || SetProcessAffinityMask(result->process.get(), affinity) ||
+              GetLastError() == ERROR_INVALID_PARAMETER,
+          "Preserve caller process affinity failed", command);

Additional context

I rebuilt the Windows x64 native addon from the rea-agents-6.4.0 source with only this change (MinGW-w64, same flags as scripts/build-windows-native.mjs) and re-ran the reproduction 10 times: 10/10 succeeded, where the stock build had failed 4/10. An alternative, more precise fix would query the child's group (GetProcessGroupAffinity) and skip or translate the mask accordingly, but the best-effort variant above is minimal and preserves the current behavior on single-group hosts.

Happy to turn this into a PR if the change direction is acceptable.

Originally created by @hzh666kevin-hue on GitHub (Oct 12, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2181 ### Area Other 鈥?owned provider process spawn (Windows native process ownership layer) ### Observed behavior `rea analyze-javascript-application <dir> --json` (and the same operation through MCP) fails intermittently on Windows with: ``` Preserve caller process affinity failed [C:\...\node.exe] (Win32 87): The parameter is incorrect. ``` The error appears in `details.diagnostics` of an `execution_failure` result with `provider_id: javascript-static-analysis` and `phase: startup`. The artifact inventory and application graph succeed; the static parse scope records the adapter failure. Over 10 consecutive runs of the same command on the machine below, 6 succeeded and 4 failed. The outcome is random per invocation (not alternating deterministically). ### Reproduction 1. Use a Windows x64 host whose logical processors span more than one processor group (more than 64 logical CPUs). Here: 2脳 Xeon E5-2696 v4 = 88 logical processors, split into 2 groups of 44. 2. Create a directory containing one small ES module file. 3. Run `rea analyze-javascript-application <dir> --json` repeatedly (10 times). 4. Observe random failures with the message above. ### Expected behavior Owned provider processes should start reliably. Preserving the caller's CPU affinity is a nicety; when the mask cannot be applied to the child, the spawn should proceed with the child's default affinity rather than failing the whole provider startup. ### Impact and scope Affects any Windows owned-process spawn that goes through the native addon's `spawnProcess` (the JavaScript analysis worker; other owned helpers) on multi-processor-group systems. The failure happens before the child starts, so evidence integrity and isolation are not affected 鈥?the operation simply fails. Hosts with a single processor group (64 or fewer logical processors) are unaffected. ### REA version 6.4.0 (npm `rea-agents`) ### Runtime and platform Node.js 22.23.1; Windows 10 19045 x64; 2脳 Intel Xeon E5-2696 v4 (88 logical processors, 2 processor groups); no Ghidra/Hopper/IDA configured. ### Target and artifact context Plain directory containing a single ES module `.js` file (97 bytes). ### Interface CLI (the MCP path uses the same spawn and is equally affected) ### Earliest failure stage Setup or provider admission ### Relevant output or evidence ```json { "error": "JavaScript application analysis failed", "code": "execution_failure", "category": "execution_failure", "message": "Analysis could not complete. Retry once; if it continues, run `rea doctor`.", "retryable": false, "details": { "provider_id": "javascript-static-analysis", "operation": "analyze_javascript_application", "diagnostics": { "source_path": "app.js", "phase": "startup", "message": "Preserve caller process affinity failed [C:\\...\\node.exe] (Win32 87): The parameter is incorrect.\r\n" }, "captured_output": { "stdout": "", "stderr": "", "exit_code": null, "signal": null } } } ``` ### Analysis or suspected cause Source-derived reasoning, verified experimentally on the affected host: `native/windows/src/process.cc` reads the caller's affinity with `GetProcessAffinityMask` and applies it to the suspended child with `SetProcessAffinityMask`. On systems with more than 64 logical processors, Windows splits CPUs into processor groups and assigns each new process to a group ([Processor Groups 鈥?Win32 API](https://learn.microsoft.com/en-us/windows/win32/procthread/processor-groups)), which the docs describe as round-robin for processes that do not request a group. The mask returned by `GetProcessAffinityMask` is relative to the caller's group. When the suspended child has been assigned to a different group, the caller's mask is not a subset of that group's active processors, so `SetProcessAffinityMask` fails with `ERROR_INVALID_PARAMETER` (87), and the current `require(...)` treats that as fatal, killing the spawn. This matches the observed randomness: each spawn lands in a random group, and the mask is only valid for the caller's group. Proposed fix 鈥?treat affinity preservation as best effort (a mask that cannot be applied cross-group should not fail the spawn): ```diff DWORD_PTR affinity = 0, systemAffinity = 0; require(GetProcessAffinityMask(GetCurrentProcess(), &affinity, &systemAffinity), "Read caller process affinity failed", command); - if (affinity != 0) - require(SetProcessAffinityMask(result->process.get(), affinity), "Preserve caller process affinity failed", command); + require(affinity == 0 || SetProcessAffinityMask(result->process.get(), affinity) || + GetLastError() == ERROR_INVALID_PARAMETER, + "Preserve caller process affinity failed", command); ``` ### Additional context I rebuilt the Windows x64 native addon from the `rea-agents-6.4.0` source with only this change (MinGW-w64, same flags as `scripts/build-windows-native.mjs`) and re-ran the reproduction 10 times: 10/10 succeeded, where the stock build had failed 4/10. An alternative, more precise fix would query the child's group (`GetProcessGroupAffinity`) and skip or translate the mask accordingly, but the best-effort variant above is minimal and preserves the current behavior on single-group hosts. Happy to turn this into a PR if the change direction is acceptable.
Author
Owner

@matbrgz commented on GitHub (Oct 12, 2026):

Root Cause & Win32 API Analysis

On systems with > 64 logical cores spanning multiple Windows Processor Groups (e.g. dual-socket Xeon / Threadripper):

  • Root Cause: Standard SetProcessAffinityMask fails with ERROR_INVALID_PARAMETER (Win32 87) when attempting to set an affinity mask that does not belong to the calling process's current primary group, or when passing a 64-bit mask that references processors outside group 0.
  • Fix: In src/process/ Windows native process affinity management, migrate from legacy SetProcessAffinityMask to SetThreadGroupAffinity / SetProcessDefaultCpuSetMasks, query GetProcessGroupAffinity, and avoid enforcing single-group masks on multi-group hosts.
<!-- gh-comment-id:6134417951 --> @matbrgz commented on GitHub (Oct 12, 2026): ### Root Cause & Win32 API Analysis On systems with > 64 logical cores spanning multiple Windows Processor Groups (e.g. dual-socket Xeon / Threadripper): - **Root Cause:** Standard `SetProcessAffinityMask` fails with `ERROR_INVALID_PARAMETER` (Win32 87) when attempting to set an affinity mask that does not belong to the calling process's current primary group, or when passing a 64-bit mask that references processors outside group 0. - **Fix:** In `src/process/` Windows native process affinity management, migrate from legacy `SetProcessAffinityMask` to `SetThreadGroupAffinity` / `SetProcessDefaultCpuSetMasks`, query `GetProcessGroupAffinity`, and avoid enforcing single-group masks on multi-group hosts.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#727
No description provided.