Author: review pass on 2026-07-01. Supersedes the ad-hoc notes in the prior Cursor sessions.
The macOS PlatformAudioIntegrationTest suite has been crashing (SIGSEGV / SIGABRT)
under repeated run/teardown. Prior investigation (three Cursor sessions on
feature/additional_ci_improvements and feature/platform-audio-stability, plus a
rust-sdks branch of the same name) converged on a real root cause and landed a fix
bundle. This document records an independent review of that work, what I agree and
disagree with, the CI evidence, and a corrected fix plan.
Bottom line:
- The final root-cause theory is correct: the macOS CoreAudio capture worker
(
webrtc::AudioDeviceMac::CaptureWorkerThread) delivers recorded audio intowebrtc::AudioTransportImplwhile the peer-connection transports / factory that own that transport are being destroyed. The landedAdmProxy::ShutdownAudioIOfix is the right shape and empirically removed the segfault. - But the investigation stopped short in three ways this review corrects:
- A 2026-07-01 rebase silently reset the
client-sdk-cppsubmodule pointer to arust-sdkscommit that does not contain the fix. - The post-fix nightlies expose a new, deterministic failure in
PlatformAudioFramesReachRemotethat is a signaling/negotiation regression, not an audio-teardown crash. - A structural teardown race survives the fix and is the most likely cause of both
the residual "crash at the very first PlatformAudio test" symptom and the
DataTrackE2ETest.PublishManyTracksteardown hang.
- A 2026-07-01 rebase silently reset the
Evidence (from CI .ips reports, LLDB captures, and the pre-fix Intel triage run):
webrtc::AudioDeviceMac::CaptureWorkerThread() audio_device_mac.cc:2498
-> AudioConverterFillComplexBuffer
-> CrashIfClientProvidedBogusAudioBufferList (Apple AudioToolbox)
concurrent with, on the teardown side:
webrtc::AudioTransportImpl::SendProcessedData(...)
-> ...RecordedDataIsAvailable -> DeliverRecordedData
~JsepTransportController() / ~BundleManager() (transport teardown)
The capture worker owned by the platform ADM keeps calling into an AudioTransportImpl
that a closing peer connection is tearing down → use-after-free / bogus buffer → SIGSEGV
or SIGABRT ("pointer being freed was not allocated").
3. The landed fix (rust-sdks feature/platform-audio-stability, core commit 01fc4c13, PR-feedback d3bb1453)
webrtc-sysAdmProxy: newShutdownAudioIO()+StopAudioIO()/StopPlatformAudioIO()helpers that stop recording/playout and detach theAudioTransportcallback. Called from~AdmProxy(),Terminate(), and at platform-ADM refcount 0.PeerConnectionFactory::shutdown_audio_io()runsAdmProxy::ShutdownAudioIO()on the worker thread beforepeer_factory_is destroyed (~PeerConnectionFactory).LkRuntime::Dropcallsshutdown_audio_io().PlatformAdmHandle::Dropstops recording before releasing the ADM reference.FfiRoom::close()now closes the room (unpublishing tracks, stopping capture) before dropping FFI track handles.FfiServer::dispose()clearsffi_handles/handle_dropped_txs.- (client-sdk-cpp)
PlatformAudioSourcemember order swapped so the FFI handle drops before the sharedPlatformAudioState.
Verdict: keep all of it. It is correct and directly targets the confirmed race. Two minor corrections (see §6, Step 2).
Also landed and correct — keep: the LocalParticipant::published_tracks_by_sid_ mutex
(5380e42), an ASan-proven data race between publishTrack (app thread) and
trackPublications()/findTrackPublication() (FFI callback thread). It is a genuine
general-purpose SIGSEGV source, though independent of the ADM crash.
- Dangling FFI handles at dispose — instrumentation showed 0 leaked handles across
738+ dispose samples. The
ffi_handles.clear()hygiene was kept anyway (harmless). - mach-port / fd / thread leaks (CoreAudio HAL client leak) — sampler showed all flat.
- Progressive memory growth — real (~1 retained
PeerConnectionper connect cycle;leaksclean → reachable retention), but orthogonal to the crash (crash hits the first PlatformAudio test, so it is not cumulative). Root cause identified in this review: see §5.
feature/platform-audio-stability (client-sdk-cpp) now pins submodule
client-sdk-rust at dad794d4 — plain rust-sdks main, without the ADM fix.
feature/additional_ci_improvements still pins d3bb1453 (has the fix). The stability
branch as it stands does not build the fix it claims to test.
Post-fix nightlies (additional_ci_improvements, rust d3bb1453), 2026-06-28 and 06-30:
- The segfault is gone — 100+ PlatformAudio iterations, zero SIGSEGV.
PlatformAudioFramesReachRemotefails 0/26 (all attempts, including the very first), debug builds only. The log shows, at publish time:This is a signaling / negotiation failure, not audio teardown. It rode in on the upstreamWARN livekit::rtc_engine::peer_transport] peer connection is closed, cannot create offer ERROR livekit::rtc_engine::rtc_session] failed to negotiate the publisher: Rtc(RtcError { InvalidState, "Failed to set local offer sdp: Called in wrong state" })rust-sdksdelta between the old pin (8e551062) and the new base (2e83ff6b). Prime suspect: #1148 "harden reconnect behaviour" (also #996 publisher offer with join, single-PC default). Release PR CI passes; only debug nightly reproduces.
- The FFI tokio runtime is a process-static and is never shut down;
dispose()clears handles but does not wait forLkRuntime(and thus thePeerConnectionFactory+ ADM- its capture worker) to finish dropping.
LK_RUNTIMEis aMutex<Weak<LkRuntime>>: the moment the last strong ref hits zero, a newinstance()call constructs a new factory/ADM — potentially while the old one'sDrop(including the capture-worker stop) is still running on another thread.EngineInner::closedeliberately retainsRtcSession("so we can still access stats"), which keeps a wholePeerConnection(and its transports) alive per connect cycle — this is the source of the "reachable growth" from §4, and it defers transport destruction to arbitrary threads at arbitrary times.
Together these mean runtime/ADM/transport teardown from test N can overlap init from test
N+1. This matches the "crash at the start of the first PlatformAudio test" signature and is
the most plausible family for the still-open PublishManyTracks teardown hang (120-min
silent timeout on macos-x64; watchdog SIGABRT on linux-x64, where gdb is not installed so
no backtrace is produced).
Step 0 — hygiene (blocking):
- Re-point the
client-sdk-cppfeature/platform-audio-stabilitysubmodule at the fixedrust-sdkstip (currentlyd3bb1453, or the new tip from Step 2/3). - Install
gdbon the Linux CI runner so the stall watchdog's cores produce backtraces.
Step 1 — attribute the FramesReachRemote regression before trusting the fix branch.
Run the triage workflow (both arches, isolated-frames arm) across three rust pins:
8e551062 (old baseline), 2e83ff6b (new upstream base, no fix commits), d3bb1453
(fix). Signature to look for: peer connection is closed, cannot create offer ~6 s into the
test, debug builds only. If 2e83ff6b alone reproduces it, the ADM fix is exonerated and
the regression is an upstream bug to bisect (start at #1148).
Step 2 — correct the ADM fix (rust-sdks), low risk:
- In
StopAudioIO()/StopPlatformAudioIO(): callStopRecording()/StopPlayout()first (this joins the worker threads), thenRegisterAudioCallback(nullptr). WebRTC'sAudioDeviceBufferrefuses a callback change while media is active, so the detach-before-stop ordering is a silent no-op today. Stopping first makes the detach real. - (Investigated and rejected as changes:
PlatformAdmHandle::Dropcalling a globalstop_recording()is safe — the handle is shared via a staticWeak, soDroponly runs when the lastPlatformAudiois gone.AdmProxy::StopRecording()stopping both ADMs is benign — the synthetic ADM has no microphone.)
Step 3 — close the surviving teardown race (rust-sdks), the genuinely new fix:
- Add a runtime teardown-completion signal to
LkRuntime: a static counter + condvar, incremented when a runtime is created ininstance()and decremented at the end ofDrop.instance()waits (bounded, ~10 s, with a loud log on timeout) for the previous runtime to finish dropping before constructing a new factory/ADM — closing theMutex<Weak>create-during-teardown window. Optionally,FfiServer::dispose()waits on the same signal after clearing handles for a clean process-exit ordering. - (Deferred, higher risk — recommend as follow-up, not landed automatically) Drop the
retained
RtcSessioninEngineInner::close(or snapshot stats at close instead of retaining the session). This removes the per-cyclePeerConnectionretention and makes transport destruction deterministic. It touches reconnection/stats semantics that external consumers may depend on, so it should be a separately reviewed change with its own CI.
FramesReachRemoteregression root cause (Step 1) — must be settled before the branch can be considered green; it is currently a hard, deterministic failure in debug CI.PublishManyTracksteardown hang — persists in both post-fix nightlies; needs a Linux backtrace (blocked ongdbinstall, Step 0).- The teardown-completion signal adds a bounded wait in
instance()/dispose(); verified by code inspection to be free of lock-ordering deadlock (the dropping thread never takes theLK_RUNTIMElock), but should be exercised under the triage matrix.