CoreAudio crashing after initialising devices with different buffer sizes

This is a bit of a weird one but would explain some of the crashes we’ve been seeing in our log files recently. I’m not sure of the exact steps to trigger this as it happened as I was trying to do a screen share with someone and share audio, so was creating multiple aggregate in and out devices on macOS to share live input and software input etc.

Anyway, it seems like it’s possible to open one device with a block size that’s smaller than another and the larger block size is seen as the “device” block size. However, the audio callbacks are allocated at their original sizes so there’s a out-of-bounds write during the audio callback.

E.g. Here’s the device getting initialised:

But here’s the input device being allocated:

And here’s where it crashes:


I guess the closest thing there is to an allocated size is this:

Which shows 1280/4 = 320 which is less than the bufferSize being used in the loop.


The input device reports it can do 512 so I’m not sure why it doesn’t get changed or allocated with that.
I’d also be happy with just refusing to open the mis-matched device but as this crashes early, I’ve got no way to reset the audio device to avoid the OOBW and memory corruption.

We recently found out that audio buffer sizes can sometimes exceed the prepared buffer size on the iOS simulator:

I wonder whether the issue you’re seeing has a similar root cause. If so, the fix we’ve just merged for the above issue may help.

There was another similar issue reported a few weeks ago, where this area of the code was triggering ASAN errors. Again, I’m not completely sure this is related, but I’m mentioning it in case you’re able to spot any similar details.

Unfortunately updating didn’t help.

Here’s the Asan trace in case it’s helpful but doesn’t really provide more info that I’ve already added:

==83809==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x61a00006c590 at pc 0x0001082063ec bp 0x00017767e170 sp 0x00017767e168
WRITE of size 4 at 0x61a00006c590 thread T17
    #0 0x1082063e8 in juce::CoreAudioClasses::CoreAudioInternal::audioCallback(AudioTimeStamp const*, AudioTimeStamp const*, AudioBufferList const*, AudioBufferList*)+0x1024 (Waveform 13:arm64+0x1037123e8)
    #1 0x108204c8c in juce::CoreAudioClasses::CoreAudioInternal::audioIOProc(unsigned int, AudioTimeStamp const*, AudioBufferList const*, AudioTimeStamp const*, AudioBufferList*, AudioTimeStamp const*, void*)+0x84 (Waveform 13:arm64+0x103710c8c)
    #2 0x1866ce1b4 in HALC_ProxyIOContext::IOWorkLoop()+0x2b38 (CoreAudio:arm64e+0x1ed1b4)
    #3 0x1866caefc in invocation function for block in HALC_ProxyIOContext::HALC_ProxyIOContext(unsigned int, unsigned int)+0xac (CoreAudio:arm64e+0x1e9efc)
    #4 0x186877104 in HALC_IOThread::Entry(void*)+0x54 (CoreAudio:arm64e+0x396104)
    #5 0x164115858 in asan_thread_start(void*)+0x40 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x51858)
    #6 0x183bbf2e0 in _pthread_start+0x84 (libsystem_pthread.dylib:arm64e+0x72e0)
    #7 0x183bba0f8 in thread_start+0x4 (libsystem_pthread.dylib:arm64e+0x20f8)

0x61a00006c590 is located 0 bytes after 1296-byte region [0x61a00006c080,0x61a00006c590)
allocated by thread T0 here:
    #0 0x164118fd0 in calloc+0x9c (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x54fd0)
    #1 0x1081f99a8 in juce::HeapBlock<float, false>::callocWrapper(unsigned long, unsigned long)::'lambda'()::operator()() const+0xc8 (Waveform 13:arm64+0x1037059a8)
    #2 0x1081f98b4 in float* juce::HeapBlock<float, false>::wrapper<juce::HeapBlock<float, false>::callocWrapper(unsigned long, unsigned long)::'lambda'()>(unsigned long, juce::HeapBlock<float, false>::callocWrapper(unsigned long, unsigned long)::'lambda'()&&)+0x7c (Waveform 13:arm64+0x1037058b4)
    #3 0x1081f978c in juce::HeapBlock<float, false>::callocWrapper(unsigned long, unsigned long)+0x1b8 (Waveform 13:arm64+0x10370578c)
    #4 0x1081f8d68 in void juce::HeapBlock<float, false>::calloc<int>(int, unsigned long)+0xb4 (Waveform 13:arm64+0x103704d68)
    #5 0x1081dc39c in juce::CoreAudioClasses::CoreAudioInternal::allocateTempBuffers()+0x338 (Waveform 13:arm64+0x1036e839c)
    #6 0x1081d7a74 in juce::CoreAudioClasses::CoreAudioInternal::updateDetailsFromDevice(juce::BigInteger const&, juce::BigInteger const&)+0xb60 (Waveform 13:arm64+0x1036e3a74)
    #7 0x1082023fc in juce::CoreAudioClasses::CoreAudioInternal::reopen(juce::BigInteger const&, juce::BigInteger const&, double, int)+0x880 (Waveform 13:arm64+0x10370e3fc)
    #8 0x1081b7ccc in juce::CoreAudioClasses::CoreAudioIODevice::open(juce::BigInteger const&, juce::BigInteger const&, double, int)+0x688 (Waveform 13:arm64+0x1036c3ccc)
    #9 0x108251d64 in juce::CoreAudioClasses::AudioIODeviceCombiner::DeviceWrapper::open(juce::BigInteger const&, juce::BigInteger const&, double, int) const+0x1b0 (Waveform 13:arm64+0x10375dd64)
    #10 0x10820dde0 in juce::CoreAudioClasses::AudioIODeviceCombiner::open(juce::BigInteger const&, juce::BigInteger const&, double, int)+0xb38 (Waveform 13:arm64+0x103719de0)
    #11 0x1080f57ac in juce::AudioDeviceManager::setAudioDeviceSetup(juce::AudioDeviceManager::AudioDeviceSetup const&, bool)+0x1f28 (Waveform 13:arm64+0x1036017ac)
    #12 0x1080ed118 in juce::AudioDeviceManager::initialiseDefault(juce::String const&, juce::AudioDeviceManager::AudioDeviceSetup const*)+0x6d8 (Waveform 13:arm64+0x1035f9118)
    #13 0x1080f10e8 in juce::AudioDeviceManager::initialise(int, int, juce::XmlElement const*, bool, juce::String const&, juce::AudioDeviceManager::AudioDeviceSetup const*)+0x50c (Waveform 13:arm64+0x1035fd0e8)
    #14 0x1080ec2c8 in juce::AudioDeviceManager::initialiseFromXML(juce::XmlElement const&, bool, juce::String const&, juce::AudioDeviceManager::AudioDeviceSetup const*)+0x1928 (Waveform 13:arm64+0x1035f82c8)
    #15 0x1080f1084 in juce::AudioDeviceManager::initialise(int, int, juce::XmlElement const*, bool, juce::String const&, juce::AudioDeviceManager::AudioDeviceSetup const*)+0x4a8 (Waveform 13:arm64+0x1035fd084)
    #16 0x111aff158 in tracktion::engine::DeviceManager::loadSettings()+0xa48 (Waveform 13:arm64+0x10d00b158)
    #17 0x111afe234 in tracktion::engine::DeviceManager::initialise(int, int)+0x27c (Waveform 13:arm64+0x10d00a234)
    #18 0x1145b7100 in tracktion::engine::Engine::initialise()+0x1164 (Waveform 13:arm64+0x10fac3100)
    #19 0x1145b4628 in tracktion::engine::Engine::Engine(std::__1::unique_ptr<tracktion::engine::PropertyStorage, std::__1::default_delete<tracktion::engine::PropertyStorage>>, std::__1::unique_ptr<tracktion::engine::UIBehaviour, std::__1::default_delete<tracktion::engine::UIBehaviour>>, std::__1::unique_ptr<tracktion::engine::EngineBehaviour, std::__1::default_delete<tracktion::engine::EngineBehaviour>>)+0x878 (Waveform 13:arm64+0x10fac0628)
    #20 0x1145b7fb8 in tracktion::engine::Engine::Engine(std::__1::unique_ptr<tracktion::engine::PropertyStorage, std::__1::default_delete<tracktion::engine::PropertyStorage>>, std::__1::unique_ptr<tracktion::engine::UIBehaviour, std::__1::default_delete<tracktion::engine::UIBehaviour>>, std::__1::unique_ptr<tracktion::engine::EngineBehaviour, std::__1::default_delete<tracktion::engine::EngineBehaviour>>)+0x88 (Waveform 13:arm64+0x10fac3fb8)
    #21 0x115d03e34 in std::__1::__unique_if<tracktion::engine::Engine>::__unique_single std::__1::make_unique[abi:de180100]<tracktion::engine::Engine, std::__1::unique_ptr<tracktion::engine::PropertyStorage, std::__1::default_delete<tracktion::engine::PropertyStorage>>, std::__1::unique_ptr<WaveformUIBehaviour, std::__1::default_delete<WaveformUIBehaviour>>, std::__1::unique_ptr<WaveformEngineBehaviour, std::__1::default_delete<WaveformEngineBehaviour>>>(std::__1::unique_ptr<tracktion::engine::PropertyStorage, std::__1::default_delete<tracktion::engine::PropertyStorage>>&&, std::__1::unique_ptr<WaveformUIBehaviour, std::__1::default_delete<WaveformUIBehaviour>>&&, std::__1::unique_ptr<WaveformEngineBehaviour, std::__1::default_delete<WaveformEngineBehaviour>>&&)+0x2ec (Waveform 13:arm64+0x11120fe34)
    #22 0x115d03988 in createEngine(std::__1::unique_ptr<tracktion::engine::PropertyStorage, std::__1::default_delete<tracktion::engine::PropertyStorage>>)+0x1d8 (Waveform 13:arm64+0x11120f988)
    #23 0x11601478c in TracktionApp::initialise(juce::String const&)+0x1ab8 (Waveform 13:arm64+0x11152078c)
    #24 0x10a3b9a30 in juce::JUCEApplicationBase::initialiseApp()+0x374 (Waveform 13:arm64+0x1058c5a30)
    #25 0x10bd4ff0c in juce::JUCEApplication::initialiseApp()+0x88 (Waveform 13:arm64+0x10725bf0c)
    #26 0x10a3b8890 in juce::JUCEApplicationBase::main()+0x338 (Waveform 13:arm64+0x1058c4890)
    #27 0x10a3b84fc in juce::JUCEApplicationBase::main(int, char const**)+0xfc (Waveform 13:arm64+0x1058c44fc)
    #28 0x115db16a8 in main+0x70 (Waveform 13:arm64+0x1112bd6a8)
    #29 0x18383c270  (<unknown module>)

I think the problem is in reopen:

        if (! audioObjectSetProperty (deviceID, { kAudioDevicePropertyBufferFrameSize,
                                                  kAudioObjectPropertyScopeGlobal,
                                                  juceAudioObjectPropertyElementMain },
                                      static_cast<UInt32> (bufferSizeSamples), err2log()))
        {
            updateDetailsFromDevice (ins, outs);
            return "Couldn't change buffer size";
        }

        // Annoyingly, after changing the rate and buffer size, some devices fail to
        // correctly report their new settings until some random time in the future, so
        // after calling updateDetailsFromDevice, we need to manually bodge these values
        // to make sure we're using the correct numbers..
        updateDetailsFromDevice (ins, outs);
        sampleRate = newSampleRate;
        bufferSize = bufferSizeSamples;

The call to audioObjectSetProperty (to set 512) seems to succeed (no error logged) but during the latter updateDetailsFromDevice, the device is still reporting 320, so the buffer gets allocated only space for 320.

The line after that bufferSize = bufferSizeSamples; assumes the buffer was correctly changed and stores 512 in the bufferSize member even though space was only allocated for 320.

It feels like there needs to be this check after reopenupdateDetailsFromDevice is called but there’s a comment to state devices don’t report their sizes correctly.

If I add this:

        if (sampleRate = newSampleRate)
            return "Couldn't change sample rate";

        if (bufferSize != bufferSizeSamples)
            return "Couldn't change buffer size";

The device fails to open as I would expect if the buffer size wasn’t changed.

But perhaps a change more in line with the comment on mis-behaving devices would be to call allocateBuffers if the buffer sizes don’t match? At least they’d be allocated large enough then?

        if (bufferSize != bufferSizeSamples)
        {
            bufferSize = bufferSizeSamples;
            allocateTempBuffers();
        }

When I do that, I don’t get the crash but I don’t get any audio output either.
Interestingly, if I try and change the buffer size to 512 from the juce::AudioDeviceSelectorComponent, it tries to but then eventually goes back to 320 where I do get audio again. So it does seem like the device just can’t run at 512 but reports that as a valid block size.

I think I’d prefer silence and not-crashing as the desired behaviour but maybe at least rather than using a raw juce::HeapBlock, some kind of sized view could be created and the audio callback could just bail out if it’s about to access OOB memory?

Thanks for your patience. We’ve now released a patch intended to fix this issue:

Please try it out and let us know if you still run into problems.

Thanks! I’ll roll that in to our next update once I’ve got the current one out.
Cheers.