[GH-ISSUE #3873] [Bug] Empty Obj-C protocol name aborts dispatch-metadata inspection and discards a valid class #1492

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

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

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

inspect-native-dispatch-metadata on a Mach-O whose protocol declaration has an empty (immediately NUL-terminated) name accepts the protocol as decoded, then fails whole-result schema validation on objc_protocols[0].name, exits 1 with execution_failure, and discards a valid class decoded from the same image. Changing only the protocol name to a nonempty string makes the same command succeed and return both the class and the protocol.

A protocol declaration whose name points to an empty NUL-terminated string is accepted as decoded; the final whole-result schema validation then throws on objc_protocols[0].name, the CLI exits 1 with execution_failure, returns no dispatch result, and discards the valid RetainedClass from the same image.

Reproduction

Generate two 4096-byte little-endian arm64 Mach-O fixtures with identical mapped class and protocol records that differ only in the protocol-name bytes, then inspect both; neither image is loaded or executed.

work="$(mktemp -d)"
cat > "$work/metadata-fixtures.mjs" <<'JS'
import {writeFileSync} from 'node:fs';
import {join} from 'node:path';
const base = 0x100000000n;
for (const emptyProtocol of [false, true]) {
  const b = Buffer.alloc(4096);
  b.writeUInt32LE(0xfeedfacf, 0);
  b.writeUInt32LE(0x0100000c, 4);
  b.writeUInt32LE(2, 12);
  b.writeUInt32LE(1, 16);
  b.writeUInt32LE(232, 20);
  b.writeUInt32LE(0x19, 32);
  b.writeUInt32LE(232, 36);
  b.write('__DATA', 40);
  b.writeBigUInt64LE(base, 56);
  b.writeBigUInt64LE(4096n, 64);
  b.writeBigUInt64LE(4096n, 80);
  b.writeUInt32LE(5, 88);
  b.writeUInt32LE(5, 92);
  b.writeUInt32LE(2, 96);
  for (const [at, name, off] of [
    [104, '__objc_classlist', 0x200],
    [184, '__objc_protolist', 0x300],
  ]) {
    b.write(name, at);
    b.write('__DATA', at + 16);
    b.writeBigUInt64LE(base + BigInt(off), at + 32);
    b.writeBigUInt64LE(8n, at + 40);
    b.writeUInt32LE(off, at + 48);
  }
  b.writeBigUInt64LE(base + 0x400n, 0x200);
  b.writeBigUInt64LE(base + 0x500n, 0x420);
  b.writeUInt32LE(2, 0x500);
  b.writeUInt32LE(16, 0x508);
  b.writeBigUInt64LE(base + 0x600n, 0x518);
  b.write('RetainedClass\0', 0x600);
  b.writeBigUInt64LE(base + 0x700n, 0x300);
  b.writeBigUInt64LE(base + 0x800n, 0x708);
  b.writeUInt32LE(72, 0x740);
  if (!emptyProtocol) b.write('RetainedProtocol\0', 0x800);
  writeFileSync(join(process.argv[2], emptyProtocol
    ? 'empty-protocol.macho' : 'valid-protocol.macho'), b);
}
JS
node "$work/metadata-fixtures.mjs" "$work"
rea inspect-native-dispatch-metadata "$work/valid-protocol.macho" --format json \
  | jq '.normalized_result.result | {classes: [.objc_classes[].name], protocols: [.objc_protocols[].name]}'
rea inspect-native-dispatch-metadata "$work/empty-protocol.macho" --format json \
  | jq '{code, issues: (.details.diagnostics.reason | fromjson)}'

The control (valid-protocol.macho) exits 0:

{"classes": ["RetainedClass"], "protocols": ["RetainedProtocol"]}

The empty-name fixture exits 1:

{
  "code": "execution_failure",
  "issues": [
    {
      "origin": "string",
      "code": "too_small",
      "minimum": 1,
      "inclusive": true,
      "path": ["objc_protocols", 0, "name"],
      "message": "Too small: expected string to have >=1 characters"
    }
  ]
}

Expected behavior

AGENTS.md:29 requires preserving partial facts and meaningful failure reasons; src/native/AppleObjcProtocols.ts:97-100 already records individual protocol decode failures and src/native/AppleDispatchCoverage.ts projects them into partial coverage, so one malformed protocol declaration should yield a protocol-level failure while retaining the independently decoded class and its evidence. src/domain/native/objcSwiftMetadata.ts:47 requires a nonempty protocol name.

Impact and scope

A single malformed protocol-name input fails the entire native dispatch-metadata inspection, including unrelated valid class evidence from the same image, dropping a partial result the decoder had already produced; the failure is bounded to this input.

Workaround: none. No workaround preserving the original artifact's binary metadata was found: editing the protocol name changes the evidence, and selecting a symbol-only provider would lose the binary metadata facts.

Reporter's impact estimate: P2 (supported).

REA version

6.5.0, built from source at main commit 034342ba28 (latest npm release is 6.5.0)

Runtime and platform

Node.js v26.10.0; macOS 27.0.1 arm64

Target and artifact context

Mach-O arm64 with mapped Objective-C class and protocol records

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: src/domain/native/objcSwiftMetadata.ts:47 requires a nonempty protocol name, and AGENTS.md:29 with src/native/AppleObjcProtocols.ts:97-100 and src/native/AppleDispatchCoverage.ts establish that per-protocol decode failures should be retained as partial coverage.
  • Failure source: the observed exit-1 error above, with root cause at src/native/AppleObjcProtocols.ts:69 reading the name without validation and appending it at lines 79-96, the per-protocol catch not covering objcSwiftMetadataSchema.parse(result) at src/native/AppleDispatchMetadata.ts:180, and src/native/NativeMacOSProvider.ts:214-225 converting the failure into a provider error without retaining the decoded metadata.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

Typed outcome or recovery expected

The command should have returned a result retaining the decoded RetainedClass and reported the malformed protocol as a protocol-level failure or partial coverage, instead of exiting 1 with execution_failure and no result.

Restoration check:

Failing observable: empty-protocol.macho exits 1 with code: "execution_failure" and a too_small issue on objc_protocols[0].name, returning no result and no class. Passing observable: the same input returns a result that retains the decoded RetainedClass and reports the malformed protocol as a protocol-level failure or partial coverage rather than discarding the whole inspection. Control: valid-protocol.macho, identical except for a nonempty protocol name, exits 0 and returns both RetainedClass and RetainedProtocol.

Analysis or suspected cause

src/native/AppleObjcProtocols.ts:69 reads the protocol name without validating nonemptiness and appends it at :79-96; the per-protocol catch cannot catch the later objcSwiftMetadataSchema.parse(result) at src/native/AppleDispatchMetadata.ts:180 (schema requires nonempty name at src/domain/native/objcSwiftMetadata.ts:47), and src/native/NativeMacOSProvider.ts:214-225 turns the throw into a provider error without retaining the already-decoded class.

Additional context

Closed #2610 concerns an unmapped protocol-list slot, while this fixture's slot and declaration are mapped; closed #2611 concerns empty class names and closed #3336 concerns malformed property records. Those decoders are different and no claim is made that their reproductions regressed.

This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3873 ### 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 `inspect-native-dispatch-metadata` on a Mach-O whose protocol declaration has an empty (immediately NUL-terminated) name accepts the protocol as decoded, then fails whole-result schema validation on `objc_protocols[0].name`, exits 1 with `execution_failure`, and discards a valid class decoded from the same image. Changing only the protocol name to a nonempty string makes the same command succeed and return both the class and the protocol. A protocol declaration whose name points to an empty NUL-terminated string is accepted as decoded; the final whole-result schema validation then throws on `objc_protocols[0].name`, the CLI exits 1 with `execution_failure`, returns no dispatch result, and discards the valid `RetainedClass` from the same image. ### Reproduction Generate two 4096-byte little-endian arm64 Mach-O fixtures with identical mapped class and protocol records that differ only in the protocol-name bytes, then inspect both; neither image is loaded or executed. ```sh work="$(mktemp -d)" cat > "$work/metadata-fixtures.mjs" <<'JS' import {writeFileSync} from 'node:fs'; import {join} from 'node:path'; const base = 0x100000000n; for (const emptyProtocol of [false, true]) { const b = Buffer.alloc(4096); b.writeUInt32LE(0xfeedfacf, 0); b.writeUInt32LE(0x0100000c, 4); b.writeUInt32LE(2, 12); b.writeUInt32LE(1, 16); b.writeUInt32LE(232, 20); b.writeUInt32LE(0x19, 32); b.writeUInt32LE(232, 36); b.write('__DATA', 40); b.writeBigUInt64LE(base, 56); b.writeBigUInt64LE(4096n, 64); b.writeBigUInt64LE(4096n, 80); b.writeUInt32LE(5, 88); b.writeUInt32LE(5, 92); b.writeUInt32LE(2, 96); for (const [at, name, off] of [ [104, '__objc_classlist', 0x200], [184, '__objc_protolist', 0x300], ]) { b.write(name, at); b.write('__DATA', at + 16); b.writeBigUInt64LE(base + BigInt(off), at + 32); b.writeBigUInt64LE(8n, at + 40); b.writeUInt32LE(off, at + 48); } b.writeBigUInt64LE(base + 0x400n, 0x200); b.writeBigUInt64LE(base + 0x500n, 0x420); b.writeUInt32LE(2, 0x500); b.writeUInt32LE(16, 0x508); b.writeBigUInt64LE(base + 0x600n, 0x518); b.write('RetainedClass\0', 0x600); b.writeBigUInt64LE(base + 0x700n, 0x300); b.writeBigUInt64LE(base + 0x800n, 0x708); b.writeUInt32LE(72, 0x740); if (!emptyProtocol) b.write('RetainedProtocol\0', 0x800); writeFileSync(join(process.argv[2], emptyProtocol ? 'empty-protocol.macho' : 'valid-protocol.macho'), b); } JS node "$work/metadata-fixtures.mjs" "$work" rea inspect-native-dispatch-metadata "$work/valid-protocol.macho" --format json \ | jq '.normalized_result.result | {classes: [.objc_classes[].name], protocols: [.objc_protocols[].name]}' rea inspect-native-dispatch-metadata "$work/empty-protocol.macho" --format json \ | jq '{code, issues: (.details.diagnostics.reason | fromjson)}' ``` The control (`valid-protocol.macho`) exits 0: ```json {"classes": ["RetainedClass"], "protocols": ["RetainedProtocol"]} ``` The empty-name fixture exits 1: ```json { "code": "execution_failure", "issues": [ { "origin": "string", "code": "too_small", "minimum": 1, "inclusive": true, "path": ["objc_protocols", 0, "name"], "message": "Too small: expected string to have >=1 characters" } ] } ``` ### Expected behavior AGENTS.md:29 requires preserving partial facts and meaningful failure reasons; src/native/AppleObjcProtocols.ts:97-100 already records individual protocol decode failures and src/native/AppleDispatchCoverage.ts projects them into partial coverage, so one malformed protocol declaration should yield a protocol-level failure while retaining the independently decoded class and its evidence. src/domain/native/objcSwiftMetadata.ts:47 requires a nonempty protocol name. ### Impact and scope A single malformed protocol-name input fails the entire native dispatch-metadata inspection, including unrelated valid class evidence from the same image, dropping a partial result the decoder had already produced; the failure is bounded to this input. Workaround: none. No workaround preserving the original artifact's binary metadata was found: editing the protocol name changes the evidence, and selecting a symbol-only provider would lose the binary metadata facts. Reporter's impact estimate: P2 (supported). ### REA version 6.5.0, built from source at main commit 034342ba28c8e56c3746be92b78aa7299afa02bd (latest npm release is 6.5.0) ### Runtime and platform Node.js v26.10.0; macOS 27.0.1 arm64 ### Target and artifact context Mach-O arm64 with mapped Objective-C class and protocol records ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence - Expected source: src/domain/native/objcSwiftMetadata.ts:47 requires a nonempty protocol name, and AGENTS.md:29 with src/native/AppleObjcProtocols.ts:97-100 and src/native/AppleDispatchCoverage.ts establish that per-protocol decode failures should be retained as partial coverage. - Failure source: the observed exit-1 error above, with root cause at src/native/AppleObjcProtocols.ts:69 reading the name without validation and appending it at lines 79-96, the per-protocol catch not covering `objcSwiftMetadataSchema.parse(result)` at src/native/AppleDispatchMetadata.ts:180, and src/native/NativeMacOSProvider.ts:214-225 converting the failure into a provider error without retaining the decoded metadata. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete ### Typed outcome or recovery expected The command should have returned a result retaining the decoded RetainedClass and reported the malformed protocol as a protocol-level failure or partial coverage, instead of exiting 1 with execution_failure and no result. Restoration check: Failing observable: `empty-protocol.macho` exits 1 with `code: "execution_failure"` and a `too_small` issue on `objc_protocols[0].name`, returning no result and no class. Passing observable: the same input returns a result that retains the decoded `RetainedClass` and reports the malformed protocol as a protocol-level failure or partial coverage rather than discarding the whole inspection. Control: `valid-protocol.macho`, identical except for a nonempty protocol name, exits 0 and returns both `RetainedClass` and `RetainedProtocol`. ### Analysis or suspected cause src/native/AppleObjcProtocols.ts:69 reads the protocol name without validating nonemptiness and appends it at :79-96; the per-protocol catch cannot catch the later objcSwiftMetadataSchema.parse(result) at src/native/AppleDispatchMetadata.ts:180 (schema requires nonempty name at src/domain/native/objcSwiftMetadata.ts:47), and src/native/NativeMacOSProvider.ts:214-225 turns the throw into a provider error without retaining the already-decoded class. ### Additional context Closed #2610 concerns an unmapped protocol-list slot, while this fixture's slot and declaration are mapped; closed #2611 concerns empty class names and closed #3336 concerns malformed property records. Those decoders are different and no claim is made that their reproductions regressed. This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.
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#1492
No description provided.