From 2cfcdaf5f073c28be4f0f6b518de0fe2473e0894 Mon Sep 17 00:00:00 2001 From: Giovanni Petrantoni <7008900+sinkingsugar@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:35:20 +0800 Subject: [PATCH 1/3] fix(coreaudio): make data_size mutable in audio_devices() to fix sanitizer crashes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In audio_devices(), data_size was declared as immutable but AudioObjectGetPropertyDataSize needs to write the actual property size into it. This caused undefined behavior that sanitizers (ASAN/TSAN) detected as writes to immutable memory. This bug was introduced in commit ed9d643 when migrating from coreaudio-rs to objc2-core-audio. The old raw pointer code hid the UB with an unsafe cast (&data_size as *const _ as *mut _), but the new NonNull API properly enforces mutability contracts. The fix changes: - let data_size = 0u32; → let mut data_size = 0u32; - NonNull::from(&data_size) → NonNull::from(&mut data_size) Fixes crashes during device enumeration under AddressSanitizer and ThreadSanitizer on macOS. --- src/host/coreaudio/macos/enumerate.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/host/coreaudio/macos/enumerate.rs b/src/host/coreaudio/macos/enumerate.rs index 4f337352f..bacf67763 100644 --- a/src/host/coreaudio/macos/enumerate.rs +++ b/src/host/coreaudio/macos/enumerate.rs @@ -26,13 +26,13 @@ unsafe fn audio_devices() -> Result, OSStatus> { }; } - let data_size = 0u32; + let mut data_size = 0u32; let status = AudioObjectGetPropertyDataSize( kAudioObjectSystemObject as AudioObjectID, NonNull::from(&property_address), 0, null(), - NonNull::from(&data_size), + NonNull::from(&mut data_size), ); try_status_or_return!(status); @@ -45,7 +45,7 @@ unsafe fn audio_devices() -> Result, OSStatus> { NonNull::from(&property_address), 0, null(), - NonNull::from(&data_size), + NonNull::from(&mut data_size), NonNull::new(audio_devices.as_mut_ptr()).unwrap().cast(), ); try_status_or_return!(status); From b866a26a1c2a97c820b73c36e18e6d798bd2f283 Mon Sep 17 00:00:00 2001 From: Giovanni Petrantoni <7008900+sinkingsugar@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:42:53 +0800 Subject: [PATCH 2/3] fix(coreaudio): make all data_size variables mutable in device.rs Fixed 7 more instances of the same UB pattern where data_size was declared immutable but passed to CoreAudio APIs that write to it: In set_sample_rate() (lines 64, 81, 128): - AudioObjectGetPropertyData writes the actual data size back - Both calls to get sample rates and sample rate ranges In Device::id() (line 376): - AudioObjectGetPropertyData for kAudioDevicePropertyDeviceUID In Device::supported_configs() (lines 420, 471): - AudioObjectGetPropertyDataSize writes the required buffer size - AudioObjectGetPropertyData confirms the size In Device::default_config() (line 598): - AudioObjectGetPropertyData for stream format All of these were causing sanitizer crashes because the CoreAudio APIs expect mutable references (the size parameter is 'inout'). --- src/host/coreaudio/macos/device.rs | 34 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index 91470e393..5e4b19c11 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -61,14 +61,14 @@ fn set_sample_rate( mElement: kAudioObjectPropertyElementMaster, }; let mut sample_rate: f64 = 0.0; - let data_size = mem::size_of::() as u32; + let mut data_size = mem::size_of::() as u32; let status = unsafe { AudioObjectGetPropertyData( audio_device_id, NonNull::from(&property_address), 0, null(), - NonNull::from(&data_size), + NonNull::from(&mut data_size), NonNull::from(&mut sample_rate).cast(), ) }; @@ -78,14 +78,14 @@ fn set_sample_rate( if sample_rate as u32 != target_sample_rate.0 { // Get available sample rate ranges. property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; - let data_size = 0u32; + let mut data_size = 0u32; let status = unsafe { AudioObjectGetPropertyDataSize( audio_device_id, NonNull::from(&property_address), 0, null(), - NonNull::from(&data_size), + NonNull::from(&mut data_size), ) }; coreaudio::Error::from_os_status(status)?; @@ -98,7 +98,7 @@ fn set_sample_rate( NonNull::from(&property_address), 0, null(), - NonNull::from(&data_size), + NonNull::from(&mut data_size), NonNull::new(ranges.as_mut_ptr()).unwrap().cast(), ) }; @@ -125,7 +125,7 @@ fn set_sample_rate( // Send sample rate updates back on a channel. let sample_rate_handler = move || { let mut rate: f64 = 0.0; - let data_size = mem::size_of::() as u32; + let mut data_size = mem::size_of::() as u32; let result = unsafe { AudioObjectGetPropertyData( @@ -133,7 +133,7 @@ fn set_sample_rate( NonNull::from(&sample_rate_address), 0, null(), - NonNull::from(&data_size), + NonNull::from(&mut data_size), NonNull::from(&mut rate).cast(), ) }; @@ -373,7 +373,7 @@ impl Device { // CFString is copied from the audio object, use wrap_under_create_rule let mut uid: *mut CFString = std::ptr::null_mut(); - let data_size = size_of::<*mut CFString>() as u32; + let mut data_size = size_of::<*mut CFString>() as u32; // SAFETY: AudioObjectGetPropertyData is documented to write a CFString pointer // for kAudioDevicePropertyDeviceUID. We check the status code before use. @@ -383,7 +383,7 @@ impl Device { NonNull::from(&property_address), 0, null(), - NonNull::from(&data_size), + NonNull::from(&mut data_size), NonNull::from(&mut uid).cast(), ) }; @@ -417,13 +417,13 @@ impl Device { unsafe { // Retrieve the devices audio buffer list. - let data_size = 0u32; + let mut data_size = 0u32; let status = AudioObjectGetPropertyDataSize( self.audio_device_id, NonNull::from(&property_address), 0, null(), - NonNull::from(&data_size), + NonNull::from(&mut data_size), ); check_os_status(status)?; @@ -434,7 +434,7 @@ impl Device { NonNull::from(&property_address), 0, null(), - NonNull::from(&data_size), + NonNull::from(&mut data_size), NonNull::new(audio_buffer_list.as_mut_ptr()).unwrap().cast(), ); check_os_status(status)?; @@ -468,13 +468,13 @@ impl Device { // See https://github.com/thestk/rtaudio/blob/master/RtAudio.cpp#L1369C1-L1375C39 property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; - let data_size = 0u32; + let mut data_size = 0u32; let status = AudioObjectGetPropertyDataSize( self.audio_device_id, NonNull::from(&property_address), 0, null(), - NonNull::from(&data_size), + NonNull::from(&mut data_size), ); check_os_status(status)?; @@ -486,7 +486,7 @@ impl Device { NonNull::from(&property_address), 0, null(), - NonNull::from(&data_size), + NonNull::from(&mut data_size), NonNull::new(ranges.as_mut_ptr()).unwrap().cast(), ); check_os_status(status)?; @@ -595,13 +595,13 @@ impl Device { unsafe { let mut asbd: AudioStreamBasicDescription = mem::zeroed(); - let data_size = mem::size_of::() as u32; + let mut data_size = mem::size_of::() as u32; let status = AudioObjectGetPropertyData( self.audio_device_id, NonNull::from(&property_address), 0, null(), - NonNull::from(&data_size), + NonNull::from(&mut data_size), NonNull::from(&mut asbd).cast(), ); default_config_error_from_os_status(status)?; From 418dd62b9c2e9b36738d2a5bbac904cc8426f2d6 Mon Sep 17 00:00:00 2001 From: Giovanni Petrantoni <7008900+sinkingsugar@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:46:44 +0800 Subject: [PATCH 3/3] ci: add sanitizer builds to catch undefined behavior Added comprehensive sanitizer testing for both macOS and Linux: - AddressSanitizer (ASAN) for memory safety bugs - ThreadSanitizer (TSAN) for race conditions and threading issues These sanitizers run the existing test suite with runtime instrumentation to catch undefined behavior that normal builds miss. This would have caught the data_size mutability bugs fixed in the previous commits. The workflow runs on both platforms since CoreAudio (macOS) and ALSA (Linux) have different code paths, and UB can be platform-specific. --- .github/workflows/sanitizers.yml | 185 +++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 .github/workflows/sanitizers.yml diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml new file mode 100644 index 000000000..0c0c73b32 --- /dev/null +++ b/.github/workflows/sanitizers.yml @@ -0,0 +1,185 @@ +name: Sanitizers + +on: + push: + branches: [master] + paths-ignore: + - "**.md" + - "docs/**" + - "LICENSE*" + - ".gitignore" + pull_request: + branches: [master] + paths-ignore: + - "**.md" + - "docs/**" + - "LICENSE*" + - ".gitignore" + workflow_dispatch: + +jobs: + asan: + name: AddressSanitizer (macOS) + runs-on: macOS-latest + steps: + - uses: actions/checkout@v5 + + - name: Install dependencies + run: brew install llvm + + - name: Install nightly Rust with rust-src + uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + key: asan-macos + + - name: Run tests with AddressSanitizer + env: + RUSTFLAGS: -Zsanitizer=address + RUSTDOCFLAGS: -Zsanitizer=address + run: | + cargo +nightly test \ + -Zbuild-std \ + --target aarch64-apple-darwin \ + --lib \ + --verbose + + - name: Upload ASAN logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: asan-logs-macos + path: | + asan.log.* + /tmp/asan.* + if-no-files-found: ignore + + tsan: + name: ThreadSanitizer (macOS) + runs-on: macOS-latest + steps: + - uses: actions/checkout@v5 + + - name: Install dependencies + run: brew install llvm + + - name: Install nightly Rust with rust-src + uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + key: tsan-macos + + - name: Run tests with ThreadSanitizer + env: + RUSTFLAGS: -Zsanitizer=thread + RUSTDOCFLAGS: -Zsanitizer=thread + TSAN_OPTIONS: second_deadlock_stack=1 + run: | + cargo +nightly test \ + -Zbuild-std \ + --target aarch64-apple-darwin \ + --lib \ + --verbose + + - name: Upload TSAN logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: tsan-logs-macos + path: | + tsan.log.* + /tmp/tsan.* + if-no-files-found: ignore + + asan-linux: + name: AddressSanitizer (Linux) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Cache Linux audio packages + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: libasound2-dev libjack-jackd2-dev libjack-jackd2-0 libdbus-1-dev + + - name: Install nightly Rust with rust-src + uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + key: asan-linux + + - name: Run tests with AddressSanitizer + env: + RUSTFLAGS: -Zsanitizer=address + RUSTDOCFLAGS: -Zsanitizer=address + run: | + cargo +nightly test \ + -Zbuild-std \ + --target x86_64-unknown-linux-gnu \ + --lib \ + --verbose + + - name: Upload ASAN logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: asan-logs-linux + path: | + asan.log.* + /tmp/asan.* + if-no-files-found: ignore + + tsan-linux: + name: ThreadSanitizer (Linux) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Cache Linux audio packages + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: libasound2-dev libjack-jackd2-dev libjack-jackd2-0 libdbus-1-dev + + - name: Install nightly Rust with rust-src + uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + key: tsan-linux + + - name: Run tests with ThreadSanitizer + env: + RUSTFLAGS: -Zsanitizer=thread + RUSTDOCFLAGS: -Zsanitizer=thread + TSAN_OPTIONS: second_deadlock_stack=1 + run: | + cargo +nightly test \ + -Zbuild-std \ + --target x86_64-unknown-linux-gnu \ + --lib \ + --verbose + + - name: Upload TSAN logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: tsan-logs-linux + path: | + tsan.log.* + /tmp/tsan.* + if-no-files-found: ignore