[GH-ISSUE #3922] [Bug] Node-limited UI traces misclassify omitted provider and indirect calls #1528

Closed
opened 2026-10-16 21:06:43 +00:00 by bsakaguchi · 2 comments
Owner

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

Before submitting

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

Area

Native analysis or decompilation

Observed behavior

The trace_native_ui_action MCP tool returns an omitted callee as relation: "direct_call" when its node budget fills, even when the provider supplied an untyped callee or a typed indirect call. The retained sibling edge has the correct relation. The omitted edge remains unresolved and its evidence description retains the original classification, but callers reading the structured relation receive conflicting information.

The registered MCP reproduction used a simulated provider. Real native providers were not exercised.

Reproduction

  1. Use the baseline checkout with its dependencies installed and runtime built. Run the following from the repository root. This is the executed reproduction, presented inline so it needs no external fixture. It isolates HOME and closes both MCP endpoints.
repro_home=$(mktemp -d)
HOME="$repro_home" node --input-type=module <<'JS'
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { createRequire } from 'node:module';
const require = createRequire(resolve('package.json'));
const load = path => import(pathToFileURL(resolve(path)));
const { Client, InMemoryTransport } = await import(pathToFileURL(require.resolve('@modelcontextprotocol/client')));
const { createServer } = await load('dist/server/createServer.js');
const { createAnalysisExecution } = await load('dist/application/AnalysisProvider.js');
const { AnalysisCapabilityUnavailableError } = await load('dist/domain/analysisErrorCore.js');
const { ok, err } = await load('dist/domain/result.js');
const provider = { id: 'fixture', name: 'Fixture', version: '1' };
const observed = result => ok(createAnalysisExecution(result, provider, {
  subject: { sha256: 'a'.repeat(64), path: 'fixture.bin', format: 'mach-o', architecture: 'arm64' }
}));
for (const max_nodes of [2, 3]) {
  const analysis = { execute: async (operation, parameters) => {
    if (['decode_interface_builder', 'inspect_native_dispatch_metadata', 'procedure_references'].includes(operation))
      return err(new AnalysisCapabilityUnavailableError('fixture', operation, 'Unavailable in fixture'));
    if (operation === 'list_names') return observed([]);
    if (operation === 'procedure_address') return observed('0x1000');
    if (operation === 'procedure_callees') return observed(parameters.procedure === '0x1000' ? ['0x2000', '0x3000'] : []);
    throw new Error(`Unexpected operation: ${operation}`);
  }};
  const server = createServer({ kind: 'fixed', analysis }, { environment: {} });
  const client = new Client({ name: 'call-relation-reproduction', version: '1' });
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
  try {
    await server.connect(serverTransport);
    await client.connect(clientTransport);
    const result = await client.callTool({ name: 'trace_native_ui_action', arguments: { action: '0x1000', max_nodes } });
    if (result.isError) throw new Error(JSON.stringify(result));
    const trace = result.structuredContent.normalized_result;
    console.log(JSON.stringify({ max_nodes,
      edges: trace.edges.map(({ to, relation }) => ({ to, relation })),
      unresolved: trace.unresolved.map(({ relation, reason, evidence }) => ({ relation, reason, description: evidence[0]?.description }))
    }));
  } finally {
    await client.close();
    await server.close();
  }
}
JS
rm -rf "$repro_home"
  1. Compare the max_nodes: 2 result with the max_nodes: 3 control. Both requests call trace_native_ui_action with action: "0x1000". The provider resolves that seed and supplies callees 0x2000 and 0x3000, with typed reference reading unavailable. All successful observations share one target SHA-256 identity. The unavailable UI decoder and binary metadata reader are supported fallbacks; the empty name inventory still carries the target identity.

Expected behavior

Bounded traces must preserve the distinction between direct references, resolved indirect references, inferred untyped provider callees, ambiguous candidates, and targetless sites. An omitted node can make an edge unresolved without changing its known relation. For this reproduction, both provider-reported routes must retain provider_call; the omission may retain its null destination and node-bound reason.

src/contracts/enhancedToolContracts.ts:80-82 and docs/native-investigation.md:210-217 require these call forms to remain distinct in bounded traces.

Impact and scope

P2. Valid bounded traces return an incorrect structured relation for an omitted untyped provider callee or typed indirect call. The edge remains unresolved, truncation is explicit, and its evidence description retains the originating classification. Real-provider frequency and severe harm were not established. Increasing the node budget from 2 to 3 restores both relation labels in this small fixture; this does not establish complete traversal or a general workaround for larger graphs.

REA version

rea 6.5.0 at source commit 4692bb3330

Runtime and platform

Node 24.18.0; macOS arm64

Target and artifact context

Simulated Mach-O arm64 subject fixture.bin with one consistent SHA-256 identity; no target execution or native engine required.

Interface

MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The compiled MCP reproduction exited successfully and returned these results:

{"max_nodes":2,"edges":[{"to":"native:function:0x2000","relation":"provider_call"}],"unresolved":[{"relation":"direct_call","reason":"native_trace_node_bound_reached","description":"Fixture reported provider_call from 0x1000 to 0x3000"}]}
{"max_nodes":3,"edges":[{"to":"native:function:0x2000","relation":"provider_call"},{"to":"native:function:0x3000","relation":"provider_call"}],"unresolved":[]}

A separate focused MCP integration test reproduced the same failure for typed indirect references. It supplied two available call references at separate sites, each with computed: true, indirect: true, and a unique target procedure. At node budget 2, the retained edge was indirect_call with resolved classification, while the omitted edge was direct_call and unresolved. The omitted edge's evidence description still identified an indirect call.

The focused run executed five cases. The provider and indirect budget-2 relation assertions failed, both budget-3 controls passed, and a genuine direct-call budget-2 control passed. The budget-3 controls establish restored relation labels for this fixture; they do not establish complete traversal. No real-provider occurrence rate was measured.

Typed outcome or recovery expected

Rerun the inline reproduction and require the budget-2 omitted edge to retain provider_call, while keeping its unresolved status, null destination, omission reason, provider evidence, and node budget. The retained sibling and budget-3 control must continue to report provider_call. With the typed computed/indirect input described above, require the omitted relation to remain indirect_call; a genuine direct-call control must remain direct_call. Preserve target-identity checks, explicit truncation, and uncertainty about runtime reachability.

Analysis or suspected cause

Source-derived: src/application/native/NativeUiActionTrace.ts:506-520 assigns direct_call in the node-bound omission branch, while lines 534-540 preserve the route relation for retained edges. src/application/native/NativeCallRoutes.ts:46-58,146-156 distinguishes typed indirect routes from inferred provider routes.

Additional context

The failing code is unchanged on current origin/main at 22ec8bdb2f7d4626587f23b1eed4a207df600386. Related #3343 and PR #3828 concern the separate Objective-C receiver-kind join; closed #2565 concerns retention of targetless sites. None changes the omitted-edge relation assignment.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3922 ### 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 Native analysis or decompilation ### Observed behavior The `trace_native_ui_action` MCP tool returns an omitted callee as `relation: "direct_call"` when its node budget fills, even when the provider supplied an untyped callee or a typed indirect call. The retained sibling edge has the correct relation. The omitted edge remains unresolved and its evidence description retains the original classification, but callers reading the structured relation receive conflicting information. The registered MCP reproduction used a simulated provider. Real native providers were not exercised. ### Reproduction 1. Use the baseline checkout with its dependencies installed and runtime built. Run the following from the repository root. This is the executed reproduction, presented inline so it needs no external fixture. It isolates HOME and closes both MCP endpoints. ```sh repro_home=$(mktemp -d) HOME="$repro_home" node --input-type=module <<'JS' import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { createRequire } from 'node:module'; const require = createRequire(resolve('package.json')); const load = path => import(pathToFileURL(resolve(path))); const { Client, InMemoryTransport } = await import(pathToFileURL(require.resolve('@modelcontextprotocol/client'))); const { createServer } = await load('dist/server/createServer.js'); const { createAnalysisExecution } = await load('dist/application/AnalysisProvider.js'); const { AnalysisCapabilityUnavailableError } = await load('dist/domain/analysisErrorCore.js'); const { ok, err } = await load('dist/domain/result.js'); const provider = { id: 'fixture', name: 'Fixture', version: '1' }; const observed = result => ok(createAnalysisExecution(result, provider, { subject: { sha256: 'a'.repeat(64), path: 'fixture.bin', format: 'mach-o', architecture: 'arm64' } })); for (const max_nodes of [2, 3]) { const analysis = { execute: async (operation, parameters) => { if (['decode_interface_builder', 'inspect_native_dispatch_metadata', 'procedure_references'].includes(operation)) return err(new AnalysisCapabilityUnavailableError('fixture', operation, 'Unavailable in fixture')); if (operation === 'list_names') return observed([]); if (operation === 'procedure_address') return observed('0x1000'); if (operation === 'procedure_callees') return observed(parameters.procedure === '0x1000' ? ['0x2000', '0x3000'] : []); throw new Error(`Unexpected operation: ${operation}`); }}; const server = createServer({ kind: 'fixed', analysis }, { environment: {} }); const client = new Client({ name: 'call-relation-reproduction', version: '1' }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); try { await server.connect(serverTransport); await client.connect(clientTransport); const result = await client.callTool({ name: 'trace_native_ui_action', arguments: { action: '0x1000', max_nodes } }); if (result.isError) throw new Error(JSON.stringify(result)); const trace = result.structuredContent.normalized_result; console.log(JSON.stringify({ max_nodes, edges: trace.edges.map(({ to, relation }) => ({ to, relation })), unresolved: trace.unresolved.map(({ relation, reason, evidence }) => ({ relation, reason, description: evidence[0]?.description })) })); } finally { await client.close(); await server.close(); } } JS rm -rf "$repro_home" ``` 2. Compare the `max_nodes: 2` result with the `max_nodes: 3` control. Both requests call `trace_native_ui_action` with `action: "0x1000"`. The provider resolves that seed and supplies callees `0x2000` and `0x3000`, with typed reference reading unavailable. All successful observations share one target SHA-256 identity. The unavailable UI decoder and binary metadata reader are supported fallbacks; the empty name inventory still carries the target identity. ### Expected behavior Bounded traces must preserve the distinction between direct references, resolved indirect references, inferred untyped provider callees, ambiguous candidates, and targetless sites. An omitted node can make an edge unresolved without changing its known relation. For this reproduction, both provider-reported routes must retain `provider_call`; the omission may retain its null destination and node-bound reason. `src/contracts/enhancedToolContracts.ts:80-82` and `docs/native-investigation.md:210-217` require these call forms to remain distinct in bounded traces. ### Impact and scope P2. Valid bounded traces return an incorrect structured relation for an omitted untyped provider callee or typed indirect call. The edge remains unresolved, truncation is explicit, and its evidence description retains the originating classification. Real-provider frequency and severe harm were not established. Increasing the node budget from 2 to 3 restores both relation labels in this small fixture; this does not establish complete traversal or a general workaround for larger graphs. ### REA version rea 6.5.0 at source commit 4692bb33300c7cb77b095b6cc0c33da861c1b860 ### Runtime and platform Node 24.18.0; macOS arm64 ### Target and artifact context Simulated Mach-O arm64 subject `fixture.bin` with one consistent SHA-256 identity; no target execution or native engine required. ### Interface MCP ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The compiled MCP reproduction exited successfully and returned these results: ```json {"max_nodes":2,"edges":[{"to":"native:function:0x2000","relation":"provider_call"}],"unresolved":[{"relation":"direct_call","reason":"native_trace_node_bound_reached","description":"Fixture reported provider_call from 0x1000 to 0x3000"}]} {"max_nodes":3,"edges":[{"to":"native:function:0x2000","relation":"provider_call"},{"to":"native:function:0x3000","relation":"provider_call"}],"unresolved":[]} ``` A separate focused MCP integration test reproduced the same failure for typed indirect references. It supplied two available call references at separate sites, each with `computed: true`, `indirect: true`, and a unique target procedure. At node budget 2, the retained edge was `indirect_call` with resolved classification, while the omitted edge was `direct_call` and unresolved. The omitted edge's evidence description still identified an indirect call. The focused run executed five cases. The provider and indirect budget-2 relation assertions failed, both budget-3 controls passed, and a genuine direct-call budget-2 control passed. The budget-3 controls establish restored relation labels for this fixture; they do not establish complete traversal. No real-provider occurrence rate was measured. ### Typed outcome or recovery expected Rerun the inline reproduction and require the budget-2 omitted edge to retain `provider_call`, while keeping its unresolved status, null destination, omission reason, provider evidence, and node budget. The retained sibling and budget-3 control must continue to report `provider_call`. With the typed computed/indirect input described above, require the omitted relation to remain `indirect_call`; a genuine direct-call control must remain `direct_call`. Preserve target-identity checks, explicit truncation, and uncertainty about runtime reachability. ### Analysis or suspected cause Source-derived: `src/application/native/NativeUiActionTrace.ts:506-520` assigns `direct_call` in the node-bound omission branch, while lines 534-540 preserve the route relation for retained edges. `src/application/native/NativeCallRoutes.ts:46-58,146-156` distinguishes typed indirect routes from inferred provider routes. ### Additional context The failing code is unchanged on current `origin/main` at `22ec8bdb2f7d4626587f23b1eed4a207df600386`. Related #3343 and PR #3828 concern the separate Objective-C receiver-kind join; closed #2565 concerns retention of targetless sites. None changes the omitted-edge relation assignment.
Author
Owner

@XinwangTao commented on GitHub (Oct 15, 2026):

Confirmed: when the node budget forces an edge out, the structured relation must not silently downgrade it to direct_call — that contradicts the retained evidence description and misleads callers into trusting an unresolved callee as a resolved one. An explicit omitted/unresolved relation placeholder would keep the trace internally consistent.

<!-- gh-comment-id:6186470060 --> @XinwangTao commented on GitHub (Oct 15, 2026): Confirmed: when the node budget forces an edge out, the structured relation must not silently downgrade it to direct_call — that contradicts the retained evidence description and misleads callers into trusting an unresolved callee as a resolved one. An explicit omitted/unresolved relation placeholder would keep the trace internally consistent.
Author
Owner

@morluto commented on GitHub (Oct 18, 2026):

Fixed on main in 65884350f.

The node-bound consumer hardcoded direct_call after the reader had already established the route kind. It now preserves the original relation, provider and source-site evidence while returning a null target with unresolved status. Generic enumeration failures use an unclassified provider_call marker. The related shared-target collision (#5128) is also fixed: omitted-edge identity includes call site, so preserving a relation does not overwrite another observation.

Validation: 69 focused cases passed, followed by a final 30-case rerun and the final 9-case public MCP assertion check. The public regressions cover mixed kinds, failed fallback, overlap, cancellation, direct/indirect/provider node omissions and shared-target sites. Before the fixes, the corrected committed-baseline public fixture failed 6/8 cases; the additional identity regression failed with 8 controls passing. Full static/architecture checks, docs, test discovery (1,593 files), hooks and pre-push checks passed. These are controlled providers through the actual SDK/server path; real native-engine behavior remains unverified. Current-head CI runs separately in the background.

<!-- gh-comment-id:6256140842 --> @morluto commented on GitHub (Oct 18, 2026): Fixed on main in [65884350f](https://github.com/morluto/rea/commit/65884350ff3373f2adc7337c1945eeb54334693f). The node-bound consumer hardcoded `direct_call` after the reader had already established the route kind. It now preserves the original relation, provider and source-site evidence while returning a null target with unresolved status. Generic enumeration failures use an unclassified `provider_call` marker. The related shared-target collision (#5128) is also fixed: omitted-edge identity includes call site, so preserving a relation does not overwrite another observation. Validation: 69 focused cases passed, followed by a final 30-case rerun and the final 9-case public MCP assertion check. The public regressions cover mixed kinds, failed fallback, overlap, cancellation, direct/indirect/provider node omissions and shared-target sites. Before the fixes, the corrected committed-baseline public fixture failed 6/8 cases; the additional identity regression failed with 8 controls passing. Full static/architecture checks, docs, test discovery (1,593 files), hooks and pre-push checks passed. These are controlled providers through the actual SDK/server path; real native-engine behavior remains unverified. Current-head CI runs separately in the background.
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#1528
No description provided.