[GH-ISSUE #1690] [Bug] Windows private runtime doubles the separator for a drive-root parent and rejects every later snapshot #565

Closed
opened 2026-10-15 18:26:13 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @moxi-2534 on GitHub (Oct 11, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/1690

Before submitting

  • I searched the open and closed issues for a related report. (#786 covers forward-slash drive paths; no report exists for a drive-root private-runtime parent.)
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Other — Windows native boundary (private runtime / Ghidra session root)

Observed behavior

On Windows x64, creating a private runtime whose parent is a bare drive root produces a doubled separator:

runtime_create(["D:\\", "rea-ghidra-"]) -> path "D:\\rea-ghidra-<hex>"

Every later operation that resolves a path inside that root is then rejected by the native path admission:

runtime_snapshot(...) ->
Ambiguous Windows path component is unsupported [D:\\rea-ghidra-<hex>\target-<sha>.exe] (Win32 50)

The same call with a normal parent (D:\Tmp) yields D:\Tmp\rea-ghidra-<hex> and succeeds, so the defect is specific to a drive-root parent.

User-visible effect: any Ghidra analysis fails on a host whose inherited temp directory contains a dot-prefixed element. ghidraSessionRoot() must avoid dot-prefixed path elements, so with TEMP=D:\.Tmp it falls back to the nearest dot-free ancestor — the drive root D:\ — and every Ghidra operation then dies in target-snapshot admission:

$ rea analyze C:/Windows/System32/where.exe --json
{"error":"Analysis failed","code":"execution_failure", ... "details":{"provider_id":"ghidra",
 "operation":"list_documents","diagnostics":{"runtime_root":"D:\\rea-ghidra-d648ea05...",
 "failure_cause":{"message":"Ambiguous Windows path component is unsupported
   [D:\\rea-ghidra-d648ea05...\\target-f51fab8041e5.exe] (Win32 50)", "code":"ERR_REA_WINDOWS_NATIVE"},
 "failure_kind":"start","failure_message":"Ghidra target snapshot failed admission"}}}

With TEMP=D:\Tmp TMP=D:\Tmp the identical command completes and returns Ghidra 12.1.3 Evidence, so the provider, bridge, and toolchain are fine.

Reproduction

Smallest form, against the published 6.3.0 artifact:

const addon = require("./native/windows/build/rea-windows-x64.node");
const root = addon.call("runtime_create", ["D:\\", "rea-ghidra-"]);
console.log(root.path); // "D:\\rea-ghidra-<hex>"  <- two separators
await addon.call("runtime_snapshot", [root.handle, "C:\\Windows\\System32\\where.exe", "probe.exe"]);
// Ambiguous Windows path component is unsupported [D:\\rea-ghidra-<hex>\probe.exe] (Win32 50)
  1. TEMP=D:\.Tmp rea analyze C:/Windows/System32/where.exe → execution_failure (above).
  2. TEMP=D:\Tmp TMP=D:\Tmp rea analyze C:/Windows/System32/where.exe → succeeds.
  3. node scripts/verify-windows-native.mjs with the new drive-root control case fails on the 6.3.0 artifact and passes with the fix.

Expected behavior

A drive root is a legitimate private-runtime parent: runtime_create(["D:\\", prefix]) should return D:\<prefix><hex> (exactly one separator), and runtime_snapshot, runtime_open, and cleanup should behave as they do for any other parent.

Impact and scope

  • Affected: the Windows x64 native private runtime (runtime_create) whenever the parent path ends with its separator, i.e. a bare drive root; in practice the Ghidra provider on hosts whose temp directory path contains a dot-prefixed element (ghidraSessionRoot()'s dot-free fallback can return a drive root).
  • Not affected: POSIX, non-drive-root Windows parents, other providers that allocate their runtime under a normal directory.
  • Evidence integrity, process isolation, and cleanup ownership are unaffected; the failure is fail-closed path admission. No security impact observed.
  • Workaround: give REA a dot-free temp base (TEMP/TMP, e.g. D:\Tmp), or keep the inherited temp directory free of dot-prefixed elements.

REA version

rea-agents 6.3.0 (npm release) and repository main at af7a4396

Runtime and platform

Node.js 24.19.0; Windows 10.0.26200 x64; Ghidra 12.1.3 with JDK 21 (Zulu 21.0.12.1)

Target and artifact context

PE32+ x86_64 (C:\Windows\System32\where.exe); not applicable to the native-only reproduction.

Interface

CLI (the same private-runtime path is used by MCP tool calls)

Earliest failure stage

Setup or provider admission

Relevant output or evidence

Unfixed 6.3.0 artifact vs. a build of the proposed fix, same script:

parent "D:\\"    -> root.path "D:\\\\rea-ghidra-723ce07d..."   SNAPSHOT ERROR: Ambiguous Windows path component ...
parent "D:\\Tmp" -> root.path "D:\\Tmp\\rea-ghidra-36eb3a40..." snapshot ok | digest matches: true

# with the fix
parent "D:\\"    -> root.path "D:\\rea-ghidra-a626454f..."     snapshot ok | digest matches: true
parent "D:\\Tmp" -> root.path "D:\\Tmp\\rea-ghidra-b0f93a6b..." snapshot ok | digest matches: true

node scripts/verify-windows-native.mjs with the added driveRootRuntimeParent control:

  • before: AssertionError ... true !== false at the doubled-separator assertion.
  • after: {"ok":true,"artifact":"verified","controls":{...,"driveRootRuntimeParent":true}}.

Typed outcome or recovery expected

execution_failure is the right typed outcome for a genuine admission failure, but a drive-root parent should never fail admission; the runtime should be created and usable, as with any other parent.

Analysis or suspected cause

native/windows/src/filesystem.cc:222 appends a separator unconditionally:

result->path = parent->path + L"\\" + prefix + hex(random.data(), random.size());

while localPath() deliberately keeps the trailing separator of a 3-character drive root (filesystem.cc:26, if (path.size() > 3 && path.back() == L'\\') path.pop_back();). parent->path for D:\ is therefore D:\, and the join yields D:\\<prefix>…; relativePath() then calls localPath() on D:\\<prefix>…\relative, whose component loop rejects the empty component (filesystem.cc:32).

The Ghidra session-root fallback (src/ghidra/GhidraSessionRoot.ts:54, nearestSafeAncestor) is what makes this reachable from ordinary analysis, but it is not itself incorrect — a drive root is a valid parent that the native boundary must accept.

Additional context

Proposed fix (PR to follow): append the separator only when the parent does not already end with one, plus a driveRootRuntimeParent control in scripts/verify-windows-native.mjs and a GhidraSessionRoot contract test that pins the drive-root fallback. Related closed issue: #786.

Originally created by @moxi-2534 on GitHub (Oct 11, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/1690 ### Before submitting - [x] I searched the open and closed issues for a related report. (#786 covers forward-slash drive paths; no report exists for a drive-root private-runtime parent.) - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Other — Windows native boundary (private runtime / Ghidra session root) ### Observed behavior On Windows x64, creating a private runtime whose parent is a bare drive root produces a **doubled separator**: ``` runtime_create(["D:\\", "rea-ghidra-"]) -> path "D:\\rea-ghidra-<hex>" ``` Every later operation that resolves a path inside that root is then rejected by the native path admission: ``` runtime_snapshot(...) -> Ambiguous Windows path component is unsupported [D:\\rea-ghidra-<hex>\target-<sha>.exe] (Win32 50) ``` The same call with a normal parent (`D:\Tmp`) yields `D:\Tmp\rea-ghidra-<hex>` and succeeds, so the defect is specific to a drive-root parent. User-visible effect: any Ghidra analysis fails on a host whose inherited temp directory contains a dot-prefixed element. `ghidraSessionRoot()` must avoid dot-prefixed path elements, so with `TEMP=D:\.Tmp` it falls back to the nearest dot-free ancestor — the drive root `D:\` — and every Ghidra operation then dies in target-snapshot admission: ``` $ rea analyze C:/Windows/System32/where.exe --json {"error":"Analysis failed","code":"execution_failure", ... "details":{"provider_id":"ghidra", "operation":"list_documents","diagnostics":{"runtime_root":"D:\\rea-ghidra-d648ea05...", "failure_cause":{"message":"Ambiguous Windows path component is unsupported [D:\\rea-ghidra-d648ea05...\\target-f51fab8041e5.exe] (Win32 50)", "code":"ERR_REA_WINDOWS_NATIVE"}, "failure_kind":"start","failure_message":"Ghidra target snapshot failed admission"}}} ``` With `TEMP=D:\Tmp TMP=D:\Tmp` the identical command completes and returns Ghidra 12.1.3 Evidence, so the provider, bridge, and toolchain are fine. ### Reproduction Smallest form, against the published 6.3.0 artifact: ```js const addon = require("./native/windows/build/rea-windows-x64.node"); const root = addon.call("runtime_create", ["D:\\", "rea-ghidra-"]); console.log(root.path); // "D:\\rea-ghidra-<hex>" <- two separators await addon.call("runtime_snapshot", [root.handle, "C:\\Windows\\System32\\where.exe", "probe.exe"]); // Ambiguous Windows path component is unsupported [D:\\rea-ghidra-<hex>\probe.exe] (Win32 50) ``` 1. `TEMP=D:\.Tmp rea analyze C:/Windows/System32/where.exe` → `execution_failure` (above). 2. `TEMP=D:\Tmp TMP=D:\Tmp rea analyze C:/Windows/System32/where.exe` → succeeds. 3. `node scripts/verify-windows-native.mjs` with the new drive-root control case fails on the 6.3.0 artifact and passes with the fix. ### Expected behavior A drive root is a legitimate private-runtime parent: `runtime_create(["D:\\", prefix])` should return `D:\<prefix><hex>` (exactly one separator), and `runtime_snapshot`, `runtime_open`, and cleanup should behave as they do for any other parent. ### Impact and scope - Affected: the Windows x64 native private runtime (`runtime_create`) whenever the parent path ends with its separator, i.e. a bare drive root; in practice the Ghidra provider on hosts whose temp directory path contains a dot-prefixed element (`ghidraSessionRoot()`'s dot-free fallback can return a drive root). - Not affected: POSIX, non-drive-root Windows parents, other providers that allocate their runtime under a normal directory. - Evidence integrity, process isolation, and cleanup ownership are unaffected; the failure is fail-closed path admission. No security impact observed. - Workaround: give REA a dot-free temp base (`TEMP`/`TMP`, e.g. `D:\Tmp`), or keep the inherited temp directory free of dot-prefixed elements. ### REA version `rea-agents` 6.3.0 (npm release) and repository main at `af7a4396` ### Runtime and platform Node.js 24.19.0; Windows 10.0.26200 x64; Ghidra 12.1.3 with JDK 21 (Zulu 21.0.12.1) ### Target and artifact context PE32+ x86_64 (`C:\Windows\System32\where.exe`); not applicable to the native-only reproduction. ### Interface CLI (the same private-runtime path is used by MCP tool calls) ### Earliest failure stage Setup or provider admission ### Relevant output or evidence Unfixed 6.3.0 artifact vs. a build of the proposed fix, same script: ``` parent "D:\\" -> root.path "D:\\\\rea-ghidra-723ce07d..." SNAPSHOT ERROR: Ambiguous Windows path component ... parent "D:\\Tmp" -> root.path "D:\\Tmp\\rea-ghidra-36eb3a40..." snapshot ok | digest matches: true # with the fix parent "D:\\" -> root.path "D:\\rea-ghidra-a626454f..." snapshot ok | digest matches: true parent "D:\\Tmp" -> root.path "D:\\Tmp\\rea-ghidra-b0f93a6b..." snapshot ok | digest matches: true ``` `node scripts/verify-windows-native.mjs` with the added `driveRootRuntimeParent` control: - before: `AssertionError ... true !== false` at the doubled-separator assertion. - after: `{"ok":true,"artifact":"verified","controls":{...,"driveRootRuntimeParent":true}}`. ### Typed outcome or recovery expected `execution_failure` is the right typed outcome for a genuine admission failure, but a drive-root parent should never fail admission; the runtime should be created and usable, as with any other parent. ### Analysis or suspected cause `native/windows/src/filesystem.cc:222` appends a separator unconditionally: ```cpp result->path = parent->path + L"\\" + prefix + hex(random.data(), random.size()); ``` while `localPath()` deliberately keeps the trailing separator of a 3-character drive root (`filesystem.cc:26`, `if (path.size() > 3 && path.back() == L'\\') path.pop_back();`). `parent->path` for `D:\` is therefore `D:\`, and the join yields `D:\\<prefix>…`; `relativePath()` then calls `localPath()` on `D:\\<prefix>…\relative`, whose component loop rejects the empty component (`filesystem.cc:32`). The Ghidra session-root fallback (`src/ghidra/GhidraSessionRoot.ts:54`, `nearestSafeAncestor`) is what makes this reachable from ordinary analysis, but it is not itself incorrect — a drive root is a valid parent that the native boundary must accept. ### Additional context Proposed fix (PR to follow): append the separator only when the parent does not already end with one, plus a `driveRootRuntimeParent` control in `scripts/verify-windows-native.mjs` and a `GhidraSessionRoot` contract test that pins the drive-root fallback. Related closed issue: #786.
bsakaguchi 2026-10-15 18:26:13 +00:00
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#565
No description provided.