Skip to content

Crash in unregisterSuperProperty and currentSuperProperties #622

Open
@daleswift

Description

@daleswift

Repeated calls to currentSuperProperties and unregisterSuperProperty can cause a crash.

We are seeing this occur frequently in our production app.

Thread 22 Crashed:
0   libsystem_kernel.dylib               0x00000001e231dfbc __pthread_kill + 8
1   libsystem_c.dylib                    0x00000001a2c65b90 abort + 176
2   libswiftCore.dylib                   0x0000000194347064 swift::fatalErrorv(unsigned int, char const*, char*) + 124
3   libswiftCore.dylib                   0x0000000194347084 swift::fatalError(unsigned int, char const*, ...) + 28
4   libswiftCore.dylib                   0x000000019434ba84 swift_deallocClassInstance + 304
5   libswiftCore.dylib                   0x000000019434b914 _swift_release_dealloc + 52
6   libswiftCore.dylib                   0x000000019434cfb0 bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1> >::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 132
7   nucleus-smart                        0x00000001009909d8 closure #1 () -> () in Mixpanel.MixpanelInstance.unregisterSuperProperty(Swift.String) -> () (<compiler-generated>:0)
8   nucleus-smart                        0x00000001009858cc reabstraction thunk helper from @escaping @callee_guaranteed () -> () to @escaping @callee_unowned @convention(block) () -> () (<compiler-generated>:0)
9   libdispatch.dylib                    0x00000001a2bab6a8 _dispatch_call_block_and_release + 28
10  libdispatch.dylib                    0x00000001a2bad300 _dispatch_client_callout + 16
11  libdispatch.dylib                    0x00000001a2bb4894 _dispatch_lane_serial_drain + 744
12  libdispatch.dylib                    0x00000001a2bb53c4 _dispatch_lane_invoke + 376
13  libdispatch.dylib                    0x00000001a2bc0004 _dispatch_root_queue_drain_deferred_wlh + 284
14  libdispatch.dylib                    0x00000001a2bbf878 _dispatch_workloop_worker_thread + 400
15  libsystem_pthread.dylib              0x0000000204bd7964 _pthread_wqthread + 284
16  libsystem_pthread.dylib              0x0000000204bd7a04 start_wqthread + 4

Diagnosis

In most cases MixPanelInstance references superProperties from a block executed from the trackingQueue and under readWriteLock and so the reference is thread safe. As in registerSuperProperties:

    public func registerSuperProperties(_ properties: Properties) {
        trackingQueue.async { [weak self] in
            guard let self = self else { return }
            let updatedSuperProperties = self.trackInstance.registerSuperProperties(properties,
                                                                                    superProperties: self.superProperties)
            self.readWriteLock.write {
                self.superProperties = updatedSuperProperties
            }
            self.readWriteLock.read {
                MixpanelPersistence.saveSuperProperties(superProperties: self.superProperties, instanceName: self.name)
            }
        }
    }

However, currentSuperProperties only protects superProperties with readWriteLock:

    public func currentSuperProperties() -> [String: Any] {
        var properties = InternalProperties()
        self.readWriteLock.read {
            properties = superProperties
        }
        return properties
    }

and unregisterSuperProperty only protects superProperties with trackingQueue:

    public func unregisterSuperProperty(_ propertyName: String) {
        trackingQueue.async { [weak self] in
            guard let self = self else { return }
            self.superProperties = self.trackInstance.unregisterSuperProperty(propertyName,
                                                                              superProperties: self.superProperties)
            MixpanelPersistence.saveSuperProperties(superProperties: self.superProperties, instanceName: self.name)
        }
    }

this causes a crash when both unregisterSuperProperty and currentSuperProperties are called from different threads at the same time.

The following unit test simulates the problem:

    func testCrash() {
        let token = randomId()
        let mixPanel = Mixpanel.initialize(token: token, trackAutomaticEvents: false)
        let property = "MySuperProperty"
        let queue1 = DispatchQueue(label: "queue1", qos: .default)

        for _ in 0...10000 {
            mixPanel.unregisterSuperProperty(property)
            mixPanel.registerSuperProperties([property: "1"])
        }

        queue1.async {
            while true {
                _ = mixPanel.currentSuperProperties()
            }
        }

        let expect = expectation(description: "tracking finished")
        mixPanel.trackingQueue.async {
            expect.fulfill()
        }
        wait(for: [expect])
    }

    func randomId() -> String {
        return String(format: "%08x%08x", arc4random(), arc4random())
    }

Solution

The solution should be straightforward. Reference superProperties under readWriteLock in unregisterSuperProperty and similarly in updateSuperProperty

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions