[GH-ISSUE #7419] [Bug] binary_session reports debugger starts available on a host both reject #4425

Closed
opened 2026-10-31 00:49:41 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 27, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/7419

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Provider or capability discovery

Observed behavior

On macOS arm64 with no analysis target open, binary_session {} lists start_gdb_session and start_rizin_debug_session as available: true, reason: "available", with null remediation. In the same production MCP connection, each start returns capability_unavailable with a Linux-only host restriction. A second discovery call still reports both available. Discovery also groups their six existing-session operations with them; those six handlers were not invoked in this verification.

Reproduction

  1. Use the built baseline checkout with its locked dependencies installed and Node 24.22.1 on PATH. From its root, retain the checkout location, create an empty working directory, and enter it:
REA_CHECKOUT="$PWD"
work_dir="$(mktemp -d)"
cd "$work_dir"
  1. Save this standard-library MCP client. It creates isolated configuration directories inline and deliberately supplies a nonexistent relative target path; no debugger, executable fixture or actual TTY is needed to reach the host refusal:
cat > probe.py <<'PYTHON'
import json, os, pathlib, subprocess, sys
root = pathlib.Path(sys.argv[1]).resolve()
work = pathlib.Path.cwd()
for name in ('home','config','data','cache','state','tmp'):
    (work/name).mkdir(exist_ok=True)
env = {'PATH':os.environ['PATH'],'HOME':str(work/'home'),'XDG_CONFIG_HOME':str(work/'config'),'XDG_DATA_HOME':str(work/'data'),'XDG_CACHE_HOME':str(work/'cache'),'XDG_STATE_HOME':str(work/'state'),'TMPDIR':str(work/'tmp'),'REA_LOG_LEVEL':'silent'}
frames=[]
with (work/'stderr.log').open('w') as log:
 p=subprocess.Popen(['node',str(root/'scripts/rea.mjs'),'mcp'],cwd=work,env=env,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=log,text=True)
 def request(i,method,params):
    msg={'jsonrpc':'2.0','id':i,'method':method,'params':params}
    frames.append({'sent':msg});p.stdin.write(json.dumps(msg)+'\n');p.stdin.flush()
    for line in p.stdout:
        msg=json.loads(line);frames.append({'received':msg})
        if msg.get('id')==i:return msg
    raise RuntimeError('stdout closed')
 def call(i,name,args):
    r=request(i,'tools/call',{'name':name,'arguments':args})['result']
    b=r.get('structuredContent')
    if b is None:b=json.loads(next(x['text'] for x in r['content'] if x['type']=='text'))
    return b.get('result',b)
 try:
    request(1,'initialize',{'protocolVersion':'2025-06-18','capabilities':{},'clientInfo':{'name':'availability-verifier','version':'1'}})
    p.stdin.write(json.dumps({'jsonrpc':'2.0','method':'notifications/initialized'})+'\n');p.stdin.flush()
    for i,name,args in [(2,'binary_session',{}),(3,'start_gdb_session',{}),(4,'start_rizin_debug_session',{'path':'./absent.bin','inferior_tty':'/dev/null'}),(5,'binary_session',{})]:
      b=call(i,name,args)
      if name=='binary_session':
        names={'start_gdb_session','gdb_console','gdb_session_status','close_gdb_session','start_rizin_debug_session','rizin_debug_command','rizin_debug_session_status','close_rizin_debug_session'}
        print(json.dumps({'call':name,'open':b.get('open'),'groups':[{k:g[k] for k in ('available','reason','remediation')}|{'names':sorted(names.intersection(g['names']))} for g in b['tool_availability']['groups'] if names.intersection(g['names'])]}))
      else:print(json.dumps({'call':name,'body':b}))
 finally:
    p.stdin.close()
    try:p.wait(timeout=20)
    except subprocess.TimeoutExpired:p.terminate();p.wait(timeout=5)
    (work/'transcript.json').write_text(json.dumps(frames,indent=2))
    print('server_exit',p.returncode)
PYTHON
python3 probe.py "$REA_CHECKOUT"
  1. Compare discovery before and after the two start calls. The exact relevant output from the final reproduction is below. Both completed isolated sessions reproduced the mismatch (2 failures in 2 attempts); both servers exited 0:
{"call": "binary_session", "open": false, "groups": [{"available": true, "reason": "available", "remediation": null, "names": ["close_gdb_session", "close_rizin_debug_session", "gdb_console", "gdb_session_status", "rizin_debug_command", "rizin_debug_session_status", "start_gdb_session", "start_rizin_debug_session"]}]}
{"call": "start_gdb_session", "body": {"error": {"code": "capability_unavailable", "category": "unsupported_provider", "message": "This analysis is unavailable for the current target. Choose another analysis or target.", "retryable": false, "remediation": {"action": "This analysis is unavailable for the current target. Choose another analysis or target."}, "details": {"provider_id": "gdb", "operation": "start_gdb_session", "reason": "Persistent GDB sessions are available only on Linux because REA verifies live inferior output descriptors through /proc."}}}}
{"call": "start_rizin_debug_session", "body": {"error": {"code": "capability_unavailable", "category": "unsupported_provider", "message": "This analysis is unavailable for the current target. Choose another analysis or target.", "retryable": false, "remediation": {"action": "This analysis is unavailable for the current target. Choose another analysis or target."}, "details": {"provider_id": "rizin", "operation": "start_rizin_debug_session", "reason": "Persistent Rizin debugger sessions require Linux /proc descriptor readback to verify inferior detachment and output routing."}}}}
{"call": "binary_session", "open": false, "groups": [{"available": true, "reason": "available", "remediation": null, "names": ["close_gdb_session", "close_rizin_debug_session", "gdb_console", "gdb_session_status", "rizin_debug_command", "rizin_debug_session_status", "start_gdb_session", "start_rizin_debug_session"]}]}
server_exit 0

Expected behavior

The Identity and discovery section of docs/mcp-contracts.md says: "Call binary_session with {} and read result.tool_availability to choose a callable operation for the current target, provider, host, and negotiated client capabilities." That section also explains that GDB and Rizin debugger tools use independent session/input owners, so an unrelated open_binary is unnecessary. The GNU GDB and Rizin sections of docs/reverse-engineering-tools.md restrict persistent debugger sessions to Linux. Discovery should therefore identify these start operations as unavailable on this macOS host, with unsupported_host and the Linux requirement, while preserving their canonical tool definitions and independence from the analysis target.

Impact and scope

In each of two tested macOS sessions, a caller relying on the documented availability field would select starts that immediately fail because of a permanent host restriction; the incorrect signal persists after those refusals. The starts refuse execution before creating a debugger or inferior. A practical workaround is to read the documented Linux requirement or the returned details.reason and exclude these starts on this host. No downstream agent behavior, provider installation, successful Linux execution, Windows execution, data loss or security boundary crossing was tested or observed.

REA version

6.8.0 (source commit a905a71e31)

Runtime and platform

Node 24.22.1; macOS 27.0.1 arm64; production stdio MCP with isolated HOME/XDG/TMPDIR.

Target and artifact context

No active analysis target or real debugger session. The relative ./absent.bin input does not exist; /dev/null supplies the required TTY argument solely to reach the earlier host guard.

Interface

MCP

Earliest failure stage

Transport or result projection

Relevant output or evidence

Observed: the executable reproduction above records full JSON-RPC requests and responses in transcript.json and prints the availability facts and exact start refusals. The first and last discovery responses have open: false and the same available: true debugger group. Both refusal bodies identify their actual Linux requirement in error.details.reason.

Contract locators: docs/mcp-contracts.md:614-623 describes host-aware operation selection; docs/mcp-contracts.md:664-680 describes independent session/input ownership; docs/reverse-engineering-tools.md:43 and docs/reverse-engineering-tools.md:76 state the Linux restrictions. Reachable public registrations are src/server/registerGdbTools.ts:38 and src/server/registerRizinDebugTools.ts:31.

Typed outcome or recovery expected

For this macOS connection, both start entries should report available: false, reason: "unsupported_host", and host-specific remediation before and after the refused calls. This does not require opening an analysis target, removing the tools from tools/list, or changing the execution guards. A Linux availability control remains a proposed check, not an executed result.

Analysis or suspected cause

Source-only (not executed as a separate test): src/application/CapabilityInventory.ts:794-809 falls back to available for native-provider contracts with no matching descriptor. These debugger contracts use that kind, while src/server/createServer.ts:301-339 supplies host restrictions only for named optional integrations and omits both debugger managers. The actual start owners reject non-Linux hosts at src/gdb/GdbSessionManager.ts:655-665 and src/rizin/RizinDebugSessionManager.ts:364-372. Their permanent host prerequisite therefore never reaches the discovery projection.

Source-only history: merged PR #5793 added unsupported-host discovery for packet, signal, firmware, pwntools, EVM and Wakaru integrations in commit 5a311d8af; its changed set excludes debugger sessions. Later commit f3bb6b1ed correctly removed the blanket native-provider active-target prerequisite for independent session tools, exposing this fallback without adding their host constraint. Both commits are ancestors of this baseline. This explains why the earlier fixes can pass while both starts remain falsely available here.

Additional context

Related (closed):
https://github.com/morluto/rea/issues/5730
https://github.com/morluto/rea/issues/5711
https://github.com/morluto/rea/issues/5447
Their fixes cover named optional integrations, pcap capture, and removal of a false analysis-target prerequisite respectively; the baseline still reproduces the independent debugger-start host mismatch above. The open provider-integration design discussion is a separate scope decision, not an exact duplicate of these discovery values. No patch or successful restoration was tested.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7419 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Provider or capability discovery ### Observed behavior On macOS arm64 with no analysis target open, `binary_session {}` lists `start_gdb_session` and `start_rizin_debug_session` as `available: true`, `reason: "available"`, with null remediation. In the same production MCP connection, each start returns `capability_unavailable` with a Linux-only host restriction. A second discovery call still reports both available. Discovery also groups their six existing-session operations with them; those six handlers were not invoked in this verification. ### Reproduction 1. Use the built baseline checkout with its locked dependencies installed and Node 24.22.1 on PATH. From its root, retain the checkout location, create an empty working directory, and enter it: ```sh REA_CHECKOUT="$PWD" work_dir="$(mktemp -d)" cd "$work_dir" ``` 2. Save this standard-library MCP client. It creates isolated configuration directories inline and deliberately supplies a nonexistent relative target path; no debugger, executable fixture or actual TTY is needed to reach the host refusal: ```sh cat > probe.py <<'PYTHON' import json, os, pathlib, subprocess, sys root = pathlib.Path(sys.argv[1]).resolve() work = pathlib.Path.cwd() for name in ('home','config','data','cache','state','tmp'): (work/name).mkdir(exist_ok=True) env = {'PATH':os.environ['PATH'],'HOME':str(work/'home'),'XDG_CONFIG_HOME':str(work/'config'),'XDG_DATA_HOME':str(work/'data'),'XDG_CACHE_HOME':str(work/'cache'),'XDG_STATE_HOME':str(work/'state'),'TMPDIR':str(work/'tmp'),'REA_LOG_LEVEL':'silent'} frames=[] with (work/'stderr.log').open('w') as log: p=subprocess.Popen(['node',str(root/'scripts/rea.mjs'),'mcp'],cwd=work,env=env,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=log,text=True) def request(i,method,params): msg={'jsonrpc':'2.0','id':i,'method':method,'params':params} frames.append({'sent':msg});p.stdin.write(json.dumps(msg)+'\n');p.stdin.flush() for line in p.stdout: msg=json.loads(line);frames.append({'received':msg}) if msg.get('id')==i:return msg raise RuntimeError('stdout closed') def call(i,name,args): r=request(i,'tools/call',{'name':name,'arguments':args})['result'] b=r.get('structuredContent') if b is None:b=json.loads(next(x['text'] for x in r['content'] if x['type']=='text')) return b.get('result',b) try: request(1,'initialize',{'protocolVersion':'2025-06-18','capabilities':{},'clientInfo':{'name':'availability-verifier','version':'1'}}) p.stdin.write(json.dumps({'jsonrpc':'2.0','method':'notifications/initialized'})+'\n');p.stdin.flush() for i,name,args in [(2,'binary_session',{}),(3,'start_gdb_session',{}),(4,'start_rizin_debug_session',{'path':'./absent.bin','inferior_tty':'/dev/null'}),(5,'binary_session',{})]: b=call(i,name,args) if name=='binary_session': names={'start_gdb_session','gdb_console','gdb_session_status','close_gdb_session','start_rizin_debug_session','rizin_debug_command','rizin_debug_session_status','close_rizin_debug_session'} print(json.dumps({'call':name,'open':b.get('open'),'groups':[{k:g[k] for k in ('available','reason','remediation')}|{'names':sorted(names.intersection(g['names']))} for g in b['tool_availability']['groups'] if names.intersection(g['names'])]})) else:print(json.dumps({'call':name,'body':b})) finally: p.stdin.close() try:p.wait(timeout=20) except subprocess.TimeoutExpired:p.terminate();p.wait(timeout=5) (work/'transcript.json').write_text(json.dumps(frames,indent=2)) print('server_exit',p.returncode) PYTHON python3 probe.py "$REA_CHECKOUT" ``` 3. Compare discovery before and after the two start calls. The exact relevant output from the final reproduction is below. Both completed isolated sessions reproduced the mismatch (2 failures in 2 attempts); both servers exited 0: ```text {"call": "binary_session", "open": false, "groups": [{"available": true, "reason": "available", "remediation": null, "names": ["close_gdb_session", "close_rizin_debug_session", "gdb_console", "gdb_session_status", "rizin_debug_command", "rizin_debug_session_status", "start_gdb_session", "start_rizin_debug_session"]}]} {"call": "start_gdb_session", "body": {"error": {"code": "capability_unavailable", "category": "unsupported_provider", "message": "This analysis is unavailable for the current target. Choose another analysis or target.", "retryable": false, "remediation": {"action": "This analysis is unavailable for the current target. Choose another analysis or target."}, "details": {"provider_id": "gdb", "operation": "start_gdb_session", "reason": "Persistent GDB sessions are available only on Linux because REA verifies live inferior output descriptors through /proc."}}}} {"call": "start_rizin_debug_session", "body": {"error": {"code": "capability_unavailable", "category": "unsupported_provider", "message": "This analysis is unavailable for the current target. Choose another analysis or target.", "retryable": false, "remediation": {"action": "This analysis is unavailable for the current target. Choose another analysis or target."}, "details": {"provider_id": "rizin", "operation": "start_rizin_debug_session", "reason": "Persistent Rizin debugger sessions require Linux /proc descriptor readback to verify inferior detachment and output routing."}}}} {"call": "binary_session", "open": false, "groups": [{"available": true, "reason": "available", "remediation": null, "names": ["close_gdb_session", "close_rizin_debug_session", "gdb_console", "gdb_session_status", "rizin_debug_command", "rizin_debug_session_status", "start_gdb_session", "start_rizin_debug_session"]}]} server_exit 0 ``` ### Expected behavior The Identity and discovery section of `docs/mcp-contracts.md` says: "Call `binary_session` with `{}` and read `result.tool_availability` to choose a callable operation for the current target, provider, host, and negotiated client capabilities." That section also explains that GDB and Rizin debugger tools use independent session/input owners, so an unrelated `open_binary` is unnecessary. The GNU GDB and Rizin sections of `docs/reverse-engineering-tools.md` restrict persistent debugger sessions to Linux. Discovery should therefore identify these start operations as unavailable on this macOS host, with `unsupported_host` and the Linux requirement, while preserving their canonical tool definitions and independence from the analysis target. ### Impact and scope In each of two tested macOS sessions, a caller relying on the documented availability field would select starts that immediately fail because of a permanent host restriction; the incorrect signal persists after those refusals. The starts refuse execution before creating a debugger or inferior. A practical workaround is to read the documented Linux requirement or the returned `details.reason` and exclude these starts on this host. No downstream agent behavior, provider installation, successful Linux execution, Windows execution, data loss or security boundary crossing was tested or observed. ### REA version 6.8.0 (source commit a905a71e31f71457cb21fd056b234f872126974e) ### Runtime and platform Node 24.22.1; macOS 27.0.1 arm64; production stdio MCP with isolated HOME/XDG/TMPDIR. ### Target and artifact context No active analysis target or real debugger session. The relative `./absent.bin` input does not exist; `/dev/null` supplies the required TTY argument solely to reach the earlier host guard. ### Interface MCP ### Earliest failure stage Transport or result projection ### Relevant output or evidence Observed: the executable reproduction above records full JSON-RPC requests and responses in `transcript.json` and prints the availability facts and exact start refusals. The first and last discovery responses have `open: false` and the same `available: true` debugger group. Both refusal bodies identify their actual Linux requirement in `error.details.reason`. Contract locators: `docs/mcp-contracts.md:614-623` describes host-aware operation selection; `docs/mcp-contracts.md:664-680` describes independent session/input ownership; `docs/reverse-engineering-tools.md:43` and `docs/reverse-engineering-tools.md:76` state the Linux restrictions. Reachable public registrations are `src/server/registerGdbTools.ts:38` and `src/server/registerRizinDebugTools.ts:31`. ### Typed outcome or recovery expected For this macOS connection, both start entries should report `available: false`, `reason: "unsupported_host"`, and host-specific remediation before and after the refused calls. This does not require opening an analysis target, removing the tools from `tools/list`, or changing the execution guards. A Linux availability control remains a proposed check, not an executed result. ### Analysis or suspected cause Source-only (not executed as a separate test): `src/application/CapabilityInventory.ts:794-809` falls back to available for native-provider contracts with no matching descriptor. These debugger contracts use that kind, while `src/server/createServer.ts:301-339` supplies host restrictions only for named optional integrations and omits both debugger managers. The actual start owners reject non-Linux hosts at `src/gdb/GdbSessionManager.ts:655-665` and `src/rizin/RizinDebugSessionManager.ts:364-372`. Their permanent host prerequisite therefore never reaches the discovery projection. Source-only history: merged PR #5793 added unsupported-host discovery for packet, signal, firmware, pwntools, EVM and Wakaru integrations in commit `5a311d8af`; its changed set excludes debugger sessions. Later commit `f3bb6b1ed` correctly removed the blanket native-provider active-target prerequisite for independent session tools, exposing this fallback without adding their host constraint. Both commits are ancestors of this baseline. This explains why the earlier fixes can pass while both starts remain falsely available here. ### Additional context Related (closed): https://github.com/morluto/rea/issues/5730 https://github.com/morluto/rea/issues/5711 https://github.com/morluto/rea/issues/5447 Their fixes cover named optional integrations, pcap capture, and removal of a false analysis-target prerequisite respectively; the baseline still reproduces the independent debugger-start host mismatch above. The open provider-integration design discussion is a separate scope decision, not an exact duplicate of these discovery values. No patch or successful restoration was tested.
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#4425
No description provided.