Skip to content

Soundness: Unsound target-feature usage on wasm32 target #106

Description

@Manishearth

Note

This finding was identified during an agentic unsafe Rust code review performed by Gemini AI, followed by human review and verification.

The Issue

The bytecount crate unconditionally enables and calls WebAssembly (WASM) SIMD instructions when compiled for the wasm32 target, regardless of whether the target environment actually supports the simd128 feature.

In src/lib.rs and src/simd/mod.rs, the crate gates the WASM SIMD implementation solely using #[cfg(target_arch = "wasm32")]. The functions in src/simd/wasm.rs (such as chunk_count and u8x16_from_offset) are annotated with #[target_feature(enable = "simd128")].

bytecount/src/lib.rs

Lines 106 to 111 in f06647f

#[cfg(target_arch = "wasm32")]
{
unsafe {
return simd::wasm::chunk_count(haystack, needle);
}
}

#[target_feature(enable = "simd128")]
pub unsafe fn chunk_count(haystack: &[u8], needle: u8) -> usize {

In Rust, calling a function annotated with #[target_feature] is Undefined Behavior (UB) if the host CPU does not support that target feature. Because wasm32-unknown-unknown compiles to a target without simd128 by default, a standard build of this crate for WASM will compile the SIMD functions and execute them unconditionally when the safe public bytecount::count or bytecount::num_chars APIs are called. When run on a WASM runtime that does not support SIMD, this will execute invalid instructions and trigger undefined behavior.

Full Audit Report

Unsafe Rust Review: bytecount (v0_6)

Overall Safety Assessment

The bytecount crate is designed for high-performance byte counting and UTF-8 char counting. To achieve this, it extensively utilizes SIMD vectorization across multiple architectures (x86, x86_64, aarch64, wasm32) and fallback integer-SIMD techniques.

While the core logic of the SIMD algorithms appears to be sound (with correct slice indexing and masking logic to handle tail/straggler elements), the safety documentation is almost entirely absent. The crate contains numerous unsafe fn declarations and unsafe blocks without # Safety documentation or // SAFETY: comments explaining the invariants being upheld.

Importantly, we found a critical target-feature soundness issue on the wasm32 architecture where SIMD instructions are executed unconditionally without runtime verification or compile-time target feature gating.

Critical Findings

Unsound target-feature usage on wasm32 target 🔴 ⚠️

  • Priority: 🔴 High

  • Threat Vector: ⚠️ Accidental Misuse

  • Bug Type: Unsound Target Feature Dispatch

The author provided explicit runtime target feature guards (such as is_x86_feature_detected!) for x86/x86_64 SIMD paths to safely dispatch SIMD instructions based on host capabilities.
However, for the WebAssembly target in src/lib.rs (lines 106-111, 172-177) and src/simd/mod.rs (lines 23-24), the crate gates the WASM SIMD implementation solely using #[cfg(target_arch = "wasm32")]:

#[cfg(target_arch = "wasm32")]
{
    unsafe {
        return simd::wasm::chunk_count(haystack, needle);
    }
}

The functions in src/simd/wasm.rs (such as chunk_count and u8x16_from_offset) are annotated with #[target_feature(enable = "simd128")].

In Rust, calling a function with #[target_feature] is undefined behavior (UB) if the host CPU does not support that target feature. Because wasm32-unknown-unknown compiles to a target without simd128 by default, a standard build of this crate for WASM will compile the SIMD functions and execute them unconditionally when the safe bytecount::count API is called. If run on a WASM runtime that does not support SIMD, this will execute invalid instructions and trigger undefined behavior (or trap/validation errors depending on the engine).

Suggested Fix

Gate the WASM SIMD implementation on target_feature = "simd128" instead of just target_arch = "wasm32":

#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]

This ensures that SIMD instructions are only emitted and called if the user compiles with SIMD support globally enabled (e.g. -C target-feature=+simd128), avoiding UB on non-SIMD runtimes.


Fishy Findings

None.


Missing Safety Comments

The crate lacks # Safety documentation for all of its internal unsafe fn helper functions, and // SAFETY: comments for all of its unsafe blocks. Below are the details and proposed safety comments.

1. usize_load_unchecked in src/integer_simd.rs 🟡

  • Priority: 🟡 Low
  • Bug Type: Missing Safety Documentation

The function lacks safety documentation.

/// # Safety
///
/// The caller must ensure that `offset + size_of::<usize>() <= bytes.len()`.
unsafe fn usize_load_unchecked(bytes: &[u8], offset: usize) -> usize { ... }

2. SIMD Load Helpers (src/simd/aarch64.rs, src/simd/wasm.rs, src/simd/x86_sse2.rs, src/simd/x86_avx2.rs, src/simd/generic.rs) 🟡

  • Priority: 🟡 Low
  • Bug Type: Missing Safety Documentation

All architecture-specific load helpers (e.g. u8x16_from_offset, mm_from_offset, u8x64_from_offset) require bounds guarantees.

Example: mm_from_offset in src/simd/x86_sse2.rs

/// # Safety
///
/// The caller must ensure that:
/// 1. `offset + 16 <= slice.len()`.
/// 2. The `sse2` target feature is supported by the CPU.
#[target_feature(enable = "sse2")]
unsafe fn mm_from_offset(slice: &[u8], offset: usize) -> __m128i {
    _mm_loadu_si128(slice.as_ptr().offset(offset as isize) as *const _)
}

3. Public SIMD entrypoints (chunk_count, chunk_num_chars) 🟡

  • Priority: 🟡 Low
  • Bug Type: Missing Safety Documentation
    Functions like chunk_count and chunk_num_chars in architecture-specific modules are marked pub unsafe fn due to target features but have no # Safety documentation.

Example: chunk_count in src/simd/x86_sse2.rs

/// # Safety
///
/// The caller must ensure that:
/// 1. `haystack.len() >= 16`.
/// 2. The `sse2` target feature is supported by the CPU.
#[target_feature(enable = "sse2")]
pub unsafe fn chunk_count(haystack: &[u8], needle: u8) -> usize { ... }

4. Unsafe Blocks in src/lib.rs 🟡

  • Priority: 🟡 Low
  • Bug Type: Missing Safety Comment
    The unsafe blocks in lib.rs call the target-feature-specific SIMD functions. They should document that the target feature is supported.

Example: AVX2 dispatch in src/lib.rs

if is_x86_feature_detected!("avx2") {
    // SAFETY:
    // 1. `is_x86_feature_detected!("avx2")` guarantees the CPU supports AVX2.
    // 2. `haystack.len() >= 32` is checked by the outer if-statement.
    unsafe {
        return simd::x86_avx2::chunk_count(haystack, needle);
    }
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions