Crash when iterating over CMSensorDataList via NSFastEnumeration

When iterating over a CMSensorDataList (which conforms to NSFastEnumeration) returned by CoreMotion's CMSensorRecorder's accelerometerData(from:to:) method, we sometimes see the crash below, with the frame above pointing to the close brace of the for loop iterating over the list.

0 CoreMotion 0x000000019fcf01dc CLInternalGetPinnedLocationAuthorizationState + 423316 1 CoreMotion 0x000000019fcf036b CLInternalGetPinnedLocationAuthorizationState + 423715 2 CoreMotion 0x000000019fcf02cb CLInternalGetPinnedLocationAuthorizationState + 423555 3 CoreMotion 0x000000019fbb8553 CLMotionActivity::isStatic() const + 1751823

It has been incredibly difficult to reproduce. We managed to reproduce a similar crash stack twice in a separate app that misused accelerometerData(from:to:) by providing a from and to Date that were more than 12 hours apart and iterating over the returned data, but it only occurred twice in many hours of testing. We hope to know if this could be a bug in Core Motion or it is definitely in our SDK, or if there is any way to fail gracefully in this scenario.

Thanks for the post, I’m not in that team, but I think that the team will look at your post and will ask you to submit a comprehensive crash report, adhering to the guidelines outlined in Posting a Crash Report.

https://developer.apple.com/forums/thread/688669

I think if you provide that now, will be easier for any knowledgeable developer of that domain to help you figure it out.

I believe CMSensorDataList is not a standard Array; it is an active iterator. When you call accelerometerData(from:to:), Core Motion doesn't immediately load all the data into memory. Instead, it sets up an XPC connection to locationd (which reads from a local SQLite database)?

When you iterate over it using a for...in loop in Swift, it uses NSFastEnumeration under the hood. This fetches data in batches. When you hit the closing brace } and the current batch is exhausted, Swift calls back into CoreMotion to fetch the next batch. This will be easy to reproduce with some code.

Are you request a massive time window (like > 12 hours), when asks for the next batch, the underlying iterator encounters a corrupted state or a dead XPC connection and hard-crashes (usually an EXC_BAD_ACCESS)?

I think you should provide at the request and how many items gets returned, you should post what’s your query and the time requested.

Albert Pascual
  Worldwide Developer Relations.

Hi, thanks for the reply!

I can attach a redacted crash report below. I've redacted our class and method names, so hopefully that does not make it too difficult to read.

As for reproduction, we can reproduce the iteration, but we cannot get it to crash despite hours of testing. This does align with the fact that this crash only happens to a very small subset of users in the field.

We are querying for at most 10 minutes of data at a time, with none being older than 3 days. I cannot attach an exact query that produces a crash because we cannot reproduce any crashes, but I can paste some of the logic below.

@Published private var dateIntervals: [DateInterval] = [] {
        didSet {
            storage.save(intervals: dateIntervals)
        }
    }

    func consumeNextDataBatch(length: TimeInterval) -> AccelerationHistoryBatch { // length is always .minutes(10)
        dropUnreachableDateIntervals()
        guard let firstInterval = dateIntervals.first else {
            return .empty
        }
        if firstInterval.duration <= length {
            // The first interval is shorter then our target lenght, no need to further slicing, just return it
            return .complete(getAccelerationHistory(for: dateIntervals.removeFirst()))
        }
        // The next interval is longer then what we need -> need to slice it
        let slices = firstInterval.split(at: length)
        // We have already checked that this interval can be split at this length so we can safely force unwrap here
        dateIntervals[0] = slices.second! // swiftlint:disable:this force_unwrapping
        return .partial(getAccelerationHistory(for: slices.first))
    }

    private func getAccelerationHistory(for interval: DateInterval) -> CMSensorDataList? {
        let fromDate = max(interval.start,
                           clock.currentDate.addingTimeInterval(-TMSensor42Constants.maxHistorySeconds)) // maxHistorySeconds == 60 * 60 * 24 * 3 - 60
        let toDate = max(fromDate, interval.end)
        return sensorRecorder.accelerometerData(from: fromDate, to: toDate)
    }

In summary, we split a date interval to be 10 minutes or less and query that time range with accelerometerData.

Hopefully these attachments have been helpful, let me know if anything else would be useful, thanks!

Crash when iterating over CMSensorDataList via NSFastEnumeration
 
 
Q