[GH-ISSUE #5685] [Bug] Swift class vtable inspection reports null implementations as decoded at 0x0 #3436

Closed
opened 2026-10-31 00:40:28 +00:00 by bsakaguchi · 1 comment
Owner

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

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 reports a Swift class vtable slot with a zero implementation displacement as implementation_address: "0x0", decode: {"status": "decoded", "reason": null}. The same response correctly records the relative pointer's target as null. A single-class fixture still receives complete class-vtable coverage. Address zero contains its Mach-O header, not a method implementation.

Reproduction

Run from a built source checkout with locked dependencies on macOS, with Swift, Python 3, nm and jq available. This compiles a control dylib and changes only the synchronous method descriptor's four-byte implementation displacement to zero in a copy. Neither target is executed.

work_dir="$(mktemp -d)"
mkdir -p "$work_dir"/{home,cfg,data,cache,state,tmp}
cat > "$work_dir/fixture.swift" <<'SWIFT'
public class NullImplementation {
    public init() {}
    public func method() {}
}
SWIFT
swiftc -Onone -parse-as-library -module-name NullSlot -emit-library \
  -o "$work_dir/control.dylib" "$work_dir/fixture.swift"

python3 - "$work_dir/control.dylib" "$work_dir/null.dylib" <<'PY'
import struct, subprocess, sys
from pathlib import Path
image = bytearray(Path(sys.argv[1]).read_bytes())
assert struct.unpack_from('<I', image)[0] == 0xfeedfacf
symbols = subprocess.check_output(['nm', '-n', sys.argv[1]], text=True)
addresses = [int(row.split()[0], 16) for row in symbols.splitlines()
             if row.split()[-1].endswith('C6methodyyFTq')]
assert len(addresses) == 1
field = addresses[0] + 4
cursor = 32
for _ in range(struct.unpack_from('<I', image, 16)[0]):
    command, length = struct.unpack_from('<II', image, cursor)
    if command == 0x19:
        base, size, fileoff, filesize = struct.unpack_from('<QQQQ', image, cursor + 24)
        if base <= field and field + 4 <= base + filesize:
            offset = fileoff + field - base
            assert base == 0 and fileoff == 0
            assert struct.unpack_from('<I', image, offset - 4)[0] == 0x10
            assert struct.unpack_from('<i', image, offset)[0] != 0
            struct.pack_into('<i', image, offset, 0)
            print('zero implementation displacement at', hex(field))
            break
    cursor += length
else:
    raise AssertionError('method descriptor field not file-backed')
Path(sys.argv[2]).write_bytes(image)
PY

rea_isolated() {
  env -i PATH="$PATH" HOME="$work_dir/home" \
    XDG_CONFIG_HOME="$work_dir/cfg" XDG_DATA_HOME="$work_dir/data" \
    XDG_CACHE_HOME="$work_dir/cache" XDG_STATE_HOME="$work_dir/state" \
    TMPDIR="$work_dir/tmp" REA_LOG_LEVEL=silent node scripts/rea.mjs "$@"
}
rea_isolated inspect-native-dispatch-metadata "$work_dir/control.dylib" --json > "$work_dir/control.json"
rea_isolated inspect-native-dispatch-metadata "$work_dir/null.dylib" --json > "$work_dir/null.json"
jq '.normalized_result.result | {
  slots: [.swift_dispatch_slots[] | {slot_index, implementation_address, decode}],
  null_targets: [.relative_pointers[] | select(.displacement == 0) | {displacement, target}],
  coverage: [.coverage[] | select(.facet == "swift_class_vtable_descriptors") | {status}]
}' "$work_dir/control.json" "$work_dir/null.json"
xcrun dyld_info -fixups "$work_dir/null.dylib"

The independent verifier ran this reproduction, and the submitter replayed it successfully at the baseline below. Both control and null-pointer inspections exited 0 on each replay.

Expected behavior

Keep the slot and its descriptor location, but return a null implementation address with a partial decode explanation when the relative implementation pointer is null. Class-vtable coverage must reflect that unresolved implementation. Preserve the initializer and both unmodified control slots as decoded at their actual addresses.

Source-derived contract: supported class-vtable decoding promises exact virtual addresses and file offsets for supported non-generic, non-resilient classes. Partial-fact preservation requires retaining uncertainty, and the slot schema permits a null implementation address and partial status.

Impact and scope

P2: affected slots supply a false implementation address and can receive complete coverage, misleading static-analysis consumers. The trigger is a zero implementation displacement in an image whose executable segment includes address zero. Callers can reject 0x0 implementation addresses and treat affected vtable coverage as partial while retaining nonzero slots; relative-pointer records expose the null target for confirmation. The verifier also observed this input shape in an unmodified shipped framework, as described below. Production prevalence, MCP transport and other platforms were not tested. No security boundary crossing or target execution was observed.

REA version

rea-agents 6.7.0, source baseline 9d8e7803326a382cbbb3ffdb900f5e93e5442d68. Fetched origin/main at 60956c8c56475e7aaab9559f780908851677ced6: no later commits touch the cited expected/failure source files. This main recheck used source history; that newer revision was not rebuilt or executed.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1 arm64; Apple Swift 6.4 (swiftlang-6.4.0.34.1 clang-2100.3.34.1); Xcode 27.1 (27A9275). No Hopper/Ghidra or deep-analysis provider involved.

Target and artifact context

Self-built thin arm64 Mach-O dylib containing a non-generic, non-resilient Swift class with an initializer and one synchronous method. Its executable-protected __TEXT segment starts at virtual address/file offset zero. Default inspection options and isolated home/XDG directories were used. The reproduction modifies metadata in a copied artifact; no claim is made about executing that copy.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The submitter's fresh replay produced this projection for the patched image:

{
  "slots": [
    {"slot_index": 10, "implementation_address": "0x7d8", "decode": {"status": "decoded", "reason": null}},
    {"slot_index": 11, "implementation_address": "0x0", "decode": {"status": "decoded", "reason": null}}
  ],
  "null_targets": [{"displacement": 0, "target": {"address": null, "file_offset": null}}],
  "coverage": [{"status": "complete"}]
}

The control reports slot 10 at 0x7d8 and slot 11 at 0x834, both decoded, with complete coverage and no zero-displacement pointers. The independent patch parser located the implementation field at 0x994; dyld_info -fixups lists no fixup there. Addresses vary with compiler layout. Evidence provenance: observed; local verification: independently reproduced through the public CLI and replayed before submission.

The verifier additionally inspected an unmodified copy of Xcode 27.1's ToolsProtocolsSwiftExtensions.framework: PipeAsStringHandler slots 14 and 15 contain raw entries 1200000000000000 and 1300000000000000, with zero implementation displacements and no fixups at their implementation fields, yet REA reports both decoded at 0x0. This shipped-image observation is verifier evidence, not a new submitter replay. Its whole-image coverage is already partial for independent unsupported records; complete coverage is demonstrated by the single-class fixture.

Typed outcome or recovery expected

Return the successful metadata result with the null slot retained, implementation_address: null, a partial decode reason and partial class-vtable coverage. Proposed restoration check: replay the same control and patched inputs, require the patched method to remain unresolved, and preserve the initializer and both control implementations. Changing coverage counter units alone would not resolve this report.

Analysis or suspected cause

Source-derived reasoning: AppleSwiftDispatchFacets.ts:43-80 records a zero displacement as a null target but returns 0n. AppleSwiftVtables.ts:87-125 passes that value to the executable predicate. AppleDispatchMetadata.ts:177-183 accepts any address inside an executable segment, including zero here. The slot becomes decoded without an unresolved issue, so class-vtable coverage remains complete. Executable-segment membership does not establish a resolved non-null implementation.

Additional context

Prepared with AI assistance. Related #5681 concerns examined/decoded counters using different units; its restoration signal differs from this null-pointer failure. #4369/#4469 concern empty owner recovery; #5543 concerns Objective-C method omissions under record-budget exhaustion. No known-good version, introducing commit or successful fix was established.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5685 ### 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` reports a Swift class vtable slot with a zero implementation displacement as `implementation_address: "0x0"`, `decode: {"status": "decoded", "reason": null}`. The same response correctly records the relative pointer's target as null. A single-class fixture still receives complete class-vtable coverage. Address zero contains its Mach-O header, not a method implementation. ### Reproduction Run from a built source checkout with locked dependencies on macOS, with Swift, Python 3, nm and jq available. This compiles a control dylib and changes only the synchronous method descriptor's four-byte implementation displacement to zero in a copy. Neither target is executed. ```sh work_dir="$(mktemp -d)" mkdir -p "$work_dir"/{home,cfg,data,cache,state,tmp} cat > "$work_dir/fixture.swift" <<'SWIFT' public class NullImplementation { public init() {} public func method() {} } SWIFT swiftc -Onone -parse-as-library -module-name NullSlot -emit-library \ -o "$work_dir/control.dylib" "$work_dir/fixture.swift" python3 - "$work_dir/control.dylib" "$work_dir/null.dylib" <<'PY' import struct, subprocess, sys from pathlib import Path image = bytearray(Path(sys.argv[1]).read_bytes()) assert struct.unpack_from('<I', image)[0] == 0xfeedfacf symbols = subprocess.check_output(['nm', '-n', sys.argv[1]], text=True) addresses = [int(row.split()[0], 16) for row in symbols.splitlines() if row.split()[-1].endswith('C6methodyyFTq')] assert len(addresses) == 1 field = addresses[0] + 4 cursor = 32 for _ in range(struct.unpack_from('<I', image, 16)[0]): command, length = struct.unpack_from('<II', image, cursor) if command == 0x19: base, size, fileoff, filesize = struct.unpack_from('<QQQQ', image, cursor + 24) if base <= field and field + 4 <= base + filesize: offset = fileoff + field - base assert base == 0 and fileoff == 0 assert struct.unpack_from('<I', image, offset - 4)[0] == 0x10 assert struct.unpack_from('<i', image, offset)[0] != 0 struct.pack_into('<i', image, offset, 0) print('zero implementation displacement at', hex(field)) break cursor += length else: raise AssertionError('method descriptor field not file-backed') Path(sys.argv[2]).write_bytes(image) PY rea_isolated() { env -i PATH="$PATH" HOME="$work_dir/home" \ XDG_CONFIG_HOME="$work_dir/cfg" XDG_DATA_HOME="$work_dir/data" \ XDG_CACHE_HOME="$work_dir/cache" XDG_STATE_HOME="$work_dir/state" \ TMPDIR="$work_dir/tmp" REA_LOG_LEVEL=silent node scripts/rea.mjs "$@" } rea_isolated inspect-native-dispatch-metadata "$work_dir/control.dylib" --json > "$work_dir/control.json" rea_isolated inspect-native-dispatch-metadata "$work_dir/null.dylib" --json > "$work_dir/null.json" jq '.normalized_result.result | { slots: [.swift_dispatch_slots[] | {slot_index, implementation_address, decode}], null_targets: [.relative_pointers[] | select(.displacement == 0) | {displacement, target}], coverage: [.coverage[] | select(.facet == "swift_class_vtable_descriptors") | {status}] }' "$work_dir/control.json" "$work_dir/null.json" xcrun dyld_info -fixups "$work_dir/null.dylib" ``` The independent verifier ran this reproduction, and the submitter replayed it successfully at the baseline below. Both control and null-pointer inspections exited 0 on each replay. ### Expected behavior Keep the slot and its descriptor location, but return a null implementation address with a partial decode explanation when the relative implementation pointer is null. Class-vtable coverage must reflect that unresolved implementation. Preserve the initializer and both unmodified control slots as decoded at their actual addresses. Source-derived contract: [supported class-vtable decoding](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/docs/native-investigation.md#L419-L427) promises exact virtual addresses and file offsets for supported non-generic, non-resilient classes. [Partial-fact preservation](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/docs/tool-design.md#L48-L59) requires retaining uncertainty, and the [slot schema](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/domain/native/objcSwiftMetadata.ts#L209-L219) permits a null implementation address and partial status. ### Impact and scope P2: affected slots supply a false implementation address and can receive complete coverage, misleading static-analysis consumers. The trigger is a zero implementation displacement in an image whose executable segment includes address zero. Callers can reject `0x0` implementation addresses and treat affected vtable coverage as partial while retaining nonzero slots; relative-pointer records expose the null target for confirmation. The verifier also observed this input shape in an unmodified shipped framework, as described below. Production prevalence, MCP transport and other platforms were not tested. No security boundary crossing or target execution was observed. ### REA version `rea-agents` 6.7.0, source baseline `9d8e7803326a382cbbb3ffdb900f5e93e5442d68`. Fetched `origin/main` at `60956c8c56475e7aaab9559f780908851677ced6`: no later commits touch the cited expected/failure source files. This main recheck used source history; that newer revision was not rebuilt or executed. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1 arm64; Apple Swift 6.4 (swiftlang-6.4.0.34.1 clang-2100.3.34.1); Xcode 27.1 (27A9275). No Hopper/Ghidra or deep-analysis provider involved. ### Target and artifact context Self-built thin arm64 Mach-O dylib containing a non-generic, non-resilient Swift class with an initializer and one synchronous method. Its executable-protected `__TEXT` segment starts at virtual address/file offset zero. Default inspection options and isolated home/XDG directories were used. The reproduction modifies metadata in a copied artifact; no claim is made about executing that copy. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The submitter's fresh replay produced this projection for the patched image: ```json { "slots": [ {"slot_index": 10, "implementation_address": "0x7d8", "decode": {"status": "decoded", "reason": null}}, {"slot_index": 11, "implementation_address": "0x0", "decode": {"status": "decoded", "reason": null}} ], "null_targets": [{"displacement": 0, "target": {"address": null, "file_offset": null}}], "coverage": [{"status": "complete"}] } ``` The control reports slot 10 at `0x7d8` and slot 11 at `0x834`, both decoded, with complete coverage and no zero-displacement pointers. The independent patch parser located the implementation field at `0x994`; `dyld_info -fixups` lists no fixup there. Addresses vary with compiler layout. Evidence provenance: observed; local verification: independently reproduced through the public CLI and replayed before submission. The verifier additionally inspected an unmodified copy of Xcode 27.1's `ToolsProtocolsSwiftExtensions.framework`: `PipeAsStringHandler` slots 14 and 15 contain raw entries `1200000000000000` and `1300000000000000`, with zero implementation displacements and no fixups at their implementation fields, yet REA reports both decoded at `0x0`. This shipped-image observation is verifier evidence, not a new submitter replay. Its whole-image coverage is already partial for independent unsupported records; complete coverage is demonstrated by the single-class fixture. ### Typed outcome or recovery expected Return the successful metadata result with the null slot retained, `implementation_address: null`, a partial decode reason and partial class-vtable coverage. Proposed restoration check: replay the same control and patched inputs, require the patched method to remain unresolved, and preserve the initializer and both control implementations. Changing coverage counter units alone would not resolve this report. ### Analysis or suspected cause Source-derived reasoning: [AppleSwiftDispatchFacets.ts:43-80](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/native/AppleSwiftDispatchFacets.ts#L43-L80) records a zero displacement as a null target but returns `0n`. [AppleSwiftVtables.ts:87-125](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/native/AppleSwiftVtables.ts#L87-L125) passes that value to the executable predicate. [AppleDispatchMetadata.ts:177-183](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/native/AppleDispatchMetadata.ts#L177-L183) accepts any address inside an executable segment, including zero here. The slot becomes decoded without an unresolved issue, so [class-vtable coverage](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/native/AppleDispatchCoverage.ts#L212-L235) remains complete. Executable-segment membership does not establish a resolved non-null implementation. ### Additional context Prepared with AI assistance. Related #5681 concerns examined/decoded counters using different units; its restoration signal differs from this null-pointer failure. #4369/#4469 concern empty owner recovery; #5543 concerns Objective-C method omissions under record-budget exhaustion. No known-good version, introducing commit or successful fix was established.
Author
Owner

@matbrgz commented on GitHub (Oct 20, 2026):

Cross-referencing with canonical issue #4368 ([Bug] Empty Swift conformance names escape entry recovery and invalidate dispatch metadata) and sibling Swift dispatch metadata reports #5424, #5529, #5543, #5681, #5682, #5683.

Consolidated under Cluster AZ (Native Dispatch Metadata Inspection & Provider Fallbacks) in tracker #4330.

<!-- gh-comment-id:6303857191 --> @matbrgz commented on GitHub (Oct 20, 2026): Cross-referencing with canonical issue #4368 (`[Bug] Empty Swift conformance names escape entry recovery and invalidate dispatch metadata`) and sibling Swift dispatch metadata reports #5424, #5529, #5543, #5681, #5682, #5683. Consolidated under **Cluster AZ (Native Dispatch Metadata Inspection & Provider Fallbacks)** in tracker #4330.
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#3436
No description provided.