Navigation Menu

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-9074] NSKeyValueObservation swizzle is broken #3609

Closed
lilyball mannequin opened this issue Oct 25, 2018 · 12 comments
Closed

[SR-9074] NSKeyValueObservation swizzle is broken #3609

lilyball mannequin opened this issue Oct 25, 2018 · 12 comments
Assignees

Comments

@lilyball
Copy link
Mannequin

lilyball mannequin commented Oct 25, 2018

Previous ID SR-9074
Radar None
Original Reporter @lilyball
Type Bug
Status Resolved
Resolution Done
Environment

Current Swift master (14d4235b571d7a4e2ae51854f0d665f96959d182)

Additional Detail from JIRA
Votes 0
Component/s Foundation
Labels Bug
Assignee @lilyball
Priority Medium

md5: 0f5da0eb292c39647bd44dceb3feb212

Issue Description:

NSKeyValueObservation has a rather weird implementation detail. Instead of implementing observeValue(forKeyPath:of:change:context:) directly, it implements a method _swizzle_me_observeValue(forKeyPath:of:change:context:) instead, and then swizzles it over observeValue(forKeyPath:of:change:context:). There's a comment referencing a radar rdar://problem/31640524 but as I am not an Apple employee I don't know what that radar says.

In any case, this swizzle is broken. It's not replacing a method on NSKeyValueObservation, it's actually replacing the IMP for the root method on NSObject. This means that if NSObject's method is ever invoked, it will actually call NSKeyValueObservation's version, which will then attempt to read a Swift property that doesn't exist on NSObject, interpret it as a weak object, convert it to a strong object, and compare it against the observed object. Assuming this itself doesn't crash, including the comparison (which BTW is using == instead of the expected ===), if the comparison actually succeeds then it will read a second property (that doesn't exist) and attempt to invoke it as a block, which definitely will crash.

Granted, NSObject's behavior here is to throw an exception, but an exception with specific information about the observation that wasn't properly handled is far far better than potentially interpreting garbage memory as an object, passing it to -[NSObject isEqual:], and crashing.

Given all this, three fixes should be made:

  1. A dummy implementation of observeValue(forKeyPath:of:change:context:) should be provided on NSKeyValueObservation so the swizzle only affects NSKeyValueObservation instead of affecting all of NSObject. If this isn't possible (I'm really curious what that radar says), then class_addMethod should be used to create this method with the desired IMP rather than exchanging it with NSObject's.

  2. The == test should be replaced with ===, because we don't want to run -[NSObject isEqual:], we want to ensure it's literally the same object.

  3. If the equality test fails, this should call super rather than just bailing, so we get a proper exception. This covers the rather stupid scenario of someone adding a KVO observation passing the NSKeyValueObservation object as the observing object.

@belkadan
Copy link

Whoops. If anyone would know this problem, it's you, Lily! @Catfish-Man, this wasn't intentional, was it?

@lilyball
Copy link
Mannequin Author

lilyball mannequin commented Oct 25, 2018

Last night I whipped up a fix for this (and for the other issues I filed), but I haven't tested it yet. I would really like to know what the radar says though.

@belkadan
Copy link

The original bug was an error message when using override:

/Users/david/swift/swift/stdlib/public/SDK/Foundation/NSObject.swift:148:25: error: method cannot be marked @objc because the type of the parameter 3 cannot be represented in Objective-C
    @objc override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
                        ^

This would have been a compiler bug at the time, and it also definitely suggests that we weren't trying to swizzle NSObject's implementation. The workaround:

Ah, since you're going to swizzle it /anyway/, you could use [NSString: Any] for now. NSString and NSKeyValueChangeKey will always have the same ABI.

The Radar itself is now marked as Verify, by me, so maybe we can just go back to the simple thing?

@lilyball
Copy link
Mannequin Author

lilyball mannequin commented Oct 25, 2018

What happens if you simply omit the @objc keyword? Isn't it supposed to be inferred in the case of an override anyway?

I did notice the stdlib seems to be using explicit @objc and @nonobjc keywords everywhere. Is Obj-C compatibility inferred differently for the stdlib than for other code?

@belkadan
Copy link

You're right about @objc being inferred; it shouldn't have any effect here. I think the explicit-ness was mostly for the transition from Swift 3 to Swift 4, where the default behavior flipped, and it's stuck as the style to demonstrate that people have thought about it.

@Catfish-Man
Copy link
Member

I would definitely like to remove the workaround if we can!

@Catfish-Man
Copy link
Member

That said, iirc the swizzling was intentional (sorry, it's been a bit since I wrote this). The case it was trying to handle is when a class hierarchy has a mix of objc-style and swift-style KVO, to make sure that each observation callback is "visible" to both. So the ObjC ones bubble up the class hierarchy, and then the Swift ones get invoked, and then if nobody handled it it crashes as usual. Crashing in that bogus a way was definitely not the intent though, and it's certainly possible I got something else about it wrong.

@lilyball
Copy link
Mannequin Author

lilyball mannequin commented Oct 25, 2018

@Catfish-Man It sounds like you're describing the swizzling for NSKeyValueObservingCustomization. This ticket is about the swizzling on NSKeyValueObservation specifically, which is different.

@Catfish-Man
Copy link
Member

And that's what I get for commenting with a headache and not thinking clearly 🙂

In that case, yes, kill it with fire if the compiler allows now

@lilyball
Copy link
Mannequin Author

lilyball mannequin commented Oct 26, 2018

FWIW looks like the compiler bug is not yet fixed. Omitting the @objc still produces the same error that you get if you include it, which is that the method signature isn't compatible with Obj-C.

@lilyball
Copy link
Mannequin Author

lilyball mannequin commented Oct 27, 2018

I've submitted PR #20103 for this issue.

@millenomi
Copy link
Contributor

Merged: apple/swift@7c51419

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
@shahmishal shahmishal transferred this issue from apple/swift May 5, 2022
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants