Skip to content

[iOS] Crash in AudioCache::invokingLoadCallbacks during Engine::destroy (CC_CURRENT_ENGINE unsafe on audio thread) #19176

@panshengneng

Description

@panshengneng

Description

On iOS, the app can crash during shutdown when the audio worker thread calls AudioCache::invokingLoadCallbacks() while cc::Engine::destroy() / AudioEngine::end() is tearing down the engine on the main thread.

Observed on Cocos Creator 3.8.8 (versions 3.8.x native audio stack) across multiple Crashlytics sessions when the app receives applicationWillTerminate or exits.

Stack traces (representative)

Direct crash in audio callback dispatch:

Crashed: audio worker thread
0  cc::AudioCache::invokingLoadCallbacks()
1  cc::AudioCache::readDataTask(unsigned int)
2  cc::AudioEngine::AudioEngineThreadPool::threadFunc()

Main thread concurrently:
0  std::thread::join()
1  cc::AudioEngine::AudioEngineThreadPool::~AudioEngineThreadPool()
2  cc::AudioEngine::end()
3  cc::Engine::destroy()

Scheduler / UAF variant:

Crashed: com.apple.root.default-qos
0  ??? (invalid address)

Main thread: Engine::destroy() / ScriptEngine::cleanup() / AudioEngineImpl dtor

Root cause

Shutdown race

applicationWillTerminate
  └── Engine::destroy()
        └── AudioEngine::end()
              ├── delete sThreadPool  → join() blocks until audio thread finishes
              │     audio thread may be in:
              │       readDataTask() → invokingLoadCallbacks()
              │         └── CC_CURRENT_ENGINE()->getScheduler()  ← crash
              └── delete sAudioEngineImpl  (engine/scheduler already invalid)

AudioCache may still be alive (*_isDestroyed == false) while the engine is being destroyed, so the destroyed-flag check does not protect this path.

Bug 1: CC_CURRENT_ENGINE() is unsafe during app teardown

File: native/cocos/audio/apple/AudioCache.mminvokingLoadCallbacks()

Current code:

auto scheduler = CC_CURRENT_ENGINE()->getScheduler();
scheduler->performFunctionInCocosThread([&, isDestroyed]() { ... });

CC_CURRENT_ENGINE() expands to something like ApplicationManager::getCurrentAppSafe()->getEngine().

  • In Release, when _currentApp is expired, getCurrentAppSafe() can return nullptr (assert is no-op).
  • Then nullptr->getEngine() is a virtual call on null → crash before any null check on the engine pointer is possible.

A null check on CC_CURRENT_ENGINE() does not fix this; the macro must not be used on the audio thread during shutdown.

Suggested approach: use ApplicationManager::getInstance()->getCurrentApp() (returns nullptr if expired without dereferencing), then null-check app, engine, and scheduler before scheduling work.

Also:

  • Capture _state by value in the lambda (avoid cross-thread read of _state while the audio thread writes it).
  • Keep _loadCallbacks main-thread-only invariant (AudioCache.h design).

Bug 2: AudioEngineThreadPool::threadFunc stop/wait logic

File: native/cocos/audio/AudioEngine.cpp

The worker loop can wake from wait without re-checking _stop in a single predicate, and pending tasks are not cleared in the destructor before join(), prolonging shutdown and allowing more readDataTask work during teardown.

Suggested improvements:

  • In dtor: set _stop = true, drain _taskQueue, then notify_all() before join().
  • Use wait(lk, [this]{ return _stop || !_taskQueue.empty(); }) then break if _stop.

Suggested patch (summary)

AudioCache.mminvokingLoadCallbacks()

auto app = ApplicationManager::getInstance()->getCurrentApp();
if (!app) return;
auto engine = app->getEngine();
if (!engine) return;
auto scheduler = engine->getScheduler();
if (!scheduler) return;

auto isDestroyed = _isDestroyed;
auto state = _state;

scheduler->performFunctionInCocosThread([this, state, isDestroyed]() {
    if (*isDestroyed) return;
    for (auto &&cb : _loadCallbacks) {
        cb(state == State::READY);
    }
    _loadCallbacks.clear();
});

AudioEngine.cpp — thread pool

// dtor: clear queue after _stop = true
// threadFunc: wait with predicate, then if (_stop) break;

Reproduction

  1. iOS build with native audio (Apple backend).
  2. Preload several audio clips (preload / play with async read).
  3. Force-quit or background-kill the app while loads are in flight.
  4. Crash in invokingLoadCallbacks or invalid scheduler access during AudioEngine::end().

Environment

  • Cocos Creator: 3.8.8
  • Platform: iOS
  • Files: native/cocos/audio/apple/AudioCache.mm, native/cocos/audio/AudioEngine.cpp

Related (not fixed by above)

Separate watchdog kills when main thread blocks in alcMakeContextCurrent(nullptr) / AURemoteIO::Stop() during OpenAL teardown — likely needs AudioSession suspend / context suspend before destroy. Can file separately if useful.

Verification

After fix: no crashes in invokingLoadCallbacks during Engine::destroy() / thread pool join() on forced app exit with in-flight audio preloads.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions