Skip to content

Fix crashes & ABI drift to build on current Swift#74

Open
NSExceptional wants to merge 13 commits into
Azoy:mainfrom
NSExceptional:modernize-for-current-swift
Open

Fix crashes & ABI drift to build on current Swift#74
NSExceptional wants to merge 13 commits into
Azoy:mainfrom
NSExceptional:modernize-for-current-swift

Conversation

@NSExceptional

@NSExceptional NSExceptional commented Jun 6, 2026

Copy link
Copy Markdown

Important

Must merge before #75 and #76

Summary

Echo no longer builds and runs cleanly on current Swift toolchains. This first PR restores a green build and test suite by fixing the crashes and ABI drift that accumulated since the library was last updated — each with a regression test that fails on the prior code.

Changes

  • Null indirect type descriptors in ConformanceDescriptor.contextDescriptor — a conformance whose type was weak-linked from a newer SDK now returns nil instead of trapping a non-optional load.
  • __swift5_types decoding — each record is a RelativeDirectPointerIntPair<ContextDescriptor, TypeReferenceKind>; the low two bits (the reference kind) weren't masked off, so indirect records (emitted whenever Foundation is imported) produced a misaligned pointer and crashed getContextDescriptor with "load from misaligned raw pointer". This was aborting the entire test suite.
  • resilientSuperclassRefKind — the 3-bit field at bit 9 was masked but never shifted down, so any class with a cross-module resilient superclass (e.g. a Foundation base class) trapped a force-unwrapped nil.
  • MetadataAccessFunction buffercreateMetadataAccessBuffer stored args[0] into every key-argument slot instead of args[i], silently corrupting any buffer-path generic metadata instantiation.
  • ClassMetadata test expectations updated for the current class-metadata ABI (the metadata header grew by one word).

Testing

swift test is green (23 tests) on Swift 6.1.2 / macOS. Every fix above has a dedicated regression test.

NSExceptional and others added 13 commits April 30, 2022 21:16
A conformance's class will be nil when the conformance refers to a class that is only present in a newer SDK. For example, SDKAdImpression is only available on iOS 14.5. An app that uses SDKAdImpression would wrap it in `if @available` guards. While the conformance and class name is still present in the binary when it runs on iOS < 14.5, the class will be `nil`.
Make some Swift functions public so that the linker can see them when linking CEcho.

If they're not public, release builds would fail with undefined symbols errors:

Undefined symbols for architecture arm64:
  "_lookupSection", referenced from:
      __loadImageFunc in CEcho.o
  "_registerProtocolConformances", referenced from:
      __loadImageFunc in CEcho.o
  "_registerProtocols", referenced from:
      __loadImageFunc in CEcho.o
  "_registerTypeMetadata", referenced from:
      __loadImageFunc in CEcho.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
The .indirectTypeDescriptor case loaded the GOT slot as a non-optional
UnsafeRawPointer, which traps when the slot holds null (e.g. a type that
was weak-linked from a newer SDK and isn't present at runtime). Load it
as an optional and return nil instead of crashing, matching how the
.directObjCClass case already guards weak-linked classes.
Each __swift5_types entry is a RelativeDirectPointerIntPair<ContextDescriptor,
TypeReferenceKind>, packing the reference kind into the low 2 bits of the
relative offset. registerTypeMetadata treated every entry as a plain direct
relative pointer, so indirect type-descriptor records (emitted for cross-module
type references, e.g. when Foundation is imported) produced a misaligned
pointer and crashed getContextDescriptor with "load from misaligned raw
pointer". Mask off the kind bits and dereference the GOT slot for indirect
records, matching ConformanceDescriptor's type-reference handling.
- classAddressPoint/classSize grew by one word in newer Swift's class
  metadata header; Echo reads them correctly (its _ClassMetadata layout
  matches TargetClassMetadata, and instanceSize/fieldOffsets still match),
  so update the stale constants (16->24, 120->128, 136->144).
- Drop the NSObject classAddressPoint/instanceAddressPoint/alignmentMask
  assertions: reading Swift class-metadata fields off a pure Objective-C
  class inspects unrelated bytes (the old 32767 sentinel was meaningless).
  Keep the isSwiftClass == false invariant.
TypeContextDescriptorFlags.resilientSuperclassRefKind masked the 3-bit
field at bit 9 (& 0xE00) but never shifted it down, so any non-direct
kind produced a value like 0x200 that isn't a valid TypeReferenceKind
raw value and trapped the force-unwrap. This crashes on any class with a
resilient superclass referenced indirectly -- i.e. a cross-module
resilient superclass such as a Foundation base class. Shift the masked
field down by 9 (matching the sibling decode in RuntimeValues).

Regression test reflects Boat3<String>: JSONEncoder, whose Foundation
superclass is referenced indirectly; reading the kind trapped before.
The key-argument loop stored args[0].0 for every slot instead of
args[i].0, so instantiating a generic type via the buffer path (4+
arguments, or 2-3 arguments with witness tables) wrote the first type
argument into every position -- silent metadata corruption. Existing
tests missed it because they instantiated with identical arguments
(Double, Double), where storing args[0] repeatedly is accidentally
correct.

Regression test instantiates FooBaz2<Int, Double> with distinct,
witness-table-bearing arguments, forcing the buffer path and asserting
argument order is preserved.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants