[GH-ISSUE #4534] [Bug] Empty Objective-C method selectors escape entry recovery and invalidate dispatch metadata #2360

Closed
opened 2026-10-17 21:03:45 +00:00 by bsakaguchi · 3 comments
Owner

Originally created by @kevinjosh10 on GitHub (Oct 16, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/4534

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 Mach-O binary containing an Objective-C class whose method list (method_list_t) contains an entry with an empty (\0) selector causes decodeAppleDispatchMetadata to fail schema validation with an unhandled ZodError (too_small: expected string to have >=1 characters at objc_dispatch_implementations[0].selector and objc_classes[0].methods[0].selector).

Because the method parser pushes the selector without verifying non-empty string length, the error is not caught by the class recovery boundary, causing the entire native dispatch metadata inspection to abort and discard all valid classes, protocols, and categories.

Reproduction

  1. Minimal in-memory Mach-O reproduction script (repro_selector.mts):
import { decodeAppleDispatchMetadata } from "./src/native/AppleDispatchMetadata.js";

const base = 0x100000000n;
const bytes = Buffer.alloc(4096);
const u32 = (offset: number, value: number) => bytes.writeUInt32LE(value, offset);
const ptr = (offset: number, target: number) => bytes.writeBigUInt64LE(base + BigInt(target), offset);

// Mach-O arm64 header + __DATA segment + __objc_classlist
u32(0, 0xfeedfacf);
u32(4, 0x0100000c);
u32(16, 1);
u32(20, 152);
u32(32, 0x19);
u32(36, 152);
bytes.write("__DATA", 40);
ptr(56, 0);
bytes.writeBigUInt64LE(4096n, 64);
bytes.writeBigUInt64LE(4096n, 80);
u32(92, 5);
u32(96, 1);
bytes.write("__objc_classlist", 104);
bytes.write("__DATA", 120);
ptr(136, 0x180);
bytes.writeBigUInt64LE(8n, 144);
u32(152, 0x180);

// class at 0x200 pointing to class_ro_t at 0x300
ptr(0x180, 0x200);
ptr(0x200, 0x280);
ptr(0x220, 0x300);
ptr(0x2a0, 0x380);
u32(0x308, 16);
ptr(0x318, 0x450); // class name "Fixture"
ptr(0x320, 0x480); // baseMethods pointing to method_list_t at 0x480
bytes.write("Fixture\0", 0x450);

// method_list_t at 0x480: flags 24 (stride 24, absolute), count 1
u32(0x480, 24);
u32(0x484, 1);
ptr(0x488, 0x460); // selector pointer -> empty string "\0"
ptr(0x490, 0x5c0); // types pointer
ptr(0x498, 0);     // imp pointer
bytes.write("\0", 0x460);
bytes.write("v16@0:8\0", 0x5c0);

decodeAppleDispatchMetadata(bytes, 100, {
  path: "/fixture/app",
  sha256: "a".repeat(64),
});
  1. Run with Node / tsx:
npx tsx repro_selector.mts
  1. Observe ZodError thrown during top-level metadata schema validation:
[
  {
    "origin": "string",
    "code": "too_small",
    "minimum": 1,
    "inclusive": true,
    "path": [
      "objc_classes",
      0,
      "methods",
      0,
      "selector"
    ],
    "message": "Too small: expected string to have >=1 characters"
  },
  {
    "origin": "string",
    "code": "too_small",
    "minimum": 1,
    "inclusive": true,
    "path": [
      "objc_dispatch_implementations",
      0,
      "selector"
    ],
    "message": "Too small: expected string to have >=1 characters"
  }
]

Expected behavior

An empty method selector should be rejected or reported inside the method/class decode boundary (e.g. throwing a TypeError("Objective-C method selector is empty") or skipping the empty selector), allowing per-class recovery to record the failure and preserve valid sibling classes and methods, returning a valid ObjcSwiftMetadata result.

Impact and scope

Any Mach-O binary with stripped, corrupted, or zeroed selector references in method_list_t crashes decodeAppleDispatchMetadata with an unhandled exception, wiping out the entire metadata inventory.

REA version

8194695144045e0a950f2989190e7337890342f7 (6.5.0)

Runtime and platform

Node v24.13.0; Windows x64

Target and artifact context

Mach-O arm64 binary; Objective-C metadata (__objc_classlist and method_list_t)

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

In src/native/AppleObjcDispatchFacets.ts:165-171:

const selector = read.string(selectorAddress);
selectors.push(selector);
result.objc_dispatch_implementations.push({
  class_name: className,
  selector,
  method_type: methodType,
  ...

And in readClass:

methods(read.pointer(ro + 32n), name, meta ? "class" : "instance");
result.objc_classes.push({
  name,
  methods: result.objc_dispatch_implementations
    ...
    .map((item) => ({ selector: item.selector, ... }))

Both objcDispatchImplementationSchema (src/domain/native/objcSwiftMetadata.ts:160) and objcMethodSchema (src/domain/native/objcSwiftMetadata.ts:26) mandate selector: z.string().min(1). When read.string(selectorAddress) returns "", the invalid selector is added unchecked and later triggers a fatal ZodError at objcSwiftMetadataSchema.parse(result).

Typed outcome or recovery expected

The method failure should be recorded, and decodeAppleDispatchMetadata should return a typed ObjcSwiftMetadata object retaining all valid classes, ivars, protocols, and methods.

Analysis or suspected cause

methods helper in AppleObjcDispatchFacets.ts does not assert selector.length > 0. Adding entry-level validation allows readClass's try/catch to isolate the defect to that class or method rather than escalating to a whole-analysis crash.

Originally created by @kevinjosh10 on GitHub (Oct 16, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4534 ### 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 Mach-O binary containing an Objective-C class whose method list (`method_list_t`) contains an entry with an empty (`\0`) selector causes `decodeAppleDispatchMetadata` to fail schema validation with an unhandled `ZodError` (`too_small: expected string to have >=1 characters at objc_dispatch_implementations[0].selector` and `objc_classes[0].methods[0].selector`). Because the method parser pushes the selector without verifying non-empty string length, the error is not caught by the class recovery boundary, causing the entire native dispatch metadata inspection to abort and discard all valid classes, protocols, and categories. ### Reproduction 1. Minimal in-memory Mach-O reproduction script (`repro_selector.mts`): ```ts import { decodeAppleDispatchMetadata } from "./src/native/AppleDispatchMetadata.js"; const base = 0x100000000n; const bytes = Buffer.alloc(4096); const u32 = (offset: number, value: number) => bytes.writeUInt32LE(value, offset); const ptr = (offset: number, target: number) => bytes.writeBigUInt64LE(base + BigInt(target), offset); // Mach-O arm64 header + __DATA segment + __objc_classlist u32(0, 0xfeedfacf); u32(4, 0x0100000c); u32(16, 1); u32(20, 152); u32(32, 0x19); u32(36, 152); bytes.write("__DATA", 40); ptr(56, 0); bytes.writeBigUInt64LE(4096n, 64); bytes.writeBigUInt64LE(4096n, 80); u32(92, 5); u32(96, 1); bytes.write("__objc_classlist", 104); bytes.write("__DATA", 120); ptr(136, 0x180); bytes.writeBigUInt64LE(8n, 144); u32(152, 0x180); // class at 0x200 pointing to class_ro_t at 0x300 ptr(0x180, 0x200); ptr(0x200, 0x280); ptr(0x220, 0x300); ptr(0x2a0, 0x380); u32(0x308, 16); ptr(0x318, 0x450); // class name "Fixture" ptr(0x320, 0x480); // baseMethods pointing to method_list_t at 0x480 bytes.write("Fixture\0", 0x450); // method_list_t at 0x480: flags 24 (stride 24, absolute), count 1 u32(0x480, 24); u32(0x484, 1); ptr(0x488, 0x460); // selector pointer -> empty string "\0" ptr(0x490, 0x5c0); // types pointer ptr(0x498, 0); // imp pointer bytes.write("\0", 0x460); bytes.write("v16@0:8\0", 0x5c0); decodeAppleDispatchMetadata(bytes, 100, { path: "/fixture/app", sha256: "a".repeat(64), }); ``` 2. Run with Node / `tsx`: ```sh npx tsx repro_selector.mts ``` 3. Observe `ZodError` thrown during top-level metadata schema validation: ```json [ { "origin": "string", "code": "too_small", "minimum": 1, "inclusive": true, "path": [ "objc_classes", 0, "methods", 0, "selector" ], "message": "Too small: expected string to have >=1 characters" }, { "origin": "string", "code": "too_small", "minimum": 1, "inclusive": true, "path": [ "objc_dispatch_implementations", 0, "selector" ], "message": "Too small: expected string to have >=1 characters" } ] ``` ### Expected behavior An empty method selector should be rejected or reported inside the method/class decode boundary (e.g. throwing a `TypeError("Objective-C method selector is empty")` or skipping the empty selector), allowing per-class recovery to record the failure and preserve valid sibling classes and methods, returning a valid `ObjcSwiftMetadata` result. ### Impact and scope Any Mach-O binary with stripped, corrupted, or zeroed selector references in `method_list_t` crashes `decodeAppleDispatchMetadata` with an unhandled exception, wiping out the entire metadata inventory. ### REA version `8194695144045e0a950f2989190e7337890342f7` (6.5.0) ### Runtime and platform Node v24.13.0; Windows x64 ### Target and artifact context Mach-O arm64 binary; Objective-C metadata (`__objc_classlist` and `method_list_t`) ### Interface Other ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence In `src/native/AppleObjcDispatchFacets.ts:165-171`: ```ts const selector = read.string(selectorAddress); selectors.push(selector); result.objc_dispatch_implementations.push({ class_name: className, selector, method_type: methodType, ... ``` And in `readClass`: ```ts methods(read.pointer(ro + 32n), name, meta ? "class" : "instance"); result.objc_classes.push({ name, methods: result.objc_dispatch_implementations ... .map((item) => ({ selector: item.selector, ... })) ``` Both `objcDispatchImplementationSchema` (`src/domain/native/objcSwiftMetadata.ts:160`) and `objcMethodSchema` (`src/domain/native/objcSwiftMetadata.ts:26`) mandate `selector: z.string().min(1)`. When `read.string(selectorAddress)` returns `""`, the invalid selector is added unchecked and later triggers a fatal `ZodError` at `objcSwiftMetadataSchema.parse(result)`. ### Typed outcome or recovery expected The method failure should be recorded, and `decodeAppleDispatchMetadata` should return a typed `ObjcSwiftMetadata` object retaining all valid classes, ivars, protocols, and methods. ### Analysis or suspected cause `methods` helper in `AppleObjcDispatchFacets.ts` does not assert `selector.length > 0`. Adding entry-level validation allows `readClass`'s `try/catch` to isolate the defect to that class or method rather than escalating to a whole-analysis crash.
Author
Owner

@Abdooo2235 commented on GitHub (Oct 17, 2026):

@kevinjosh10 I reproduced a remaining recovery gap after the empty-selector validation already on main: a valid/empty/valid method list still discarded its class or category and the last method. Draft #4861 keeps those records and reports the malformed entry in the affected coverage facet. The four new recovery cases fail on the base and pass with the fix; 64 focused tests, static checks and docs checks pass locally. @morluto the PR includes the executed evidence and remaining platform limits; initial human readiness is still pending.

<!-- gh-comment-id:6238661984 --> @Abdooo2235 commented on GitHub (Oct 17, 2026): @kevinjosh10 I reproduced a remaining recovery gap after the empty-selector validation already on main: a valid/empty/valid method list still discarded its class or category and the last method. Draft #4861 keeps those records and reports the malformed entry in the affected coverage facet. The four new recovery cases fail on the base and pass with the fix; 64 focused tests, static checks and docs checks pass locally. @morluto the PR includes the executed evidence and remaining platform limits; initial human readiness is still pending.
Author
Owner

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

Checked on Linux: this is fixed on main by a70c42e03. The commit's message says "Refs #4534", which is why GitHub left this issue open.

  • src/native/AppleDispatchMetadata.test.ts passes on main 36d156ff0 on Node 22.23.1, 24.18.0 and 26.11.1 (52 tests).
  • On the commit's parent, 7 of 57 tests fail, including the empty class selector case, ZodError (too_small) at selector.
  • That case does not check that valid sibling classes and methods survive.

Thanks, kevinjosh10, for the clear report.

If nothing else is open here, this can be closed.

🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊

<!-- gh-comment-id:6254877434 --> @CassieFox23 commented on GitHub (Oct 18, 2026): Checked on Linux: this is fixed on main by a70c42e03. The commit's message says "Refs #4534", which is why GitHub left this issue open. - `src/native/AppleDispatchMetadata.test.ts` passes on main 36d156ff0 on Node 22.23.1, 24.18.0 and 26.11.1 (52 tests). - On the commit's parent, 7 of 57 tests fail, including the empty class selector case, `ZodError` (too_small) at `selector`. - That case does not check that valid sibling classes and methods survive. Thanks, kevinjosh10, for the clear report. If nothing else is open here, this can be closed. 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊
Author
Owner

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

Closing as fixed on main after reconciling this report with the landed owner/consumer changes.

  • a70c42e03: fix(native): recover malformed Objective-C names at decode boundaries. Byte fixtures reproduced the reported validation failures. Final focused 4-file run: 118 passed; npm run check passed. Six existing CLI property controls remain blocked before decoding by deep-provider auto-selection ambiguity, as exposed by improved assertion diagnostics (#4555). No Windows runtime or real malformed-application verification performed.

Evidence scope: this closure uses the landed source, regression coverage and validation already recorded in the linked commits/thread. No new local tests or native-provider reproduction were run during this backlog pass; previously documented platform and real-provider gaps still apply.

<!-- gh-comment-id:6256005257 --> @morluto commented on GitHub (Oct 18, 2026): <!-- rea-backlog-reconciliation-2026-10-19-4534 --> Closing as fixed on `main` after reconciling this report with the landed owner/consumer changes. - [a70c42e03](https://github.com/morluto/rea/commit/a70c42e03dc526e76f360ecec9f1002187ffb23e): fix(native): recover malformed Objective-C names at decode boundaries. Byte fixtures reproduced the reported validation failures. Final focused 4-file run: 118 passed; npm run check passed. Six existing CLI property controls remain blocked before decoding by deep-provider auto-selection ambiguity, as exposed by improved assertion diagnostics (#4555). No Windows runtime or real malformed-application verification performed. Evidence scope: this closure uses the landed source, regression coverage and validation already recorded in the linked commits/thread. No new local tests or native-provider reproduction were run during this backlog pass; previously documented platform and real-provider gaps still apply.
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#2360
No description provided.