Skip to content
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

[SR-12344] Xcode 11.4.beta3 - objc_copyClassList Returns Invalid Data #54778

Closed
swift-ci opened this issue Mar 11, 2020 · 26 comments
Closed

[SR-12344] Xcode 11.4.beta3 - objc_copyClassList Returns Invalid Data #54778

swift-ci opened this issue Mar 11, 2020 · 26 comments
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior.

Comments

@swift-ci
Copy link
Collaborator

Previous ID SR-12344
Radar https://feedbackassistant.apple.com/feedback/7613367
Original Reporter kkline (JIRA User)
Type Bug
Status Resolved
Resolution Done

Attachment: Download

Additional Detail from JIRA
Votes 5
Component/s
Labels Bug
Assignee None
Priority Medium

md5: 4672a6d522080a19e260c94b01ceb33f

Issue Description:

We are getting a crash when running this code -

        var actualClassCount = UInt32(0)
        let allClasses = objc_copyClassList(&actualClassCount)
        var classes = [AnyClass]()

        for i in 0 ..< Int(actualClassCount) {
            if let currentClass: AnyClass = allClasses?[i] {
                classes.append(currentClass)
            }
        }

The Crash we see is on the line: if let currentClass: AnyClass = allClasses?[i] {

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

We are getting these errors printed out, right before it crashes

2020-03-04 12:34:09.581854-0600 ConnectVoice[31996:619862] *** NSForwarding: warning: object 0x7fff890dda60 of class 'PFEmbeddedMulticasterImplementation' does not implement methodSignatureForSelector: -- trouble ahead
2020-03-04 12:34:09.582026-0600 ConnectVoice[31996:619862] *** NSForwarding: warning: object 0x7fff890dda60 of class 'PFEmbeddedMulticasterImplementation' does not implement doesNotRecognizeSelector: -- abort

It seems the problem is that the array returned by objc_copyClassList is either filled with invalid objects, or somehow getting cleaned up right after it gets created, because if I try to access the objects in allClasses as Any, it crashes as soon as it tries to access.

-Thanks

@swift-ci
Copy link
Collaborator Author

Comment by Mikhail Lobanov (JIRA)

I've got the same issue.

@swift-ci
Copy link
Collaborator Author

Comment by Sergei Shirokov (JIRA)

Me too. No idea how to fix this.

@swift-ci
Copy link
Collaborator Author

Comment by Kris Kline (JIRA)

Unfortunately it looks like it would have to be fixed by Apple...don't see anything we can do about it. 🙁

@swift-ci
Copy link
Collaborator Author

swift-ci commented Apr 3, 2020

Comment by Kris Kline (JIRA)

Attached a sample project showing the problem (also attached the project to the ticket logged with Apple)

FailToLoadClasses.zip

@swift-ci
Copy link
Collaborator Author

swift-ci commented Apr 3, 2020

Comment by Kris Kline (JIRA)

To get around it for now, we re-architected a bit and now use the class name and pass it to NSStringFromClass. It is not near as elegant 🙁

@swift-ci
Copy link
Collaborator Author

Comment by Kris Kline (JIRA)

Haven't heard anything since the initial response that requested the sample project...

@swift-ci
Copy link
Collaborator Author

Comment by Kris Kline (JIRA)

Xcode 11.4.1 was released today (or yesterday...not sure), looks like this might be fixed...

Swift

Resolved Issues

Fixed a crash that could occur in Swift code that imported an Objective-C class defined with the objc_runtime_name attribute. (60888835)

Still working on installing and then will try it out...

@swift-ci
Copy link
Collaborator Author

Comment by Kris Kline (JIRA)

False hope...not fixed. 🙁

@johnno1962
Copy link
Contributor

I've raised a PR that seems to resolve this issue with Xcode 11.4+ #31128, cc: @lorentey

@CodaFi
Copy link
Member

CodaFi commented Apr 19, 2020

What y'all are noticing is that loads involving AutoreleasingUnsafeMutablePointer still involve ARC traffic against unknown objects. Unfortunately, that means that this access `allClasses?[i]` was always doomed to fail. Let's take a step back

// SomeObjCFramework.h
OBJC_ROOT_CLASS @interface MyRootClass
@end
// SomeObjCFramework.m
@implementation MyRootClass
@end
// SomeSwiftCode.swift
import SomeObjCFramework

print(MyRootClass.self) // Explodes!

This code explodes for the same reason, but demonstrates the issue with a simpler example. Swift attempts to manipulate the metaclass MyRootClass.self as though it were any other "unknown object" reference by sending them `swift_unknownObjectRetain` and `swift_unknownObjectRelease`. Unfortunately, though `MyRootClass` implements enough to convince the Objective-C and Swift runtimes that it is a class, it is really a ticking time bomb. The first message we send to this object is going to blow up because it neither implements nor forwards the message.

Returning to your code, there is a hidden `swift_unknownObjectRetain` to try to balance out the +0 load from the pointer, and that retain occurs on the line you called out. Since the object doesn't respond to or forward `retain`, the default forwarding handler installed by CF has no choice but to abort.

So, what can you do about it? Well, if ARC is the problem, then skirting ARC is the solution. You'll need to drop down to raw pointer accesses

    var nc: UInt32 = 0
    if let classes = objc_copyClassList(&nc) {
      let ptr = UnsafePointer<AnyClass>(classes)
      for i in (0 ..< Int(nc)) {
        print(ptr[i])
      }
      free(UnsafeMutableRawPointer(classes))
    }

@CodaFi
Copy link
Member

CodaFi commented Apr 19, 2020

Note that the above "works" by convincing the compiler we have the appropriate __strong references to the metaclasses in this buffer, so it doesn't need to defensively retain on the access. This is a lie that has very bad consequences in optimized code. The most correct answer would be to reinterpret the class list as `UnsafePointer<Unmanaged<AnyClass>>`, but this type is impossible to form as `AnyObject.Type` does not satisfy class bounds.

@swift-ci
Copy link
Collaborator Author

Comment by Kris Kline (JIRA)

What @CodaFi commented does seem to work just fine.

Changing the return type to an UnsafePointer<AnyClass>, and then freeing as an 'UnsafeMutableRawPointer'.

@swift-ci
Copy link
Collaborator Author

Comment by Leslie Godwin (JIRA)

@CodaFi 's example does not work for me when I try add the type to a list (I guess retaining it).

So this works:

for i in (0 ..< Int(nc)) {
   print(ptr[I]) // OK
}

But if you try do something else with the type (not just a print()), maybe add to an array...

for i in (0 ..< Int(nc)) { 
    results.append(ptr[I]) // CRASH: Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) 
}  

@swift-ci
Copy link
Collaborator Author

Comment by Leslie Godwin (JIRA)

I've just created a pure Objective-C implementation and it has the same issue:

+ (NSArray<Class> *) allTypes
{
    NSMutableArray *results = [NSMutableArray array];

    int numClasses = objc_getClassList(NULL, 0);
    if (numClasses > 0 )
    {
        Class *classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);
        numClasses = objc_getClassList(classes, numClasses);
        for (int i = 0; i < numClasses; i++)
        {
            [results addObject: classes[i]];
// CRASH: Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
        }
        free(classes);
    }

    return [results copy];
}

@swift-ci
Copy link
Collaborator Author

Comment by Leslie Godwin (JIRA)

The class that causes the crash is `PFEmbeddedMulticasterImplementation`

@swift-ci
Copy link
Collaborator Author

Comment by Kris Kline (JIRA)

raygun (JIRA User) - You might want to try calling objc_copyClassList(), instead of objc_getClassList().

It's been working fine for us since changing to adapt to @CodaFi's suggestion.

@CodaFi
Copy link
Member

CodaFi commented May 15, 2020

Resolving this issue since it is not specific to Swift, it is specific to the set of frameworks you have loaded into your process at runtime. `PF`-prefixed classes, for example, come from CoreData.

@swift-ci
Copy link
Collaborator Author

Comment by Leslie Godwin (JIRA)

kkline (JIRA User) , in a playground this still crashes, using `objc_copyClassList`:

  • However it crashes, not the `print()`, but on the append to the list. Also it crashes on the first item in the list.

It prints:

JSExport

and then crashes.

  • There is also no reference to CoreData
import Foundation

var typeCount: UInt32 = 0
if let classes = objc_copyClassList(&typeCount)
{
    let types = UnsafePointer<AnyClass>(classes)


    var result: [AnyClass] = []
    
    for index in 0 ..< Int(typeCount)
    {
        print(types[index])
        result.append(types[index]) // error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).
    }


    print(result)


  free(UnsafeMutableRawPointer(classes))
}

(The first attachment is incorrect, don't know how to delete it)

@swift-ci
Copy link
Collaborator Author

Comment by Kris Kline (JIRA)

raygun (JIRA User) - Actually...I think you're issue may be that you aren't appending to the array, you are setting at an unallocated index..

Maybe try changing that line to -

result.append(types[index])

Just guessing that might fix the problem...

@swift-ci
Copy link
Collaborator Author

Comment by Leslie Godwin (JIRA)

Sorry my bad, I just deleted the incorrect attachment now. I couldn't find how to when I logged the comment.
The correct attachment is: Playground-1

This is the dilemma:

print(types[index])
 result.append(types[index]) // error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).

If you and a look at the attachment again, (sorry) you'll see that `print()` does print the name. ("JSExport" is printed in the playground console)

So print works....?, it finds a class and printed its name> Actually the first class found.
But then it crashes on the next line, when appending that same class to the array. Probably because `append` is retaining it.

SO, `print` works, but `append` crashes.

@swift-ci
Copy link
Collaborator Author

Comment by Kris Kline (JIRA)

raygun (JIRA User) - I'm not using a playground, but in a library, this still works just fine for me- (called from a consuming app, or from unit tests)

    static func objc_getAllClasses() -> [AnyClass] {
        var actualClassCount = UInt32(0)

        // Following the suggestions/details from the SWIFT issue: https://bugs.swift.org/browse/SR-12344
        let allClasses = UnsafePointer<AnyClass>(objc_copyClassList(&actualClassCount))

        var classes: [AnyClass] = []

        for i in 0 ..< Int(actualClassCount) {
            if let currentClass: AnyClass = allClasses?[i] {
                print(currentClass)
                classes.append(currentClass)
            }
        }

        free(UnsafeMutableRawPointer(mutating: allClasses))

        return classes
    }

There is a chance that if you're just doing this in a 'playground', it may not like the objc class inspection logic...

@swift-ci
Copy link
Collaborator Author

Comment by Leslie Godwin (JIRA)

kkline (JIRA User), Thank you, 😃 only saw your reply now.

Your solution works. 😉

It took me a while to spot the difference: `objc_copyClassList`....
I still get a crash with `objc_getClassList` on Xcode 12.0.1 ....is it really still not working?

@swift-ci
Copy link
Collaborator Author

Comment by Leslie Godwin (JIRA)

kkline (JIRA User)
So I figured out what is stopping the crash.
Here's my original crashing code again, but now it copies items from the 'types' UnsafePointer variable, not from the 'safeTypes` AutoreleasingUnsafeMutablePointer variable....

public static func allTypes() -> [AnyObject.Type]
{
    let typeCount = Int(objc_getClassList(nil, 0))
    let     types = UnsafeMutablePointer<AnyObject.Type>.allocate(capacity: typeCount)
    defer { types.deallocate() }

                  let safeTypes = AutoreleasingUnsafeMutablePointer<AnyObject.Type>(types)
    objc_getClassList(safeTypes, Int32(typeCount))

    return [AnyObject.Type](unsafeUninitializedCapacity: typeCount)
    {
        buffer, initializedCapacity in

        for index in 0 ..< typeCount
        {
            buffer[index] = types[index] // <---- if you use `safeTypes` it crashes...
        }

        initializedCapacity = typeCount
    }
}

Does this sound correct? Why when accessing from `safeTypes` is it breaking? Should it still not have worked? You'll notice in your example by remove the UnsafePointer() wrapping, that it will also crash.

@swift-ci
Copy link
Collaborator Author

Comment by Kris Kline (JIRA)

At least in the code you put...if you're intentionally creating 'safeTypes' from 'types' and then loading the class list into safeTypes...that could be doing some weird things...

I would just suggest going from the code I posted, as that appears to work good for us (and work on Xcode 12.2).

-Thanks

@swift-ci
Copy link
Collaborator Author

Comment by Leslie Godwin (JIRA)

Seems Xcode 13 Beta3 is now again returning class types that give the error:

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

when casting.

This code is working in Xcode 12.5.1, (if I re-compile in Xcode 13 beta3 this crashes)

    protocol ITest
    {
    }
    
    struct ObjC
    {
        public static func allTypes() -> [AnyObject.Type]
        {
            var _typesCount  = UInt32(0)
            let typesPointer = UnsafePointer(objc_copyClassList(&_typesCount))!
            let typesCount   = Int(_typesCount)
    
    
            return [AnyObject.Type](unsafeUninitializedCapacity: typesCount)
            {
                buffer, initializedCapacity in
    
    
                for index in 0 ..< typesCount
                {
                    // this line was inserted to show the error
                    typesPointer[index] is ITest.Type //<----- CRASHES HERE !
    
                    buffer[index] = typesPointer[index]
                }
    
    
                initializedCapacity = typesCount
            }
        }
    }

@CodaFi
Copy link
Member

CodaFi commented Jul 22, 2021

raygun (JIRA User) I want to reiterate that this is not a compiler problem and your code was always incorrect - the OS version and installed frameworks your code is executing against is the variable here, not the compiler. You may not use Swift casting primitives to detect the dynamic type of these raw classes for the same reasons outlined above - an `is` check may be implemented as a conditional-cast-then-check-for-nil which is always going to occur at +1. You must manipulate these values without incurring ARC traffic of any kind or dispatching through runtime primitives that do not expect these values.

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior.
Projects
None yet
Development

No branches or pull requests

3 participants