Skip to content

Invalid offset calculation for some ivars #113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
xavierjurado opened this issue Apr 23, 2025 · 1 comment
Open

Invalid offset calculation for some ivars #113

xavierjurado opened this issue Apr 23, 2025 · 1 comment

Comments

@xavierjurado
Copy link

Runtime is not able to calculate the correct ivar offsets under some specific circumstances. Take a look at the following example:

import Foundation
import Runtime

class IncorrectLayout: NSObject {
  let ivar1 = AttributedString("ivar1") // Value type
  let ivar2 = NSDate()                  // Reference type
}

Inspecting this type through Runtime gives the following offsets:

PropertyInfo(name: "ivar1", type: Foundation.AttributedString, isVar: false, offset: 0, ownerType: ...)
PropertyInfo(name: "ivar2", type: NSDate, isVar: false, offset: 8, ownerType: ...)

But that's not correct as far as I understand. According to ivar_getOffset the offsets are off by 8 bytes. Because of that, using get(from:) will return an instance pointing to an invalid memory address, resulting in a crash when invoking any method on it. Eg:

let instance = IncorrectLayout()
let info = try typeInfo(of: IncorrectLayout.self)

try info.properties.forEach { property in
  if property.name == "ivar2" {
    let ivar = try property.get(from: instance) as NSDate
    print(ivar.timeIntervalSinceNow) // Unrecognized selector -[_TtCV10Foundation16AttributedString4Guts timeIntervalSinceNow]!
  }
}

Additional notes

Attempting to change almost anything from IncorrectLayout will likely fix the issue. Eg:

  • Removing the NSObject superclass
  • Changing the type of ivar1 to a reference type
  • Removing ivar1
@xavierjurado
Copy link
Author

xavierjurado commented May 4, 2025

Context

I partially know what's going on. First, here's a simpler example that illustrates the problem:

// This is the API Swit.Mirror uses under the hood: https://github.com/swiftlang/swift/blob/ff790c6d01edab1695c744d22dad0d8b32f2d749/stdlib/public/core/ReflectionMirror.swift#L42-L46
@_silgen_name("swift_reflectionMirror_recursiveChildOffset")
internal func _getChildOffset(
  _: Any.Type,
  index: Int
) -> Int

class IncorrectLayout: NSObject {
  let ivar1 = AttributedString("ivar1") // Value type
}

let info = try typeInfo(of: IncorrectLayout.self)

let mirrorOffset = _getChildOffset(IncorrectLayout.self, index: 0)
let runtimeOffset = info.properties[0].offset

XCTAssert(mirrorOffset == runtimeOffset)

The assertion fails, with Swift API correctly returning 8 bytes but Runtime returning 0. With this example, attempting to try info.properties[0].get(from: instance) will crash the process.

Problem

As far as I can tell the underlying problem here is not Runtime's implementation, but Swift's metadata representation which just happens to be incorrect for some classes. Apple gets away from this bug/limitation by using Objective-C's ivar_getOffset API as per:

https://github.com/swiftlang/swift/blob/ff790c6d01edab1695c744d22dad0d8b32f2d749/stdlib/public/runtime/ReflectionMirror.cpp#L731-L747

    // FIXME: If the class has ObjC heritage, get the field offset using the ObjC
    // metadata, because we don't update the field offsets in the face of
    // resilient base classes.
    uintptr_t fieldOffset;
    if (usesNativeSwiftReferenceCounting(Clazz)) {
      fieldOffset = Clazz->getFieldOffsets()[i];
    } else {
  #if SWIFT_OBJC_INTEROP
      Ivar *ivars = class_copyIvarList(
          reinterpret_cast<Class>(const_cast<ClassMetadata *>(Clazz)), nullptr);
      fieldOffset = ivar_getOffset(ivars[i]);
      free(ivars);
  #else
      swift::crash("Object appears to be Objective-C, but no runtime.");
  #endif
    }

By using the debugger to force usesNativeSwiftReferenceCounting to always return true I've reproduced the same issue in Apple's Swift.Mirror API. The FIXME is 7 years old so this seems like a longtime issue.

As for what type of layout triggers this error I'm not sure. Looking at the different values of ClassTypeDescriptor I've found that for cases where the offset is invalid, var flags: ContextDescriptorFlags has the bit 16 (MetadataInitialization) set to 1 (SingletonMetadataInitialization).

According to Apple's source code this means there's something unusual about how the metadata is initialized, but I don't know if/how that could affect the ivar offset calculation. It does seem to affect the number of trailing objects.

Next steps

The easiest workaround could be doing what Apple does and rely on ivar_getOffset. A bit more risky approach would be to figure out how the offset should be corrected, but with upstream having the same issue it may be difficult. I'll try to at least report the issue to Apple's Swift repository.

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

No branches or pull requests

1 participant