[GH-ISSUE #6470] [Bug] decompile forwards --file-offset to IDA as a procedure name #3881

Closed
opened 2026-10-31 00:44:53 +00:00 by bsakaguchi · 0 comments
Owner

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

Before submitting

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

Area

CLI

Observed behavior

node scripts/rea.mjs decompile "$WORK/fixture.bin" --file-offset=0x10 --provider ida --json forwards file-offset:0x10 to the legacy IDA profile's get_function_by_name, although the option specifies source-file coordinates.

With a local stdio simulator exposing only main, the command exits 1 with unreadable_output and Not found. When the simulator exposes a synthetic function named file-offset:0x10, the identical command exits 0 and returns its pseudocode without any file-offset mapping. The normal --procedure=main control succeeds.

Reproduction

From a fresh checkout at the revision below, prepare the locked runtime, then run this self-contained fixture. Python 3 and Node.js are required. The simulator does not start IDA, execute the target, or contact a remote service.

git checkout 9eb829aca34be36ff112826337996f6139f82eff
npm ci && npm run build:cached
export WORK="$(mktemp -d)"
python3 <<'PY'
import os, json, hashlib, struct, subprocess
from pathlib import Path
work = Path(os.environ['WORK'])
(work / 'home').mkdir(parents=True, exist_ok=True)
(work / 'stub.mjs').write_text(r"""import {createInterface} from 'node:readline';
import {appendFileSync} from 'node:fs';
const names=['get_metadata','list_functions','list_strings','get_function_by_name','get_function_by_address','decompile_function','disassemble_function','get_callers','get_callees','get_xrefs_to'];
const fn={address:'0x100000f00',name:process.env.SYMBOL,size:'0x20'};
createInterface({input:process.stdin}).on('line',line=>{
 const r=JSON.parse(line);appendFileSync(process.env.LOG,line+'\n');if(r.id===undefined)return;
 const reply=result=>process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:r.id,result})+'\n');
 const val=x=>reply({content:[{type:'text',text:JSON.stringify(x)}]});
 if(r.method==='initialize')return reply({protocolVersion:r.params.protocolVersion,capabilities:{tools:{}},serverInfo:{name:'local-fixture',version:'1.4.0'}});
 if(r.method==='tools/list')return reply({tools:names.map(name=>({name,inputSchema:{type:'object'}}))});
 const {name,arguments:a={}}=r.params;
 if(name==='get_metadata')return val({path:process.env.TARGET_PATH,module:'fixture',sha256:process.env.TARGET_SHA,base:'0x0'});
 if(name==='get_function_by_name'||name==='get_function_by_address'){
  const q=name==='get_function_by_name'?a.name:a.address;
  if(q===fn.name||q===fn.address)return val(fn);
  return reply({isError:true,content:[{type:'text',text:'Not found'}]});
 }
 if(name==='decompile_function')return val('int selected_by_name(void) { return 7; }');
 return val([]);
});
""")
image = bytearray(64)
struct.pack_into('<IIIIIIII', image, 0, 0xfeedfacf, 0x0100000c, 0, 2, 0, 0, 0, 0)
(work / 'fixture.bin').write_bytes(image)
env = {k: v for k, v in os.environ.items()
       if not k.startswith(('REA_', 'HOPPER_', 'GHIDRA_'))}
env['HOME'] = str(work / 'home')
for case, symbol, selector in [
    ('missing', 'main', '--file-offset=0x10'),
    ('collision', 'file-offset:0x10', '--file-offset=0x10'),
    ('control', 'main', '--procedure=main'),
    ('repeat', 'file-offset:0x10', '--file-offset=0x10'),
]:
    registration = {
        'command': 'node', 'args': [str(work / 'stub.mjs')],
        'env': {'TARGET_PATH': str(work / 'fixture.bin'),
                'TARGET_SHA': hashlib.sha256(image).hexdigest(),
                'SYMBOL': symbol, 'LOG': str(work / (case + '.requests.jsonl'))}
    }
    config = work / 'registration.json'
    config.write_text(json.dumps(registration))
    env['REA_IDA_MCP_CONFIG'] = str(config)
    result = subprocess.run([
        'node', 'scripts/rea.mjs', 'decompile', str(work / 'fixture.bin'),
        selector, '--provider', 'ida', '--json'
    ], env=env, capture_output=True, text=True, timeout=45)
    (work / (case + '.stdout.json')).write_text(result.stdout)
    outcome = json.loads(result.stdout)
    calls = [json.loads(line) for line in
             (work / (case + '.requests.jsonl')).read_text().splitlines()]
    print(case, 'exit', result.returncode,
          {k: outcome[k] for k in ('code', 'normalized_result', 'parameters')
           if k in outcome})
    print([(r['params']['name'], r['params'].get('arguments'))
           for r in calls if r['method'] == 'tools/call'])
PY

Observed frequency across two executions of the four-case fixture: missing-symbol failure 2/2, synthetic matching-symbol success 4/4, and normal procedure-name control success 2/2. Existing dependencies and built runtime were used for the observed runs; the fresh-checkout installation/build commands above are preparation instructions.

Expected behavior

Preserve the caller's explicit file-coordinate intent. If the selected provider does not support source-file offset selection, reject the combination with an actionable input or capability error instead of interpreting the offset selector as a procedure name.

The option contract calls this a Ghidra source-file byte offset mapped internally to an analysis address. The provider guide distinguishes explicit file coordinates from procedure names and analysis addresses. No new IDA mapping capability is required to reject this unsupported combination honestly.

Impact and scope

P2: callers combining --file-offset with IDA receive a misleading producer-failure result; the synthetic collision demonstrates successful pseudocode projection using name semantics instead of file-coordinate semantics. A known procedure name is a demonstrated workaround. Ghidra is the documented provider for file-offset decompilation, but was not run here.

The reproduction covers public CLI routing through the legacy attached IDA adapter with simulated responses. Real IDA acceptance of the synthetic symbol spelling, natural failure frequency, other IDA profiles, Hopper and direct MCP calls were not tested. No security boundary crossing, target mutation or data loss was observed.

REA version

6.7.0 (source 9eb829aca3)

Runtime and platform

Node.js v26.12.1; macOS 27.0.1; arm64. Legacy IDA compatibility profile simulated over local stdio; no real IDA engine version was exercised.

Target and artifact context

An inert 64-byte arm64 thin Mach-O header admitted by REA, with matching metadata SHA-256 supplied by the simulator. This is an adapter-routing fixture, not a complete executable or a real decompilation test.

Interface

CLI

Earliest failure stage

Request parsing or validation

Relevant output or evidence

Observed summaries:

missing: exit 1, code=unreadable_output, category=execution_failure
         details.diagnostics.reason="Not found"
         get_function_by_name({name:"file-offset:0x10"})
collision and repeat: exit 0
         get_function_by_name({name:"file-offset:0x10"})
         decompile_function({address:"0x100000f00"})
         normalized_result="int selected_by_name(void) { return 7; }"
control: exit 0
         get_function_by_name({name:"main"})
         normalized_result="int selected_by_name(void) { return 7; }"

The success envelope also retains parameters.procedure="file-offset:0x10". The simulator's request logs and the envelope's raw observations establish which lookup was used; they do not establish real-engine behavior.

Typed outcome or recovery expected

An input-validation or capability-unavailable result identifying --file-offset and the selected provider's limitation, before sending that selector to name lookup. The exact error code is a proposed outcome; the documented contract establishes the coordinate interpretation.

Analysis or suspected cause

Source-derived: the CLI handler constructs the file-offset: string independently of provider selection. The legacy resolver chooses address lookup only for a 0x-prefixed address and otherwise uses the complete string as a name. The CLI trace confirms this route is reachable.

Additional context

Related but different: #2078 concerns Ghidra rebasing and file-offset support; #1769 concerns modern headless IDA missing-name error classification; #6467 concerns schemas omitting required-selector relationships. None addresses forwarding explicit file coordinates as names. No last known good revision is established.

Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6470 ### 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 CLI ### Observed behavior `node scripts/rea.mjs decompile "$WORK/fixture.bin" --file-offset=0x10 --provider ida --json` forwards `file-offset:0x10` to the legacy IDA profile's `get_function_by_name`, although the option specifies source-file coordinates. With a local stdio simulator exposing only `main`, the command exits 1 with `unreadable_output` and `Not found`. When the simulator exposes a synthetic function named `file-offset:0x10`, the identical command exits 0 and returns its pseudocode without any file-offset mapping. The normal `--procedure=main` control succeeds. ### Reproduction From a fresh checkout at the revision below, prepare the locked runtime, then run this self-contained fixture. Python 3 and Node.js are required. The simulator does not start IDA, execute the target, or contact a remote service. ```sh git checkout 9eb829aca34be36ff112826337996f6139f82eff npm ci && npm run build:cached export WORK="$(mktemp -d)" python3 <<'PY' import os, json, hashlib, struct, subprocess from pathlib import Path work = Path(os.environ['WORK']) (work / 'home').mkdir(parents=True, exist_ok=True) (work / 'stub.mjs').write_text(r"""import {createInterface} from 'node:readline'; import {appendFileSync} from 'node:fs'; const names=['get_metadata','list_functions','list_strings','get_function_by_name','get_function_by_address','decompile_function','disassemble_function','get_callers','get_callees','get_xrefs_to']; const fn={address:'0x100000f00',name:process.env.SYMBOL,size:'0x20'}; createInterface({input:process.stdin}).on('line',line=>{ const r=JSON.parse(line);appendFileSync(process.env.LOG,line+'\n');if(r.id===undefined)return; const reply=result=>process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:r.id,result})+'\n'); const val=x=>reply({content:[{type:'text',text:JSON.stringify(x)}]}); if(r.method==='initialize')return reply({protocolVersion:r.params.protocolVersion,capabilities:{tools:{}},serverInfo:{name:'local-fixture',version:'1.4.0'}}); if(r.method==='tools/list')return reply({tools:names.map(name=>({name,inputSchema:{type:'object'}}))}); const {name,arguments:a={}}=r.params; if(name==='get_metadata')return val({path:process.env.TARGET_PATH,module:'fixture',sha256:process.env.TARGET_SHA,base:'0x0'}); if(name==='get_function_by_name'||name==='get_function_by_address'){ const q=name==='get_function_by_name'?a.name:a.address; if(q===fn.name||q===fn.address)return val(fn); return reply({isError:true,content:[{type:'text',text:'Not found'}]}); } if(name==='decompile_function')return val('int selected_by_name(void) { return 7; }'); return val([]); }); """) image = bytearray(64) struct.pack_into('<IIIIIIII', image, 0, 0xfeedfacf, 0x0100000c, 0, 2, 0, 0, 0, 0) (work / 'fixture.bin').write_bytes(image) env = {k: v for k, v in os.environ.items() if not k.startswith(('REA_', 'HOPPER_', 'GHIDRA_'))} env['HOME'] = str(work / 'home') for case, symbol, selector in [ ('missing', 'main', '--file-offset=0x10'), ('collision', 'file-offset:0x10', '--file-offset=0x10'), ('control', 'main', '--procedure=main'), ('repeat', 'file-offset:0x10', '--file-offset=0x10'), ]: registration = { 'command': 'node', 'args': [str(work / 'stub.mjs')], 'env': {'TARGET_PATH': str(work / 'fixture.bin'), 'TARGET_SHA': hashlib.sha256(image).hexdigest(), 'SYMBOL': symbol, 'LOG': str(work / (case + '.requests.jsonl'))} } config = work / 'registration.json' config.write_text(json.dumps(registration)) env['REA_IDA_MCP_CONFIG'] = str(config) result = subprocess.run([ 'node', 'scripts/rea.mjs', 'decompile', str(work / 'fixture.bin'), selector, '--provider', 'ida', '--json' ], env=env, capture_output=True, text=True, timeout=45) (work / (case + '.stdout.json')).write_text(result.stdout) outcome = json.loads(result.stdout) calls = [json.loads(line) for line in (work / (case + '.requests.jsonl')).read_text().splitlines()] print(case, 'exit', result.returncode, {k: outcome[k] for k in ('code', 'normalized_result', 'parameters') if k in outcome}) print([(r['params']['name'], r['params'].get('arguments')) for r in calls if r['method'] == 'tools/call']) PY ``` Observed frequency across two executions of the four-case fixture: missing-symbol failure 2/2, synthetic matching-symbol success 4/4, and normal procedure-name control success 2/2. Existing dependencies and built runtime were used for the observed runs; the fresh-checkout installation/build commands above are preparation instructions. ### Expected behavior Preserve the caller's explicit file-coordinate intent. If the selected provider does not support source-file offset selection, reject the combination with an actionable input or capability error instead of interpreting the offset selector as a procedure name. The [option contract](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/cli/coreBinaryCommands.ts#L175-L180) calls this a Ghidra source-file byte offset mapped internally to an analysis address. The [provider guide](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/docs/ghidra-provider.md#L84-L92) distinguishes explicit file coordinates from procedure names and analysis addresses. No new IDA mapping capability is required to reject this unsupported combination honestly. ### Impact and scope P2: callers combining `--file-offset` with IDA receive a misleading producer-failure result; the synthetic collision demonstrates successful pseudocode projection using name semantics instead of file-coordinate semantics. A known procedure name is a demonstrated workaround. Ghidra is the documented provider for file-offset decompilation, but was not run here. The reproduction covers public CLI routing through the legacy attached IDA adapter with simulated responses. Real IDA acceptance of the synthetic symbol spelling, natural failure frequency, other IDA profiles, Hopper and direct MCP calls were not tested. No security boundary crossing, target mutation or data loss was observed. ### REA version 6.7.0 (source 9eb829aca34be36ff112826337996f6139f82eff) ### Runtime and platform Node.js v26.12.1; macOS 27.0.1; arm64. Legacy IDA compatibility profile simulated over local stdio; no real IDA engine version was exercised. ### Target and artifact context An inert 64-byte arm64 thin Mach-O header admitted by REA, with matching metadata SHA-256 supplied by the simulator. This is an adapter-routing fixture, not a complete executable or a real decompilation test. ### Interface CLI ### Earliest failure stage Request parsing or validation ### Relevant output or evidence Observed summaries: ```text missing: exit 1, code=unreadable_output, category=execution_failure details.diagnostics.reason="Not found" get_function_by_name({name:"file-offset:0x10"}) collision and repeat: exit 0 get_function_by_name({name:"file-offset:0x10"}) decompile_function({address:"0x100000f00"}) normalized_result="int selected_by_name(void) { return 7; }" control: exit 0 get_function_by_name({name:"main"}) normalized_result="int selected_by_name(void) { return 7; }" ``` The success envelope also retains `parameters.procedure="file-offset:0x10"`. The simulator's request logs and the envelope's raw observations establish which lookup was used; they do not establish real-engine behavior. ### Typed outcome or recovery expected An input-validation or capability-unavailable result identifying `--file-offset` and the selected provider's limitation, before sending that selector to name lookup. The exact error code is a proposed outcome; the documented contract establishes the coordinate interpretation. ### Analysis or suspected cause Source-derived: the [CLI handler](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/cli/coreBinaryCommands.ts#L185-L207) constructs the `file-offset:` string independently of provider selection. The [legacy resolver](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/ida/IdaOperationRunner.ts#L168-L193) chooses address lookup only for a `0x`-prefixed address and otherwise uses the complete string as a name. The CLI trace confirms this route is reachable. ### Additional context Related but different: #2078 concerns Ghidra rebasing and file-offset support; #1769 concerns modern headless IDA missing-name error classification; #6467 concerns schemas omitting required-selector relationships. None addresses forwarding explicit file coordinates as names. No last known good revision is established.
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#3881
No description provided.