[GH-ISSUE #6000] [Bug] Kind-3 Swift conformance drops a decoded rebase target at address zero #3669

Closed
opened 2026-10-31 00:42:53 +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/6000

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 public inspect-native-dispatch-metadata CLI drops a known kind-3 conformance target when its indirect cell has a decoded legacy rebase to virtual address zero. It emits type_reference_target: { address: null, file_offset: null } even though Apple’s dyld_info and REA’s fixup parser both resolve the cell as a rebase to 0x0. The result succeeds and retains the partial runtime-lookup explanation.

This was executed with a linker-produced dylib containing deliberately assembled conformance metadata, without a mocked pointer resolver.

Reproduction

Run this executed driver from a built source checkout with locked dependencies. Save it outside the checkout, set REPRO_ROOT to an existing disposable writable directory, then invoke it with node. It compiles and inspects an arm64 dylib and two control copies; it never executes them. Its assertions characterize the current incorrect null projection.

import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
import assert from 'node:assert/strict';
const repo=process.cwd();
const root=await mkdtemp(join(process.env.REPRO_ROOT,'swift-zero-'));
const assembly=`.section __TEXT,__swift5_proto,regular,no_dead_strip
.p2align 2
.long Lconformance - .
.section __TEXT,__const
.p2align 2
Lconformance:
.long Lprotocol - .
.long Lclassref - .
.long 0
.long 24
Lprotocol:
.long 3
.long 0
.long Lname - .
.long 0
.long 0
.long 0
Lname:
.asciz "LocalProto"
.section __DATA,__data
.p2align 3
Lclassref:
.quad ___dso_handle
`;
const run=(command,args,options={})=>{
 const result=spawnSync(command,args,{encoding:'utf8',timeout:45000,...options});
 if(result.error)throw result.error;
 assert.equal(result.status,0,result.stdout+result.stderr);return result;
};
try {
 await writeFile(join(root,'fixture.s'),assembly);
 run('xcrun',['clang','-dynamiclib','-Wl,-no_fixup_chains','-o',join(root,'fixture.dylib'),join(root,'fixture.s')]);
 const fixups=run('xcrun',['dyld_info','-fixups',join(root,'fixture.dylib')]);console.log(fixups.stdout);
 const bytes=await readFile(join(root,'fixture.dylib'));
 const {parseMachoLayout}=await import(join(repo,'dist/native/AppleMachoSelection.js'));
 const {parsePointerFixups}=await import(join(repo,'dist/native/AppleMachoFixups.js'));
 const layout=parseMachoLayout(bytes,'arm64');
 const section=layout.sections.find(x=>x.name==='__data');
 const cell=layout.offset(section.address,8);
 const decoded=parsePointerFixups(bytes,layout.fixups,layout.segments,layout.baseAddress).decode(section.address,bytes.readBigUInt64LE(cell));
 assert.deepEqual(decoded,{kind:'rebase',target:0n});
 console.log('REA fixup: rebase, target 0x0');
 const env={PATH:process.env.PATH,REA_LOG_LEVEL:'silent'};
 for(const key of ['HOME','XDG_CONFIG_HOME','XDG_DATA_HOME','XDG_CACHE_HOME','XDG_STATE_HOME','TMPDIR']){env[key]=join(root,key.toLowerCase());await mkdir(env[key]);}
 for(const label of ['zero','nonzero','raw-zero']) {
  const b=Buffer.from(bytes);
  if(label==='nonzero')b.writeBigUInt64LE(0x100n,cell);
  if(label==='raw-zero'){
   let at=32;
   for(let n=0;n<b.readUInt32LE(16);n++){if(b.readUInt32LE(at)===0x80000022)b.writeUInt32LE(0,at+12);at+=b.readUInt32LE(at+4);}
  }
  const path=join(root,label+'.dylib');await writeFile(path,b);
  const result=run(process.execPath,[join(repo,'scripts/rea.mjs'),'inspect-native-dispatch-metadata',path,'--json'],{env});
  assert.equal(result.stderr,'');
  const row=JSON.parse(result.stdout).normalized_result.result.swift_conformances[0];
  assert.equal(row.type_reference_kind,3);
  assert.equal(row.type_reference_target.address,label==='nonzero'?'0x100':null);
  console.log(label,JSON.stringify({type_reference_kind:row.type_reference_kind,target:row.type_reference_target,decode:row.decode}));
 }
}finally{await rm(root,{recursive:true,force:true});}

Expected behavior

Retain type_reference_target: { address: "0x0", file_offset: 0 } for this decoded rebase. Keep the partial runtime-lookup status and nullable class name. The raw-zero control should remain null.

The documented rule explicitly preserves decoded rebases to zero and distinguishes them from unfixed nullable zero. Kind 3 retains its class-reference address even when runtime class/name recovery remains partial. The neighboring kind-1 decoder and shared relative reader preserve this distinction.

Impact and scope

P2: a known decoded address is lost from a partial metadata record. The relative field still identifies the indirect cell, and Apple’s dyld_info recovers its rebase target, providing a demonstrated manual workaround. No crash, loss of sibling records, target execution or security-boundary crossing was observed.

The fixture proves that a real linker-produced Mach-O can contain a rebase to zero. Its assembled kind-3 reference intentionally targets ___dso_handle, the Mach-O header, not a valid Objective-C class object. This report therefore establishes address/provenance loss on a partial metadata input; it does not claim a valid live class at zero, a successful runtime class lookup, natural Swift compiler emission of this conformance, or production prevalence.

REA version

rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e; prepared built runtime. Fetched main c538a843cb49f7156f86beb3b21501e03255af9a leaves the cited decoder, fixup parser and contract unchanged; no later-revision runtime replay is claimed.

Runtime and platform

macOS 27.0.1 arm64; Node.js v26.12.0; Apple clang 21.0.0 (clang-2100.3.34.2). Repository Node pin: 24.18.0. Native static byte reader and real Apple linker/dyld_info; no Hopper, Ghidra or simulated provider.

Target and artifact context

Self-assembled thin arm64 Mach-O dylib with __swift5_proto, a local protocol descriptor, kind-3 flags 24, and an indirect __DATA pointer to ___dso_handle. Legacy LC_DYLD_INFO rebase emitted using -no_fixup_chains. Nonzero/raw-zero control copies change only the pointer value or rebase range; they are inspected statically without renewing signatures or executing code.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed Apple tool output:

segment   section   address       type    target
__DATA    __data    0x00004000    rebase  0x00000000

REA’s production fixup parser independently returns { kind: "rebase", target: 0n } for that cell. Public CLI results (all exit 0, empty stderr):

zero:     kind 3, target {address:null,file_offset:null}
nonzero:  kind 3, target {address:"0x100",file_offset:256}
raw-zero: kind 3, target {address:null,file_offset:null}

All retain decode.status: "partial", reason Indirect Objective-C class metadata requires runtime lookup. The original zero image has complete pointer-fixup coverage with one stream examined/decoded and reason LC_DYLD_INFO rebase opcodes. Address zero maps to file offset 0 in this image. The reduced driver repeated the initial three-case probe successfully.

Typed outcome or recovery expected

A known rebase to zero remains an address: { address: "0x0", file_offset: 0 } here, or a null file offset if unmapped. type_binding remains null; class name/runtime recovery may remain partial. Only an unfixed raw zero should collapse to a nullable absent target.

Analysis or suspected cause

Source-derived: decodeIndirectObjcClass combines raw and rebase cases and applies pointer.target === 0n ? null : pointer.target to both. The conformance projection then publishes null. The legacy fixup reader already preserves the decoded zero correctly. Restrict the null conversion to the raw case while retaining the runtime limitation.

Additional context

Distinct from #5738 (inline kind-2 class-name strings), #5683 (external nominal descriptor binding retention), #4368 and PRs #4429/#4472 (empty-name whole-result failure), #5685 (unfixed null class-vtable implementation), and #1499 (legacy rebase coverage). This case uses the correct type-kind bits 3–5 and nonempty protocol name. No known-good revision or repaired-behavior run was established.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6000 ### 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 public `inspect-native-dispatch-metadata` CLI drops a known kind-3 conformance target when its indirect cell has a decoded legacy rebase to virtual address zero. It emits `type_reference_target: { address: null, file_offset: null }` even though Apple’s `dyld_info` and REA’s fixup parser both resolve the cell as a rebase to `0x0`. The result succeeds and retains the partial runtime-lookup explanation. This was executed with a linker-produced dylib containing deliberately assembled conformance metadata, without a mocked pointer resolver. ### Reproduction Run this executed driver from a built source checkout with locked dependencies. Save it outside the checkout, set `REPRO_ROOT` to an existing disposable writable directory, then invoke it with `node`. It compiles and inspects an arm64 dylib and two control copies; it never executes them. Its assertions characterize the current incorrect null projection. ```js import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises'; import { join } from 'node:path'; import { spawnSync } from 'node:child_process'; import assert from 'node:assert/strict'; const repo=process.cwd(); const root=await mkdtemp(join(process.env.REPRO_ROOT,'swift-zero-')); const assembly=`.section __TEXT,__swift5_proto,regular,no_dead_strip .p2align 2 .long Lconformance - . .section __TEXT,__const .p2align 2 Lconformance: .long Lprotocol - . .long Lclassref - . .long 0 .long 24 Lprotocol: .long 3 .long 0 .long Lname - . .long 0 .long 0 .long 0 Lname: .asciz "LocalProto" .section __DATA,__data .p2align 3 Lclassref: .quad ___dso_handle `; const run=(command,args,options={})=>{ const result=spawnSync(command,args,{encoding:'utf8',timeout:45000,...options}); if(result.error)throw result.error; assert.equal(result.status,0,result.stdout+result.stderr);return result; }; try { await writeFile(join(root,'fixture.s'),assembly); run('xcrun',['clang','-dynamiclib','-Wl,-no_fixup_chains','-o',join(root,'fixture.dylib'),join(root,'fixture.s')]); const fixups=run('xcrun',['dyld_info','-fixups',join(root,'fixture.dylib')]);console.log(fixups.stdout); const bytes=await readFile(join(root,'fixture.dylib')); const {parseMachoLayout}=await import(join(repo,'dist/native/AppleMachoSelection.js')); const {parsePointerFixups}=await import(join(repo,'dist/native/AppleMachoFixups.js')); const layout=parseMachoLayout(bytes,'arm64'); const section=layout.sections.find(x=>x.name==='__data'); const cell=layout.offset(section.address,8); const decoded=parsePointerFixups(bytes,layout.fixups,layout.segments,layout.baseAddress).decode(section.address,bytes.readBigUInt64LE(cell)); assert.deepEqual(decoded,{kind:'rebase',target:0n}); console.log('REA fixup: rebase, target 0x0'); const env={PATH:process.env.PATH,REA_LOG_LEVEL:'silent'}; for(const key of ['HOME','XDG_CONFIG_HOME','XDG_DATA_HOME','XDG_CACHE_HOME','XDG_STATE_HOME','TMPDIR']){env[key]=join(root,key.toLowerCase());await mkdir(env[key]);} for(const label of ['zero','nonzero','raw-zero']) { const b=Buffer.from(bytes); if(label==='nonzero')b.writeBigUInt64LE(0x100n,cell); if(label==='raw-zero'){ let at=32; for(let n=0;n<b.readUInt32LE(16);n++){if(b.readUInt32LE(at)===0x80000022)b.writeUInt32LE(0,at+12);at+=b.readUInt32LE(at+4);} } const path=join(root,label+'.dylib');await writeFile(path,b); const result=run(process.execPath,[join(repo,'scripts/rea.mjs'),'inspect-native-dispatch-metadata',path,'--json'],{env}); assert.equal(result.stderr,''); const row=JSON.parse(result.stdout).normalized_result.result.swift_conformances[0]; assert.equal(row.type_reference_kind,3); assert.equal(row.type_reference_target.address,label==='nonzero'?'0x100':null); console.log(label,JSON.stringify({type_reference_kind:row.type_reference_kind,target:row.type_reference_target,decode:row.decode})); } }finally{await rm(root,{recursive:true,force:true});} ``` ### Expected behavior Retain `type_reference_target: { address: "0x0", file_offset: 0 }` for this decoded rebase. Keep the partial runtime-lookup status and nullable class name. The raw-zero control should remain null. [The documented rule](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/native-investigation.md#L444-L460) explicitly preserves decoded rebases to zero and distinguishes them from unfixed nullable zero. Kind 3 retains its class-reference address even when runtime class/name recovery remains partial. [The neighboring kind-1 decoder](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/AppleSwiftConformanceTypeReference.ts#L95-L107) and [shared relative reader](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/AppleSwiftDispatchFacets.ts#L279-L283) preserve this distinction. ### Impact and scope P2: a known decoded address is lost from a partial metadata record. The relative field still identifies the indirect cell, and Apple’s `dyld_info` recovers its rebase target, providing a demonstrated manual workaround. No crash, loss of sibling records, target execution or security-boundary crossing was observed. The fixture proves that a real linker-produced Mach-O can contain a rebase to zero. Its assembled kind-3 reference intentionally targets `___dso_handle`, the Mach-O header, not a valid Objective-C class object. This report therefore establishes address/provenance loss on a partial metadata input; it does not claim a valid live class at zero, a successful runtime class lookup, natural Swift compiler emission of this conformance, or production prevalence. ### REA version `rea-agents` 6.7.0; source revision `52415975942a4b9c53a817915b313ccc404f280e`; prepared built runtime. Fetched main `c538a843cb49f7156f86beb3b21501e03255af9a` leaves the cited decoder, fixup parser and contract unchanged; no later-revision runtime replay is claimed. ### Runtime and platform macOS 27.0.1 arm64; Node.js v26.12.0; Apple clang 21.0.0 (clang-2100.3.34.2). Repository Node pin: 24.18.0. Native static byte reader and real Apple linker/dyld_info; no Hopper, Ghidra or simulated provider. ### Target and artifact context Self-assembled thin arm64 Mach-O dylib with __swift5_proto, a local protocol descriptor, kind-3 flags 24, and an indirect __DATA pointer to ___dso_handle. Legacy LC_DYLD_INFO rebase emitted using `-no_fixup_chains`. Nonzero/raw-zero control copies change only the pointer value or rebase range; they are inspected statically without renewing signatures or executing code. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed Apple tool output: ```text segment section address type target __DATA __data 0x00004000 rebase 0x00000000 ``` REA’s production fixup parser independently returns `{ kind: "rebase", target: 0n }` for that cell. Public CLI results (all exit 0, empty stderr): ```text zero: kind 3, target {address:null,file_offset:null} nonzero: kind 3, target {address:"0x100",file_offset:256} raw-zero: kind 3, target {address:null,file_offset:null} ``` All retain `decode.status: "partial"`, reason `Indirect Objective-C class metadata requires runtime lookup`. The original zero image has complete pointer-fixup coverage with one stream examined/decoded and reason `LC_DYLD_INFO rebase opcodes`. Address zero maps to file offset 0 in this image. The reduced driver repeated the initial three-case probe successfully. ### Typed outcome or recovery expected A known rebase to zero remains an address: `{ address: "0x0", file_offset: 0 }` here, or a null file offset if unmapped. `type_binding` remains null; class name/runtime recovery may remain partial. Only an unfixed raw zero should collapse to a nullable absent target. ### Analysis or suspected cause Source-derived: [decodeIndirectObjcClass](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/AppleSwiftConformanceTypeReference.ts#L218-L223) combines raw and rebase cases and applies `pointer.target === 0n ? null : pointer.target` to both. [The conformance projection](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/AppleSwiftDispatchFacets.ts#L405-L412) then publishes null. [The legacy fixup reader](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/AppleMachoFixups.ts#L812-L844) already preserves the decoded zero correctly. Restrict the null conversion to the raw case while retaining the runtime limitation. ### Additional context Distinct from #5738 (inline kind-2 class-name strings), #5683 (external nominal descriptor binding retention), #4368 and PRs #4429/#4472 (empty-name whole-result failure), #5685 (unfixed null class-vtable implementation), and #1499 (legacy rebase coverage). This case uses the correct type-kind bits 3–5 and nonempty protocol name. No known-good revision or repaired-behavior run was established.
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#3669
No description provided.