Bad access with a fixed size array and index in range

Hi, first post here, I've also raised this question on stack overflow.

I'm getting crashes from a fixed size array even though I thought I'd protected the index from going out of range. The array updates in a loop, like a ring buffer.

This crashes reliably when the array size >638 but I've not managed to get it to crash with fewer elements.

I'm wondering whether this is compiler 'optimisation'... and what to do if it is. Any ideas gratefully received.

struct ZeroCrossing {
  //The integer index of the second sample (cross over is between two samples)
  var index: UInt
  //The highest amplitude peak (negative or positive) between this and the previous crossing
  let previousPeak: Float
  //The interpolated crossover point between the two sample indices
  let indexWithOffset: Double
}

class CrossingBuffer {
  private var array: [ZeroCrossing]
  private let size: Int
  private var nextWriteIndex = 0
  private var full: Bool {
    nextWriteIndex >= size
  }

  init(size: Int) {
    self.size = size
    array = [ZeroCrossing](repeating: ZeroCrossing(index: 0, previousPeak: 0, indexWithOffset: 0), count: size)
    array.reserveCapacity(size)
  }
  
  public func write(_ val: ZeroCrossing) {
    array[nextWriteIndex % size] = val
    nextWriteIndex += 1
  }
    
  public func getAfterIndex(_ refIndex: Double) -> [ZeroCrossing]? {
    if !full { return nil }
    var subArray = [ZeroCrossing]()
    let lastElementIndex = nextWriteIndex - 1
    for i in 0...size - 1 {

      //   CRASHES ON NEXT LINE !!!
      let thisCrossing = array[(lastElementIndex - i) % size]

      if thisCrossing.indexWithOffset > refIndex {
        subArray.append(thisCrossing)
      } else {
        break
      }
    }
    return subArray.reversed()
  }
  
  public func reset() {
    array = [ZeroCrossing](repeating: ZeroCrossing(index: 0, previousPeak: 0, indexWithOffset: 0), count: size)
    nextWriteIndex = 0
  }
}

The backtrace ends with the following

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x1016dc008)
    frame #0: 0x000000018bf5c090 libswiftCore.dylib`swift_retain + 60
    frame #1: 0x000000018bf9c704 libswiftCore.dylib`swift_bridgeObjectRetain + 56
  * frame #2: 0x0000000100838e50 CrossingBuffer.getAfterIndex(refIndex=431975.76999999583, self=0x00000002800a86f0) at CrossingBuffer.swift:97:31
    frame #3: 0x00000001007cd704 correlate(self=0x000000010130e5c0) at PitchEngine.swift:146:52
    frame #4: 0x00000001007a155c correlate(self=0x0000000283b91200) at TunerEngine.swift:57:39
    frame #5: 0x00000001007a1bd8 @objc correlate() at <compiler-generated>:0

But what's strange is that I can access the element from the debug console using the same index reference as the line it crashed on:

(lldb) print (lastElementIndex - i) % size
(Int) $R4 = 838
(lldb) print array.count
(Int) $R5 = 1000
(lldb) print array[(lastElementIndex - i) % size]
(ZeroCrossing) $R6 = (index = 438691, previousPeak = 0.0251232013, indexWithOffset = 438690.12000000477)

The following code should trigger the crash, the mic needs to pick up some noise for the array to populate.

import Foundation
import AVKit

final class AudioTap {
  
  private var audioEngine = AVAudioEngine()
  private var windowIndex: UInt = 0
  private var lastPeak: Float = 0
  private var lastSample: Sample?
  public var minPeakSize: Float = 0.005
  
  // Size of crossingBuffer changes crashing behavior
  private var crossingBuffer = CrossingBuffer(size: 2000)
  
  private var lastSeekIndex: Double = 0.0
  
  private var timer: Timer?
  
  init() {
    installTunerTap()
    audioEngine.prepare()
    do {
      try audioEngine.start()
    } catch let error as NSError {
      print("AVAudioEngine error on start: \(error.domain), \(error)")
    }
    timer = Timer.scheduledTimer(
      timeInterval: 0.01,
      target: self,
      selector: #selector(getNext),
      userInfo: nil,
      repeats: true)
  }
  
  private func installTunerTap() {
    let inputNode = audioEngine.inputNode
    inputNode.installTap( onBus: 0,
                          bufferSize: 1000,
                          format: nil,
                          block: { buffer, when in
      let sampleCount = Int(buffer.frameLength)
      var sampleIndex = 0
      
      while (sampleIndex < sampleCount) {
        if let val = buffer.floatChannelData?.pointee[sampleIndex]{
          let sample = Sample(index: self.windowIndex, val: val)
          self.update(sample: sample)
        }
        self.windowIndex += 1
        sampleIndex += 1
      }
    })
  }
  
  private func update(sample: Sample) {
    lastPeak = abs(sample.val) > abs(lastPeak) ? sample.val : lastPeak
    if let last = lastSample {
      if last.val * sample.val < 0 && abs(lastPeak) > minPeakSize { // this is a zero crossing
        let offset = Double(sample.index) + Double(round((sample.val/(last.val - sample.val)) * 100) / 100)
        
        let crossing = ZeroCrossing(
          index: sample.index,
          previousPeak: lastPeak,
          indexWithOffset: offset
        )
        crossingBuffer.write(crossing)
        lastPeak = 0
      }
    }
    lastSample = sample
  }
  
  @objc func getNext() {
    if let arr = crossingBuffer.getAfterIndex(lastSeekIndex) {
      if let s = arr.last {
        lastSeekIndex = s.indexWithOffset
      }
    }
  }
}

To help debug, could you add some print and tell what you have just at crash (as you did in debug):

    for i in 0...size - 1 {
      print("i", i, "lastElementIndex", lastElementIndex, "size", size, "index", (lastElementIndex - i) % size)
      //   CRASHES ON NEXT LINE !!!
      let thisCrossing = array[(lastElementIndex - i) % size]

What happens if you remove:

    array.reserveCapacity(size)

I've added that print, here are the last lines of output before the crash occurred along with the backtrace. This was also with array.reserveCapacity(size) commented out. Also the array size is set to 639 for this example.

i 0 lastElementIndex 13626 size 639 index 207
i 0 lastElementIndex 13627 size 639 index 208
i 1 lastElementIndex 13627 size 639 index 207
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x104690008)
  frame #0: 0x000000018bf5c090 libswiftCore.dylib`swift_retain + 60
  frame #1: 0x000000018bf9c704 libswiftCore.dylib`swift_bridgeObjectRetain + 56
 * frame #2: 0x0000000102671200 RingBufferCrashing`CrossingBuffer.getAfterIndex(refIndex=739167.76999999583, self=0x0000000280b622e0) at CrossingBuffer.swift:55:31
  frame #3: 0x000000010266ffb4 RingBufferCrashing`AudioTap.getNext(self=0x0000000282108180) at AudioTap.swift:81:33
  frame #4: 0x00000001026701d0 RingBufferCrashing`@objc AudioTap.getNext() at <compiler-generated>:0

Edit: I just added a bit more of the backtrace as I'm wondering whether the compiler-generated:0 is a clue.

I probably missed something in your code. How is it print with i == 0 comes twice ?

Could you explain what are (I mean, what do they represent):

  • lastElementIndex
  • array size is size ?

That's really surprising: it works a first time with index 207, but not second time.

Just as if something changed in array, in another thread ? Or the crash is reported here but occurs in fact elsewhere.

May you try something more (normally it should not change anything):

  for i in 0...size - 1 {
     let realIndex = (lastElementIndex - i) % size
      print("realIndex", realIndex)
      print("i", i, "lastElementIndex", lastElementIndex, "size", size, "index", (lastElementIndex - i) % size)
      //   CRASHES ON NEXT LINE !!!
      let thisCrossing = array[realIndex]

The i==0 occurs twice because there were no updates for the first one i.e. it read the element then if thisCrossing.indexWithOffset > refIndex returned false.

I think you may be on to something with the thread comment, feedback from my stack exchange post suggested a thread problem too as installTap may run on a separate thread. I've just tried using an NSLock around each of the writes and reads in CrossingBuffer and it seems to prevent the crashes. I'll test a bit more to see if it seems like a permanent fix.

I'm going to call this one fixed now, thanks for the help @Claude31. Here's the amended code for the CrossingBuffer class:

class CrossingBuffer {
  private var array: [ZeroCrossing]
  
  // NSLock Added
  var lock = NSLock()

  private let size: Int
  private var nextWriteIndex = 0
  private var full: Bool {
    nextWriteIndex >= size
  }

  init(size: Int) {
    self.size = size
    array = [ZeroCrossing](repeating: ZeroCrossing(index: 0, previousPeak: 0, indexWithOffset: 0), count: size)
    array.reserveCapacity(size)
  }
  
  public func write(_ val: ZeroCrossing) {
    //Lock before write
    lock.lock()
    array[nextWriteIndex % size] = val
    //Unlock
    lock.unlock()
    nextWriteIndex += 1
  }
    
  public func getAfterIndex(_ refIndex: Double) -> [ZeroCrossing]? {
    if !full { return nil }
    var subArray = [ZeroCrossing]()
    let lastElementIndex = nextWriteIndex - 1
    for i in 0...size - 1 {

        //   Was Crashing here, now wrapped in lock / unlock

        lock.lock()
        let thisCrossing = array[(lastElementIndex - i) % size]
        lock.unlock()

      if thisCrossing.indexWithOffset > refIndex {
        subArray.append(thisCrossing)
      } else {
        break
      }
    }
    return subArray.reversed()
  }
  
  public func reset() {
    array = [ZeroCrossing](repeating: ZeroCrossing(index: 0, previousPeak: 0, indexWithOffset: 0), count: size)
    nextWriteIndex = 0
  }
}
Bad access with a fixed size array and index in range
 
 
Q