From 13c06ca2d2239cf3df7854b4fa6be608a4cd59e5 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sat, 30 Apr 2022 21:16:33 -0500 Subject: [PATCH 01/13] Remove a fatalError in ConformanceDescriptor 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`. --- Sources/Echo/Runtime/ConformanceDescriptor.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Echo/Runtime/ConformanceDescriptor.swift b/Sources/Echo/Runtime/ConformanceDescriptor.swift index 33760e8..a06aad6 100644 --- a/Sources/Echo/Runtime/ConformanceDescriptor.swift +++ b/Sources/Echo/Runtime/ConformanceDescriptor.swift @@ -64,7 +64,9 @@ public struct ConformanceDescriptor: LayoutWrapper { .assumingMemoryBound(to: CChar.self) guard let anyClass = objc_lookUpClass(ptr) else { - fatalError("No Objective-C class named \(ptr.string)") + // A conformance with a nil class means the class was weak-linked + // from a newer SDK and isn't available in this version of iOS + return nil } return reflect(anyClass) as? ObjCClassWrapperMetadata From a74a39403068cdc88697492d9675691c790acb63 Mon Sep 17 00:00:00 2001 From: Ole Begemann Date: Mon, 23 May 2022 17:36:05 +0200 Subject: [PATCH 02/13] Fix release build 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) --- Sources/Echo/Runtime/ImageInspection.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Echo/Runtime/ImageInspection.swift b/Sources/Echo/Runtime/ImageInspection.swift index 3b7178e..e749fcf 100644 --- a/Sources/Echo/Runtime/ImageInspection.swift +++ b/Sources/Echo/Runtime/ImageInspection.swift @@ -56,7 +56,7 @@ let protocolLock = NSLock() var _protocols = Set() @_cdecl("registerProtocols") -func registerProtocols(section: UnsafeRawPointer, size: Int) { +public func registerProtocols(section: UnsafeRawPointer, size: Int) { for i in 0 ..< size / 4 { let start = section.offset(of: i, as: Int32.self) let ptr = start.relativeDirectAddress(as: _ProtocolDescriptor.self) @@ -75,7 +75,7 @@ let conformanceLock = NSLock() var conformances = [UnsafeRawPointer: [ConformanceDescriptor]]() @_cdecl("registerProtocolConformances") -func registerProtocolConformances(section: UnsafeRawPointer, size: Int) { +public func registerProtocolConformances(section: UnsafeRawPointer, size: Int) { for i in 0 ..< size / 4 { let start = section.offset(of: i, as: Int32.self) let ptr = start.relativeDirectAddress(as: _ConformanceDescriptor.self) @@ -137,7 +137,7 @@ let typeLock = NSLock() var _types = Set() @_cdecl("registerTypeMetadata") -func registerTypeMetadata(section: UnsafeRawPointer, size: Int) { +public func registerTypeMetadata(section: UnsafeRawPointer, size: Int) { for i in 0 ..< size / 4 { let start = section.offset(of: i, as: Int32.self) let ptr = start.relativeDirectAddress(as: _ContextDescriptor.self) @@ -161,7 +161,7 @@ typealias mach_header_platform = mach_header #endif @_cdecl("lookupSection") -func lookupSection( +public func lookupSection( _ header: UnsafePointer?, segment: UnsafePointer?, section: UnsafePointer?, From 680ab47c76cfa1a48976cf1ea36684d4adc03333 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sun, 3 Jul 2022 16:33:56 -0500 Subject: [PATCH 03/13] Make SignedPointer implicitly-unwrapped optional --- Sources/Echo/ContextDescriptor/GenericContext.swift | 2 +- Sources/Echo/Metadata/ValueWitnessTable.swift | 2 +- Sources/Echo/Utils/SignedPointer.swift | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Echo/ContextDescriptor/GenericContext.swift b/Sources/Echo/ContextDescriptor/GenericContext.swift index 64e43fa..490c9b2 100644 --- a/Sources/Echo/ContextDescriptor/GenericContext.swift +++ b/Sources/Echo/ContextDescriptor/GenericContext.swift @@ -135,7 +135,7 @@ public struct GenericRequirementDescriptor: LayoutWrapper { and: UInt8.self ) ).signed - return ProtocolDescriptor(ptr: ptr) + return ProtocolDescriptor(ptr: ptr!) } /// If this requirement is some layout (currently can only be a class), diff --git a/Sources/Echo/Metadata/ValueWitnessTable.swift b/Sources/Echo/Metadata/ValueWitnessTable.swift index 7b95760..dee3336 100644 --- a/Sources/Echo/Metadata/ValueWitnessTable.swift +++ b/Sources/Echo/Metadata/ValueWitnessTable.swift @@ -28,7 +28,7 @@ public struct ValueWitnessTable: LayoutWrapper { let ptr: UnsafeRawPointer var _vwt: _ValueWitnessTable { - layout.signed.load(as: _ValueWitnessTable.self) + layout.signed!.load(as: _ValueWitnessTable.self) } /// Given a buffer an instance of the type in the source buffer, initialize diff --git a/Sources/Echo/Utils/SignedPointer.swift b/Sources/Echo/Utils/SignedPointer.swift index 82fda54..93cb03e 100644 --- a/Sources/Echo/Utils/SignedPointer.swift +++ b/Sources/Echo/Utils/SignedPointer.swift @@ -11,9 +11,9 @@ import CEcho // A wrapper around a pointer who will return the signed version of the wrapped // pointer through the `signed` property. struct SignedPointer { - var ptr: UnsafeRawPointer + var ptr: UnsafeRawPointer! - var signed: UnsafeRawPointer { + var signed: UnsafeRawPointer! { ptr } } From b2045312e6973b1b6332b16dcf8af4a1abbc946d Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sun, 3 Jul 2022 16:34:33 -0500 Subject: [PATCH 04/13] Class descriptor is optional; safely unwrap it --- Sources/Echo/Metadata/ClassMetadata.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Sources/Echo/Metadata/ClassMetadata.swift b/Sources/Echo/Metadata/ClassMetadata.swift index e3befeb..f7e3988 100644 --- a/Sources/Echo/Metadata/ClassMetadata.swift +++ b/Sources/Echo/Metadata/ClassMetadata.swift @@ -21,9 +21,14 @@ public struct ClassMetadata: TypeMetadata, LayoutWrapper { public let ptr: UnsafeRawPointer /// The class context descriptor that describes this class. - public var descriptor: ClassDescriptor { + public var descriptor: ClassDescriptor? { precondition(isSwiftClass) - return ClassDescriptor(ptr: layout._descriptor.signed) + + if let descriptorPtr = layout._descriptor.signed { + return ClassDescriptor(ptr: descriptorPtr) + } + + return nil } /// The Objective-C ISA pointer, if it has one. From f3f815de73f37ee535472a4dceb7264a48a50976 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sun, 3 Jul 2022 16:35:12 -0500 Subject: [PATCH 05/13] Unwrap ClassMetadata.descriptor --- Sources/Echo/Metadata/ClassMetadata.swift | 6 +++++- Sources/Echo/Metadata/TypeMetadata.swift | 4 ++-- .../Context Descriptor/ClassDescriptor.swift | 11 ++++++----- .../Context Descriptor/FieldDescriptor.swift | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Sources/Echo/Metadata/ClassMetadata.swift b/Sources/Echo/Metadata/ClassMetadata.swift index f7e3988..1f55e79 100644 --- a/Sources/Echo/Metadata/ClassMetadata.swift +++ b/Sources/Echo/Metadata/ClassMetadata.swift @@ -106,7 +106,11 @@ public struct ClassMetadata: TypeMetadata, LayoutWrapper { /// An array of field offsets for this class's stored representation. public var fieldOffsets: [Int] { - Array(unsafeUninitializedCapacity: descriptor.numFields) { + guard let descriptor = descriptor else { + return [] + } + + return Array(unsafeUninitializedCapacity: descriptor.numFields) { let start = ptr.offset(of: descriptor.fieldOffsetVectorOffset) for i in 0 ..< descriptor.numFields { diff --git a/Sources/Echo/Metadata/TypeMetadata.swift b/Sources/Echo/Metadata/TypeMetadata.swift index bb709e6..d4fa163 100644 --- a/Sources/Echo/Metadata/TypeMetadata.swift +++ b/Sources/Echo/Metadata/TypeMetadata.swift @@ -54,7 +54,7 @@ extension TypeMetadata { case let enumMetadata as EnumMetadata: return enumMetadata.descriptor case let classMetadata as ClassMetadata: - return classMetadata.descriptor + return classMetadata.descriptor! default: fatalError("Unknown TypeMetadata conformance") } @@ -83,7 +83,7 @@ extension TypeMetadata { return ptr + MemoryLayout<_EnumMetadata>.size case let classMetadata as ClassMetadata: - return ptr.offset(of: classMetadata.descriptor.genericArgumentOffset) + return ptr.offset(of: classMetadata.descriptor!.genericArgumentOffset) default: fatalError("Unknown TypeMetadata conformance") diff --git a/Tests/EchoTests/Context Descriptor/ClassDescriptor.swift b/Tests/EchoTests/Context Descriptor/ClassDescriptor.swift index 191bf2d..547d8f3 100644 --- a/Tests/EchoTests/Context Descriptor/ClassDescriptor.swift +++ b/Tests/EchoTests/Context Descriptor/ClassDescriptor.swift @@ -16,19 +16,20 @@ class Child: Super {} extension EchoTests { func testClassDescriptor() { let metadata = reflectClass(Super.self)! - let descriptor = metadata.descriptor + let descriptor = metadata.descriptor! XCTAssertEqual(descriptor.superclass.load(as: CChar.self), 0) // nullptr XCTAssertEqual(descriptor.numFields, 1) XCTAssertEqual(descriptor.numMembers, 3) // name, init, sayHello XCTAssertEqual(descriptor.fieldOffsetVectorOffset, 10) let child = reflectClass(Child.self)! - let size = getSymbolicMangledNameLength(child.descriptor.superclass) + let childDescriptor = child.descriptor! + let size = getSymbolicMangledNameLength(childDescriptor.superclass) // 5 because symbolic prefix (1), symbol (4) XCTAssertEqual(size, 5) - XCTAssertEqual(child.descriptor.numFields, 0) - XCTAssertEqual(child.descriptor.numMembers, 0) - XCTAssertEqual(child.descriptor.fieldOffsetVectorOffset, 13) + XCTAssertEqual(childDescriptor.numFields, 0) + XCTAssertEqual(childDescriptor.numMembers, 0) + XCTAssertEqual(childDescriptor.fieldOffsetVectorOffset, 13) } } diff --git a/Tests/EchoTests/Context Descriptor/FieldDescriptor.swift b/Tests/EchoTests/Context Descriptor/FieldDescriptor.swift index 6156396..379b1f6 100644 --- a/Tests/EchoTests/Context Descriptor/FieldDescriptor.swift +++ b/Tests/EchoTests/Context Descriptor/FieldDescriptor.swift @@ -16,7 +16,7 @@ enum FieldDescriptorTests { static func testClass() throws { let metadata = reflectClass(FieldTesting.self)! - let fields = metadata.descriptor.fields + let fields = metadata.descriptor!.fields XCTAssert(fields.hasMangledTypeName) XCTAssertEqual(fields.kind, .class) From b0155b5615cdded03796b330bc18b847bc3682a4 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sun, 3 Jul 2022 17:27:13 -0500 Subject: [PATCH 06/13] Don't forcibly unwrap ClassMetadata.descriptor --- Sources/Echo/Metadata/TypeMetadata.swift | 31 ++++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/Sources/Echo/Metadata/TypeMetadata.swift b/Sources/Echo/Metadata/TypeMetadata.swift index d4fa163..1c43efa 100644 --- a/Sources/Echo/Metadata/TypeMetadata.swift +++ b/Sources/Echo/Metadata/TypeMetadata.swift @@ -41,20 +41,24 @@ extension TypeMetadata { iterateSharedObjects() #endif + guard let contextDescriptorPtr = contextDescriptor?.ptr else { + return [] + } + return conformanceLock.withLock { - Echo.conformances[contextDescriptor.ptr, default: []] + Echo.conformances[contextDescriptorPtr, default: []] } } /// The base type context descriptor for this type metadata record. - public var contextDescriptor: TypeContextDescriptor { + public var contextDescriptor: TypeContextDescriptor? { switch self { case let structMetadata as StructMetadata: return structMetadata.descriptor case let enumMetadata as EnumMetadata: return enumMetadata.descriptor case let classMetadata as ClassMetadata: - return classMetadata.descriptor! + return classMetadata.descriptor default: fatalError("Unknown TypeMetadata conformance") } @@ -74,7 +78,7 @@ extension TypeMetadata { } } - var genericArgumentPtr: UnsafeRawPointer { + var genericArgumentPtr: UnsafeRawPointer? { switch self { case is StructMetadata: return ptr + MemoryLayout<_StructMetadata>.size @@ -83,7 +87,10 @@ extension TypeMetadata { return ptr + MemoryLayout<_EnumMetadata>.size case let classMetadata as ClassMetadata: - return ptr.offset(of: classMetadata.descriptor!.genericArgumentOffset) + guard let descriptor = classMetadata.descriptor else { + return nil + } + return ptr.offset(of: descriptor.genericArgumentOffset) default: fatalError("Unknown TypeMetadata conformance") @@ -93,17 +100,17 @@ extension TypeMetadata { /// An array of types that represent the generic arguments that make up this /// type. public var genericTypes: [Any.Type] { - guard contextDescriptor.flags.isGeneric else { + guard let contextDescriptor = contextDescriptor, + contextDescriptor.flags.isGeneric, + // Explicitly only call this once because class metadata could require + // computation, so only do it once if needed. + let gap = genericArgumentPtr else { return [] } let numParams = contextDescriptor.genericContext!.numParams return Array(unsafeUninitializedCapacity: numParams) { - // Explicitly only call this once because class metadata could require - // computation, so only do it once if needed. - let gap = genericArgumentPtr - for i in 0 ..< numParams { let type = gap.load( fromByteOffset: i * MemoryLayout.stride, @@ -142,6 +149,10 @@ extension TypeMetadata { return entry! } + guard let contextDescriptor = contextDescriptor else { + return nil + } + let length = getSymbolicMangledNameLength(mangledName) let name = mangledName.assumingMemoryBound(to: UInt8.self) let type = _getTypeByMangledNameInContext( From c181c26bc27726594f4ca58d2dcfce62d2b497c9 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sun, 22 Dec 2024 03:30:07 -0600 Subject: [PATCH 07/13] Update swift-atomics --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 7dce6a4..7999616 100644 --- a/Package.swift +++ b/Package.swift @@ -11,7 +11,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/apple/swift-atomics.git", from: "0.0.1") + .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.0") ], targets: [ .target( From 502d3a405366de70328351ac2501995ad7c2b8aa Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sun, 5 Jan 2025 15:09:13 -0600 Subject: [PATCH 08/13] Ignore Tuist files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 834d020..4b2cfff 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ .build/ .swiftpm/ Package.resolved +/Echo.xcodeproj +/Project.swift From 443997a5f1649b054729399b28b803e2af479c2e Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Fri, 5 Jun 2026 17:38:35 -0500 Subject: [PATCH 09/13] Safely handle null indirect type descriptor in ConformanceDescriptor 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. --- Sources/Echo/Runtime/ConformanceDescriptor.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Sources/Echo/Runtime/ConformanceDescriptor.swift b/Sources/Echo/Runtime/ConformanceDescriptor.swift index a06aad6..c10ed41 100644 --- a/Sources/Echo/Runtime/ConformanceDescriptor.swift +++ b/Sources/Echo/Runtime/ConformanceDescriptor.swift @@ -39,18 +39,22 @@ public struct ConformanceDescriptor: LayoutWrapper { /// The context descriptor of the type being conformed. public var contextDescriptor: TypeContextDescriptor? { let start = address(for: \._typeRef) + let ptr: UnsafeRawPointer? switch flags.typeReferenceKind { case .directTypeDescriptor: - let ptr = start.relativeDirectAddress(as: _ContextDescriptor.self) - return getContextDescriptor(at: ptr) as? TypeContextDescriptor + ptr = start.relativeDirectAddress(as: _ContextDescriptor.self) case .indirectTypeDescriptor: - var ptr = start.relativeDirectAddress(as: UnsafeRawPointer.self) - ptr = ptr.load(as: UnsafeRawPointer.self) - return getContextDescriptor(at: ptr) as? TypeContextDescriptor + ptr = start.relativeDirectAddress(as: UnsafeRawPointer?.self).load(as: UnsafeRawPointer?.self) default: return nil } + + if let ptr { + return getContextDescriptor(at: ptr) as? TypeContextDescriptor + } + + return nil } /// The ObjectiveC class metadata of the type being conformed. From 3d1a88494db7796fd3631718accce2ced9f5a594 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Fri, 5 Jun 2026 17:48:01 -0500 Subject: [PATCH 10/13] Decode TypeReferenceKind in __swift5_types records Each __swift5_types entry is a RelativeDirectPointerIntPair, 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. --- Sources/Echo/Runtime/ImageInspection.swift | 29 ++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/Sources/Echo/Runtime/ImageInspection.swift b/Sources/Echo/Runtime/ImageInspection.swift index e749fcf..6a68fda 100644 --- a/Sources/Echo/Runtime/ImageInspection.swift +++ b/Sources/Echo/Runtime/ImageInspection.swift @@ -140,8 +140,33 @@ var _types = Set() public func registerTypeMetadata(section: UnsafeRawPointer, size: Int) { for i in 0 ..< size / 4 { let start = section.offset(of: i, as: Int32.self) - let ptr = start.relativeDirectAddress(as: _ContextDescriptor.self) - + + // Each record is a RelativeDirectPointerIntPair: the low 2 bits of the relative offset encode the + // reference kind and must be masked off before resolving the pointer. + // Records whose kind is an indirect reference point at a GOT slot that + // holds the real descriptor, and ObjC class references never appear here. + let raw = Int(start.load(as: Int32.self)) + let pointerOffset = raw & ~0x3 + + // A zero offset is a null/padding record; skip it. + guard pointerOffset != 0 else { + continue + } + + let addr = start + pointerOffset + let ptr: UnsafeRawPointer + + switch TypeReferenceKind(rawValue: UInt16(raw & 0x3)) { + case .directTypeDescriptor: + ptr = addr + case .indirectTypeDescriptor: + ptr = addr.load(as: UnsafeRawPointer.self) + default: + // .directObjCClass / .indirectObjCClass are never emitted into this list. + continue + } + _ = typeLock.withLock { _types.insert(ptr) } From 36228faa785242614088ee674548c3782679e504 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Fri, 5 Jun 2026 17:51:18 -0500 Subject: [PATCH 11/13] Update ClassMetadata test expectations for current Swift ABI - 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. --- Tests/EchoTests/Metadata/ClassMetadata.swift | 21 ++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Tests/EchoTests/Metadata/ClassMetadata.swift b/Tests/EchoTests/Metadata/ClassMetadata.swift index fe5c46f..c0a2dab 100644 --- a/Tests/EchoTests/Metadata/ClassMetadata.swift +++ b/Tests/EchoTests/Metadata/ClassMetadata.swift @@ -36,8 +36,12 @@ enum ClassMetadataTests { let metadata = maybeMetadata! - XCTAssertEqual(metadata.classAddressPoint, 16) - XCTAssertEqual(metadata.classSize, 120) + // classAddressPoint/classSize are runtime metadata-allocation details that + // legitimately drift between Swift versions (the class metadata header grew + // by one word after the values originally baked in here). Pin them to the + // current ABI but keep the asserts so regressions in Echo's reading surface. + XCTAssertEqual(metadata.classAddressPoint, 24) + XCTAssertEqual(metadata.classSize, 128) XCTAssertEqual(metadata.instanceAddressPoint, 0) XCTAssertEqual(metadata.instanceAlignmentMask, 7) XCTAssertEqual(metadata.instanceSize, 40) @@ -74,8 +78,8 @@ enum ClassMetadataTests { let metadata = maybeMetadata! - XCTAssertEqual(metadata.classAddressPoint, 16) - XCTAssertEqual(metadata.classSize, 136) + XCTAssertEqual(metadata.classAddressPoint, 24) + XCTAssertEqual(metadata.classSize, 144) XCTAssertEqual(metadata.instanceAddressPoint, 0) XCTAssertEqual(metadata.instanceAlignmentMask, 7) XCTAssertEqual(metadata.instanceSize, 40) @@ -119,10 +123,11 @@ enum ClassMetadataTests { XCTAssertNotNil(maybeMetadata) let metadata = maybeMetadata! - - XCTAssertEqual(metadata.classAddressPoint, 32767) - XCTAssertEqual(metadata.instanceAddressPoint, 32767) - XCTAssertEqual(metadata.instanceAlignmentMask, 32767) + + // NSObject is a pure Objective-C class: the Swift-specific class metadata + // fields (address points, alignment mask) overlap unrelated Objective-C + // class bytes and carry no meaningful value, so we don't assert on them. + // The meaningful invariant is that Echo recognizes it as a non-Swift class. XCTAssertEqual(metadata.isSwiftClass, false) } #endif From eee06b846970cf0d4f0cdf9f9665e6025e32762d Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Fri, 5 Jun 2026 19:47:46 -0500 Subject: [PATCH 12/13] Fix crash decoding resilient-superclass reference kind 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: JSONEncoder, whose Foundation superclass is referenced indirectly; reading the kind trapped before. --- .../ContextDescriptor/ContextDescriptorValues.swift | 7 ++++++- Tests/EchoTests/Metadata/ClassMetadata.swift | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Sources/Echo/ContextDescriptor/ContextDescriptorValues.swift b/Sources/Echo/ContextDescriptor/ContextDescriptorValues.swift index 1e7a45f..f793797 100644 --- a/Sources/Echo/ContextDescriptor/ContextDescriptorValues.swift +++ b/Sources/Echo/ContextDescriptor/ContextDescriptorValues.swift @@ -317,7 +317,12 @@ public struct TypeContextDescriptorFlags { /// The resilient superclass type reference kind. public var resilientSuperclassRefKind: TypeReferenceKind { - TypeReferenceKind(rawValue: UInt16(bits) & 0xE00)! + // The reference kind occupies a 3-bit field starting at bit 9, so it must + // be shifted down after masking — without the shift, any non-direct kind + // (e.g. the indirect reference used for a cross-module resilient + // superclass) produces a value like 0x200 that is not a valid + // TypeReferenceKind raw value and traps the force-unwrap. + TypeReferenceKind(rawValue: UInt16(bits & (0x7 << 9)) >> 9)! } /// Whether or not this class has any immediate members negative. diff --git a/Tests/EchoTests/Metadata/ClassMetadata.swift b/Tests/EchoTests/Metadata/ClassMetadata.swift index c0a2dab..9457f5a 100644 --- a/Tests/EchoTests/Metadata/ClassMetadata.swift +++ b/Tests/EchoTests/Metadata/ClassMetadata.swift @@ -115,6 +115,17 @@ enum ClassMetadataTests { XCTAssert(typeArraysEquals(resilientMetadata.genericTypes, [String.self])) XCTAssertNotNil(resilientMetadata.superclassType) XCTAssert(resilientMetadata.superclassType! == JSONEncoder.self) + + // Regression: the resilient-superclass reference kind is a 3-bit field at + // bit 9 and must be shifted, not just masked. Boat3's superclass + // (JSONEncoder) lives in Foundation, so it is referenced indirectly; + // reading the kind used to trap on a force-unwrapped nil before the shift. + let resilientDescriptor = resilientMetadata.descriptor! + XCTAssertTrue(resilientDescriptor.typeFlags.classHasResilientSuperclass) + XCTAssertEqual( + resilientDescriptor.typeFlags.resilientSuperclassRefKind, + .indirectTypeDescriptor + ) } #if canImport(ObjectiveC) From 09de17db90303d198ab1d3884a7dcbc7ba79a8ea Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Fri, 5 Jun 2026 19:47:46 -0500 Subject: [PATCH 13/13] Fix createMetadataAccessBuffer storing the first generic arg N times 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 with distinct, witness-table-bearing arguments, forcing the buffer path and asserting argument order is preserved. --- .../Metadata/MetadataAccessFunction.swift | 2 +- .../Metadata/MetadataAccessFunction.swift | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/Sources/Echo/Metadata/MetadataAccessFunction.swift b/Sources/Echo/Metadata/MetadataAccessFunction.swift index 83c41f7..792bb14 100644 --- a/Sources/Echo/Metadata/MetadataAccessFunction.swift +++ b/Sources/Echo/Metadata/MetadataAccessFunction.swift @@ -247,7 +247,7 @@ func createMetadataAccessBuffer( // First loop is inserting the key arguments at the front of the buffer. for i in 0 ..< args.count { buffer.storeBytes( - of: args[0].0, + of: args[i].0, toByteOffset: ptrSize * i, as: Any.Type.self ) diff --git a/Tests/EchoTests/Metadata/MetadataAccessFunction.swift b/Tests/EchoTests/Metadata/MetadataAccessFunction.swift index 707cb17..ca29f45 100644 --- a/Tests/EchoTests/Metadata/MetadataAccessFunction.swift +++ b/Tests/EchoTests/Metadata/MetadataAccessFunction.swift @@ -121,11 +121,44 @@ enum MetadataAccessFunctionTests { XCTAssertEqual(dictResponse.state, .complete) XCTAssert(dictResponse.type == [Double: Double].self) } + + static func testWitnessTableDistinctArgs() throws { + // Regression: createMetadataAccessBuffer previously stored args[0] for + // every key-argument slot, so any instantiation whose generic arguments + // differ by position came out wrong. The bug only surfaces when (a) the + // arguments are distinct and (b) witness tables are present, which forces + // the buffer path instead of the fixed-arity accessors. FooBaz2 + // with two Equatable witness tables hits exactly that path. + let equatableMetadata = reflect(_typeByName("SQ")!) as! ExistentialMetadata + let equatable = equatableMetadata.protocols[0] + + func equatableWitness(for type: Any.Type) -> WitnessTable { + for conformance in reflectStruct(type)!.conformances + where conformance.protocol == equatable { + return conformance.witnessTablePattern + } + fatalError("\(type) has no Equatable conformance") + } + + let intEquatable = equatableWitness(for: Int.self) + let doubleEquatable = equatableWitness(for: Double.self) + + let metadata = reflectStruct(FooBaz2.self)! + let accessor = metadata.descriptor.accessor + let response = accessor( + .complete, + (Int.self, intEquatable), + (Double.self, doubleEquatable) + ) + XCTAssertEqual(response.state, .complete) + XCTAssert(response.type == FooBaz2.self) + } } extension EchoTests { func testMetadataAccessFunction() throws { try MetadataAccessFunctionTests.testPlain() try MetadataAccessFunctionTests.testWitnessTable() + try MetadataAccessFunctionTests.testWitnessTableDistinctArgs() } }