[GH-ISSUE #6086] [Bug] Empty incomplete Objective-C import scans lose partial coverage #3721

Closed
opened 2026-10-31 00:43:22 +00:00 by bsakaguchi · 0 comments
Owner

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

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

A successful public stdio MCP call to inspect_native_dispatch_metadata with objc_class_import_addresses: [] and objc_class_import_addresses_complete: false returns binding coverage not_requested, a null reason, and zero examined/decoded addresses. The result discards the caller's explicit declaration that the candidate scan was incomplete.

With the same target and one actual bound address, the same false completeness flag correctly produces partial with the existing candidate-budget explanation. Both requests pass input and output schema validation.

Reproduction

Executed once through the built production stdio MCP server and real native static Mach-O reader, with no simulated external provider. From a built checkout with locked dependencies, save this as tests/acceptance/classImportCoverage.test.ts, set ARTIFACT_DIR to an existing disposable writable directory, and run node node_modules/vitest/vitest.mjs run tests/acceptance/classImportCoverage.test.ts --maxWorkers 1. Remove the test afterward. Its assertions characterize the current behavior.

import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { expect, it } from 'vitest';
import { connectLocalToolsMcp } from '../fixtures/localToolsMcp.js';
import { nativeObjcClassImportMachO } from '../fixtures/nativeObjcClassImportMachO.js';

it('compares omitted, complete-empty, incomplete-empty and nonempty import requests through stdio MCP', async () => {
  const root = process.env.ARTIFACT_DIR!;
  const fixture = nativeObjcClassImportMachO();
  const target = join(root, 'class-import.macho');
  await writeFile(target, fixture.bytes);
  const environment: Record<string, string> = { REA_LOG_LEVEL: 'silent', REA_ANALYSIS_PROVIDER: 'auto' };
  for (const name of ['HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME']) {
    environment[name] = join(root, name.toLowerCase());
    await mkdir(environment[name], { recursive: true });
  }
  const { call } = await connectLocalToolsMcp(environment);
  expect((await call('open_binary', { path: target })).isError).not.toBe(true);
  const cases = [
    { label: 'omitted', input: {} },
    { label: 'complete-empty', input: { objc_class_import_addresses: [], objc_class_import_addresses_complete: true } },
    { label: 'incomplete-empty', input: { objc_class_import_addresses: [], objc_class_import_addresses_complete: false } },
    { label: 'incomplete-nonempty', input: { objc_class_import_addresses: [fixture.address], objc_class_import_addresses_complete: false } },
  ];
  const observations = [];
  for (const { label, input } of cases) {
    const response = await call('inspect_native_dispatch_metadata', { max_records: 100, ...input });
    expect(response.isError, JSON.stringify(response)).not.toBe(true);
    const result = (response.structuredContent as any).normalized_result.result;
    observations.push({ label, coverage: result.objc_class_import_binding_coverage, bindings: result.objc_class_import_bindings });
    if (label === 'omitted') expect(result.objc_class_import_binding_coverage).toBeUndefined();
    if (label.endsWith('-empty')) expect(result.objc_class_import_binding_coverage).toMatchObject({ status: 'not_requested', reason: null, examined: 0, decoded: 0 });
    if (label === 'incomplete-nonempty') expect(result.objc_class_import_binding_coverage).toMatchObject({ status: 'partial', reason: 'Candidate address scan reached the existing max_records limit', examined: 1, decoded: 1 });
  }
  console.log('IMPORT_OBSERVATIONS', JSON.stringify(observations));
  await writeFile(join(root, 'import-observations.json'), JSON.stringify(observations, null, 2));
  expect((await call('close_binary', {})).isError).not.toBe(true);
}, 30_000);

The fixture is the existing source-authored thin arm64 Mach-O with a classic dyld class-symbol bind. It is inspected without loading or executing target code. The MCP harness starts and closes the built server, validates schemas, and uses isolated home/XDG directories supplied by the test.

Expected behavior

When the address array is explicitly present and its completeness flag is false, retain partial binding coverage with the existing explanation: Candidate address scan reached the existing max_records limit. The empty case should still report examined 0 and decoded 0.

The public input schema accepts an empty array and independently documents false completeness as retaining partial coverage after a bounded candidate scan. The coverage contract bases completeness on examined coverage and exhaustiveness. This report does not prescribe whether an explicitly complete empty list should remain not_requested.

Impact and scope

P2: a bounded coverage-integrity defect for a valid direct MCP request. The returned status and null reason lose an explicit limitation, making an incomplete candidate scan indistinguishable from a complete empty request at this field. Callers can retain and honor their own false completeness flag as a practical workaround.

The executed cases supply the flag directly; a naturally truncated candidate inventory was not captured. The current get_objc_classes wrapper exits before native binding inspection when it selects no candidates, so this report does not claim that wrapper emits the empty/incomplete request. Other metadata remains available; no target changes, data loss or security-boundary crossing were observed. Production frequency and downstream effects are unknown.

REA version

rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e; prepared built runtime. Fetched main f86206535f8ec71014781f9d432a84b01e183e5d leaves the relevant input contract and native decoder unchanged; later-main runtime behavior was not separately executed.

Runtime and platform

macOS 27.0.1 arm64; Node.js v24.18.0 (repository pin); locked Vitest 5.0.3. Production stdio MCP server and native static reader; no Hopper, Ghidra or IDA session.

Target and artifact context

Existing inert arm64 nativeObjcClassImportMachO fixture with LC_DYLD_INFO_ONLY and one _OBJC_CLASS_$_Document bind at 0x100001000. The nonempty control successfully resolves that bind.

Interface

MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Reduced output from the four successful calls:

[
  {"input":"addresses omitted","coverage_field":"absent"},
  {"input":"empty, complete true","coverage":{"facet":"objc_class_import_bindings","status":"not_requested","reason":null,"examined":0,"decoded":0}},
  {"input":"empty, complete false","coverage":{"facet":"objc_class_import_bindings","status":"not_requested","reason":null,"examined":0,"decoded":0}},
  {"input":"one bound address, complete false","coverage":{"facet":"objc_class_import_bindings","status":"partial","reason":"Candidate address scan reached the existing max_records limit","examined":1,"decoded":1}}
]

The empty requests return objc_class_import_bindings: []; the nonempty request retains the actual class bind. open_binary, each inspection, and close_binary all succeeded. These are MCP observations, rather than isolated calls to the internal binding helper.

Typed outcome or recovery expected

For the empty incomplete request, successful metadata with:

{"facet":"objc_class_import_bindings","status":"partial","reason":"Candidate address scan reached the existing max_records limit","examined":0,"decoded":0}

Analysis or suspected cause

Source-derived: the early return combines omitted and empty addresses before considering addressesComplete. The outer decoder publishes the resulting coverage for every present array, including an empty one. The application dispatch forwards both public fields unchanged. The explicit incompleteness must survive the empty-array branch.

Additional context

Related reports #6000 (Swift kind-3 rebase-to-zero retention), #5706 (class/metaclass counter units), #5840 (provider explanation loss in the class-label fallback), and #5607 (duplicate import aliases) concern different inputs and producing boundaries. No first affected or last known good revision was established. No fix was attempted.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6086 ### 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 A successful public stdio MCP call to `inspect_native_dispatch_metadata` with `objc_class_import_addresses: []` and `objc_class_import_addresses_complete: false` returns binding coverage `not_requested`, a null reason, and zero examined/decoded addresses. The result discards the caller's explicit declaration that the candidate scan was incomplete. With the same target and one actual bound address, the same false completeness flag correctly produces `partial` with the existing candidate-budget explanation. Both requests pass input and output schema validation. ### Reproduction Executed once through the built production stdio MCP server and real native static Mach-O reader, with no simulated external provider. From a built checkout with locked dependencies, save this as `tests/acceptance/classImportCoverage.test.ts`, set `ARTIFACT_DIR` to an existing disposable writable directory, and run `node node_modules/vitest/vitest.mjs run tests/acceptance/classImportCoverage.test.ts --maxWorkers 1`. Remove the test afterward. Its assertions characterize the current behavior. ```ts import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { expect, it } from 'vitest'; import { connectLocalToolsMcp } from '../fixtures/localToolsMcp.js'; import { nativeObjcClassImportMachO } from '../fixtures/nativeObjcClassImportMachO.js'; it('compares omitted, complete-empty, incomplete-empty and nonempty import requests through stdio MCP', async () => { const root = process.env.ARTIFACT_DIR!; const fixture = nativeObjcClassImportMachO(); const target = join(root, 'class-import.macho'); await writeFile(target, fixture.bytes); const environment: Record<string, string> = { REA_LOG_LEVEL: 'silent', REA_ANALYSIS_PROVIDER: 'auto' }; for (const name of ['HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME']) { environment[name] = join(root, name.toLowerCase()); await mkdir(environment[name], { recursive: true }); } const { call } = await connectLocalToolsMcp(environment); expect((await call('open_binary', { path: target })).isError).not.toBe(true); const cases = [ { label: 'omitted', input: {} }, { label: 'complete-empty', input: { objc_class_import_addresses: [], objc_class_import_addresses_complete: true } }, { label: 'incomplete-empty', input: { objc_class_import_addresses: [], objc_class_import_addresses_complete: false } }, { label: 'incomplete-nonempty', input: { objc_class_import_addresses: [fixture.address], objc_class_import_addresses_complete: false } }, ]; const observations = []; for (const { label, input } of cases) { const response = await call('inspect_native_dispatch_metadata', { max_records: 100, ...input }); expect(response.isError, JSON.stringify(response)).not.toBe(true); const result = (response.structuredContent as any).normalized_result.result; observations.push({ label, coverage: result.objc_class_import_binding_coverage, bindings: result.objc_class_import_bindings }); if (label === 'omitted') expect(result.objc_class_import_binding_coverage).toBeUndefined(); if (label.endsWith('-empty')) expect(result.objc_class_import_binding_coverage).toMatchObject({ status: 'not_requested', reason: null, examined: 0, decoded: 0 }); if (label === 'incomplete-nonempty') expect(result.objc_class_import_binding_coverage).toMatchObject({ status: 'partial', reason: 'Candidate address scan reached the existing max_records limit', examined: 1, decoded: 1 }); } console.log('IMPORT_OBSERVATIONS', JSON.stringify(observations)); await writeFile(join(root, 'import-observations.json'), JSON.stringify(observations, null, 2)); expect((await call('close_binary', {})).isError).not.toBe(true); }, 30_000); ``` The fixture is the existing source-authored thin arm64 Mach-O with a classic dyld class-symbol bind. It is inspected without loading or executing target code. The MCP harness starts and closes the built server, validates schemas, and uses isolated home/XDG directories supplied by the test. ### Expected behavior When the address array is explicitly present and its completeness flag is false, retain partial binding coverage with the existing explanation: `Candidate address scan reached the existing max_records limit`. The empty case should still report examined 0 and decoded 0. The [public input schema](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/contracts/enhancedInputs.ts#L27-L39) accepts an empty array and independently documents false completeness as retaining partial coverage after a bounded candidate scan. The [coverage contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/tool-design.md#L170-L176) bases completeness on examined coverage and exhaustiveness. This report does not prescribe whether an explicitly complete empty list should remain `not_requested`. ### Impact and scope P2: a bounded coverage-integrity defect for a valid direct MCP request. The returned status and null reason lose an explicit limitation, making an incomplete candidate scan indistinguishable from a complete empty request at this field. Callers can retain and honor their own false completeness flag as a practical workaround. The executed cases supply the flag directly; a naturally truncated candidate inventory was not captured. The current `get_objc_classes` wrapper exits before native binding inspection when it selects no candidates, so this report does not claim that wrapper emits the empty/incomplete request. Other metadata remains available; no target changes, data loss or security-boundary crossing were observed. Production frequency and downstream effects are unknown. ### REA version rea-agents 6.7.0; source revision `52415975942a4b9c53a817915b313ccc404f280e`; prepared built runtime. Fetched main `f86206535f8ec71014781f9d432a84b01e183e5d` leaves the relevant input contract and native decoder unchanged; later-main runtime behavior was not separately executed. ### Runtime and platform macOS 27.0.1 arm64; Node.js v24.18.0 (repository pin); locked Vitest 5.0.3. Production stdio MCP server and native static reader; no Hopper, Ghidra or IDA session. ### Target and artifact context Existing inert arm64 `nativeObjcClassImportMachO` fixture with `LC_DYLD_INFO_ONLY` and one `_OBJC_CLASS_$_Document` bind at `0x100001000`. The nonempty control successfully resolves that bind. ### Interface MCP ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Reduced output from the four successful calls: ```json [ {"input":"addresses omitted","coverage_field":"absent"}, {"input":"empty, complete true","coverage":{"facet":"objc_class_import_bindings","status":"not_requested","reason":null,"examined":0,"decoded":0}}, {"input":"empty, complete false","coverage":{"facet":"objc_class_import_bindings","status":"not_requested","reason":null,"examined":0,"decoded":0}}, {"input":"one bound address, complete false","coverage":{"facet":"objc_class_import_bindings","status":"partial","reason":"Candidate address scan reached the existing max_records limit","examined":1,"decoded":1}} ] ``` The empty requests return `objc_class_import_bindings: []`; the nonempty request retains the actual class bind. `open_binary`, each inspection, and `close_binary` all succeeded. These are MCP observations, rather than isolated calls to the internal binding helper. ### Typed outcome or recovery expected For the empty incomplete request, successful metadata with: ```json {"facet":"objc_class_import_bindings","status":"partial","reason":"Candidate address scan reached the existing max_records limit","examined":0,"decoded":0} ``` ### Analysis or suspected cause Source-derived: [the early return](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/AppleObjcClassImportBindings.ts#L24-L51) combines omitted and empty addresses before considering `addressesComplete`. [The outer decoder](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/AppleDispatchMetadata.ts#L143-L151) publishes the resulting coverage for every present array, including an empty one. [The application dispatch](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/EnhancedTools.ts#L190-L202) forwards both public fields unchanged. The explicit incompleteness must survive the empty-array branch. ### Additional context Related reports #6000 (Swift kind-3 rebase-to-zero retention), #5706 (class/metaclass counter units), #5840 (provider explanation loss in the class-label fallback), and #5607 (duplicate import aliases) concern different inputs and producing boundaries. No first affected or last known good revision was established. No fix was attempted.
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#3721
No description provided.