diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 38861e6f..56b2319c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -162,3 +162,57 @@ jobs: run: >- cargo package --locked --all-features + + asan: + strategy: + # Don't give up on the whole matrix if one variant fails. + fail-fast: false + matrix: + # Only native targets: the sanitizer runtime must run on the build host. + include: + - target: aarch64-apple-darwin + runner_os: macos-15 + + - target: x86_64-unknown-linux-gnu + runner_os: ubuntu-24.04 + + runs-on: ${{ matrix.runner_os }} + + steps: + - name: Install Rust toolchain + # Sanitizers are only available on nightly Rust. + uses: dtolnay/rust-toolchain@nightly + + - name: Install cargo-binstall + uses: cargo-bins/cargo-binstall@main + + - name: Install Cargo helpers + run: >- + cargo binstall just + + - name: Use LLVM Clang for native code on macOS + if: startsWith(matrix.runner_os, 'macos-') + # Code instrumented by Apple Clang does not link against the sanitizer + # runtime from the LLVM bundled with Rust (see README of `open62541-sys`). + run: | + brew install llvm + echo "CC=$(brew --prefix llvm)/bin/clang" >> "$GITHUB_ENV" + + # Check out the repository before the remaining steps that depend on it. + # All preceding steps are independent of the repository contents. + - name: Check out repository + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Cache Rust toolchain and build artifacts + uses: Swatinem/rust-cache@v2 + with: + # The cache should not be shared between different workflows, jobs, and targets. + shared-key: ${{ github.workflow }}-${{ github.job }}-${{ matrix.target }}-${{ matrix.runner_os }} + + # The recipe sets the target explicitly, which keeps the sanitizer flags + # away from build scripts and proc macros. + - name: Run tests (bins/lib/tests/examples) with AddressSanitizer + run: >- + just test-asan diff --git a/.justfile b/.justfile index 29b9af32..90dd50e3 100644 --- a/.justfile +++ b/.justfile @@ -26,6 +26,21 @@ test: # would exclude the doctests. Specifying none of them gives the desired result. RUST_BACKTRACE=1 cargo test --locked --all-features -- --nocapture +# Run unit tests under AddressSanitizer with leak detection (requires nightly toolchain) +test-asan: + # Doctests are excluded, because they would also need `RUSTDOCFLAGS` to be instrumented. + just _asan test --locked --all-features --bins --lib --tests --examples + +# Run cargo command under AddressSanitizer with leak detection (requires nightly toolchain) +_asan +args: + #!/usr/bin/env bash + set -euo pipefail + # An explicit target keeps `RUSTFLAGS` from applying to build scripts and proc macros. + export CARGO_BUILD_TARGET="$(rustc +nightly -vV | sed -n 's/^host: //p')" + export RUSTFLAGS="-Zsanitizer=address --cfg asan" + export ASAN_OPTIONS="detect_leaks=1" + cargo +nightly {{ args }} + # Set up (and update) development tools setup: # Ignore rustup failures, because not everyone might use it diff --git a/src/ua/array.rs b/src/ua/array.rs index e0e3bc18..c6ee9ddd 100644 --- a/src/ua/array.rs +++ b/src/ua/array.rs @@ -601,15 +601,17 @@ mod tests { // Create array in `open62541`, delete locally. // - let size = LEN; - let ptr = unsafe { UA_Array_new(size, T::data_type()) }.cast::<::Inner>(); + let mut size = LEN; + let mut ptr = + unsafe { UA_Array_new(size, T::data_type()) }.cast::<::Inner>(); // Copy value with allocated data into array to catch double free. let string = CString::new(STRING).unwrap(); *unsafe { ptr.add(POS).as_mut() }.unwrap() = unsafe { UA_NODEID_STRING_ALLOC(0, string.as_ptr()) }; drop(string); - let array: Array = Array::from_raw_parts(size, ptr).unwrap(); + // Take ownership; copying with `from_raw_parts()` would leak the original allocation. + let array: Array = unsafe { Array::move_from_raw_parts(&mut size, &mut ptr) }.unwrap(); assert_eq!(array.len(), LEN); drop(array);