[GH-ISSUE #4533] [Bug] Empty Objective-C ivar names escape entry recovery and invalidate dispatch metadata #2361

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

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

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 ivar_list_t includes an ivar record with an empty (\0) name causes decodeAppleDispatchMetadata to throw an unhandled ZodError (too_small: expected string to have >=1 characters at objc_ivars[0].name).

Instead of isolating the malformed ivar or reporting an entry failure for that class (which would allow siblings and other classes to survive), the entire metadata analysis aborts and discards all valid classes, protocols, methods, and categories decoded from the binary.

Reproduction

  1. Minimal in-memory Mach-O reproduction script (repro_ivar.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(0x330, 0x500); // ivar_list_t at 0x500
bytes.write("Fixture\0", 0x450);

// ivar_list_t: stride 32, count 1
u32(0x500, 32);
u32(0x504, 1);
ptr(0x508, 0x580); // offset pointer
ptr(0x510, 0x5a0); // name pointer -> empty string "\0"
ptr(0x518, 0x5c0); // type encoding pointer
u32(0x580, 8);
bytes.write("\0", 0x5a0);
bytes.write("i\0", 0x5c0);

decodeAppleDispatchMetadata(bytes, 100, {
  path: "/fixture/app",
  sha256: "a".repeat(64),
});
  1. Run with Node / tsx:
npx tsx repro_ivar.mts
  1. Observe ZodError thrown from objcSwiftMetadataSchema.parse:
[
  {
    "origin": "string",
    "code": "too_small",
    "minimum": 1,
    "inclusive": true,
    "path": [
      "objc_ivars",
      0,
      "name"
    ],
    "message": "Too small: expected string to have >=1 characters"
  }
]

Expected behavior

An empty or unreadable ivar name should be rejected inside the class/ivar decoding boundary (e.g., throwing a TypeError("Objective-C ivar has an empty name") or recording an ivar decode failure), allowing the per-class recovery to catch it, record the failure, and preserve valid sibling classes and metadata. The operation should return a valid ObjcSwiftMetadata result rather than crashing with an uncaught ZodError.

Impact and scope

Any Mach-O binary containing corrupted, obfuscated, or sentinel empty ivar records completely crashes decodeAppleDispatchMetadata during native dispatch analysis. All successfully recovered classes, protocols, and categories from the same target are lost.

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 ivar_list_t)

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

In src/native/AppleObjcDispatchFacets.ts:285-300:

const entry = ivarList + 8n + BigInt(index * stride);
...
result.objc_ivars.push({
  class_name: name,
  name: read.string(read.pointer(entry + 8n)),
  type_encoding: read.string(read.pointer(entry + 16n)),
  ...

read.string(read.pointer(entry + 8n)) is pushed directly into result.objc_ivars without verifying length > 0.
At line 428 of AppleObjcDispatchFacets.ts:

return objcSwiftMetadataSchema.parse(result);

objcIvarSchema in src/domain/native/objcSwiftMetadata.ts:136 enforces name: z.string().min(1). The schema rejection occurs after all class iterations finish, escaping the per-class try/catch in decodeObjcDispatchFacets.

Typed outcome or recovery expected

The malformed ivar or class should be reported in failures / issues, and decodeAppleDispatchMetadata should return a typed ObjcSwiftMetadata containing all other valid classes, protocols, and methods.

Analysis or suspected cause

readClass lacks a check for name.length === 0 on ivar names before pushing to result.objc_ivars. This contrasts with class names (if (name.length === 0) throw new TypeError(...) in AppleObjcDispatchFacets.ts:265), protocol names (AppleObjcProtocols.ts:73), and property names (AppleObjcProperties.ts:129), which all reject empty names inside their respective record boundaries.

Originally created by @kevinjosh10 on GitHub (Oct 16, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4533 ### 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 `ivar_list_t` includes an ivar record with an empty (`\0`) name causes `decodeAppleDispatchMetadata` to throw an unhandled `ZodError` (`too_small: expected string to have >=1 characters at objc_ivars[0].name`). Instead of isolating the malformed ivar or reporting an entry failure for that class (which would allow siblings and other classes to survive), the entire metadata analysis aborts and discards all valid classes, protocols, methods, and categories decoded from the binary. ### Reproduction 1. Minimal in-memory Mach-O reproduction script (`repro_ivar.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(0x330, 0x500); // ivar_list_t at 0x500 bytes.write("Fixture\0", 0x450); // ivar_list_t: stride 32, count 1 u32(0x500, 32); u32(0x504, 1); ptr(0x508, 0x580); // offset pointer ptr(0x510, 0x5a0); // name pointer -> empty string "\0" ptr(0x518, 0x5c0); // type encoding pointer u32(0x580, 8); bytes.write("\0", 0x5a0); bytes.write("i\0", 0x5c0); decodeAppleDispatchMetadata(bytes, 100, { path: "/fixture/app", sha256: "a".repeat(64), }); ``` 2. Run with Node / `tsx`: ```sh npx tsx repro_ivar.mts ``` 3. Observe `ZodError` thrown from `objcSwiftMetadataSchema.parse`: ```json [ { "origin": "string", "code": "too_small", "minimum": 1, "inclusive": true, "path": [ "objc_ivars", 0, "name" ], "message": "Too small: expected string to have >=1 characters" } ] ``` ### Expected behavior An empty or unreadable ivar name should be rejected inside the class/ivar decoding boundary (e.g., throwing a `TypeError("Objective-C ivar has an empty name")` or recording an ivar decode failure), allowing the per-class recovery to catch it, record the failure, and preserve valid sibling classes and metadata. The operation should return a valid `ObjcSwiftMetadata` result rather than crashing with an uncaught `ZodError`. ### Impact and scope Any Mach-O binary containing corrupted, obfuscated, or sentinel empty ivar records completely crashes `decodeAppleDispatchMetadata` during native dispatch analysis. All successfully recovered classes, protocols, and categories from the same target are lost. ### 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 `ivar_list_t`) ### Interface Other ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence In `src/native/AppleObjcDispatchFacets.ts:285-300`: ```ts const entry = ivarList + 8n + BigInt(index * stride); ... result.objc_ivars.push({ class_name: name, name: read.string(read.pointer(entry + 8n)), type_encoding: read.string(read.pointer(entry + 16n)), ... ``` `read.string(read.pointer(entry + 8n))` is pushed directly into `result.objc_ivars` without verifying `length > 0`. At line 428 of `AppleObjcDispatchFacets.ts`: ```ts return objcSwiftMetadataSchema.parse(result); ``` `objcIvarSchema` in `src/domain/native/objcSwiftMetadata.ts:136` enforces `name: z.string().min(1)`. The schema rejection occurs after all class iterations finish, escaping the per-class `try/catch` in `decodeObjcDispatchFacets`. ### Typed outcome or recovery expected The malformed ivar or class should be reported in `failures` / issues, and `decodeAppleDispatchMetadata` should return a typed `ObjcSwiftMetadata` containing all other valid classes, protocols, and methods. ### Analysis or suspected cause `readClass` lacks a check for `name.length === 0` on ivar names before pushing to `result.objc_ivars`. This contrasts with class names (`if (name.length === 0) throw new TypeError(...)` in `AppleObjcDispatchFacets.ts:265`), protocol names (`AppleObjcProtocols.ts:73`), and property names (`AppleObjcProperties.ts:129`), which all reject empty names inside their respective record boundaries.
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 #4533", 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 ivar name case, ZodError (too_small) at objc_ivars[0].name.

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:6254880707 --> @CassieFox23 commented on GitHub (Oct 18, 2026): Checked on Linux: this is fixed on main by a70c42e03. The commit's message says "Refs #4533", 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 ivar name case, `ZodError` (too_small) at `objc_ivars[0].name`. 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:6256005996 --> @morluto commented on GitHub (Oct 18, 2026): <!-- rea-backlog-reconciliation-2026-10-19-4533 --> 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#2361
No description provided.