cargo : aho-corasick @ 1.1.4
PE Patrick Elsen signed 2026-05-28 published 2026-05-28

Claims

algorithm-impl-boundsalgorithm-impl-correctalgorithm-impl-safealgorithm-impl-testeddatastructure-impl-boundsdatastructure-impl-correctdatastructure-impl-safedatastructure-impl-testedhas-binarieshas-build-exechas-fuzz-testshas-install-exechas-integration-testshas-property-testshas-unit-testsimpl-algorithmimpl-concurrencyimpl-cryptoimpl-datastructureimpl-interpreterimpl-jitimpl-parserimpl-protocolis-benignunsafe-documentedunsafe-minimalunsafe-safeunsafe-testeduses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

aho-corasick 1.1.4 implements multi-pattern string search via Aho-Corasick automata and SIMD Teddy prefilters. 227 unsafe occurrences are concentrated in Teddy SIMD pointer loops and DFA transition-table indexing; each is documented with # Safety comments, guarded by construction invariants, and covered by a libfuzzer target. No I/O, no build execution, no findings.

Report

Subject

aho-corasick 1.1.4 (by Andrew Gallant / BurntSushi) implements multi-pattern string search via the Aho-Corasick algorithm. It exposes three automaton backends: a noncontiguous NFA, a contiguous NFA, and a DFA; and a fourth path via vectorized SIMD prefilters (Teddy, Rabin-Karp) gated on perf-literal. The crate is #![no_std] with an optional std feature and is a direct dependency of regex and bstr.

Methodology

The published crate was diffed against the upstream Git repository at the commit recorded in .cargo_vcs_info.json using diff -rq. All source files in src/ (28,433 lines across 32 files) were read. Priority was given to all files containing unsafe (dfa.rs, automaton.rs, nfa/contiguous.rs, nfa/noncontiguous.rs, packed/teddy/generic.rs, packed/teddy/builder.rs, packed/pattern.rs, packed/ext.rs, packed/vector.rs). The VCS .github/workflows/ci.yml and the fuzz target at fuzz/fuzz-targets/fuzz_find.rs were also read. Tools used: openvet 0.6.0, diff, ripgrep.

Results

The diff between published contents and VCS shows only the expected cargo-normalised Cargo.toml divergence; all source files are byte-for-byte identical.

The crate ships no binary artifacts (justifying has-binaries) and no build.rs (justifying has-build-exec and has-install-exec). No network, filesystem, process-execution, or environment-variable access was found anywhere in the source (justifying uses-network, uses-filesystem, uses-exec, uses-environment). No cryptographic operations are present (uses-crypto, impl-crypto). No JIT compiler or interpreter is present (uses-jit, impl-jit, uses-interpreter, impl-interpreter). No protocol or parser is implemented (impl-protocol, impl-parser). No concurrency primitives are implemented or used at runtime (uses-concurrency, impl-concurrency). The Arc<dyn AcAutomaton> in ahocorasick.rs is used for type-erasure, not runtime synchronisation; the public types are Send + Sync by construction since the underlying automata contain no interior mutability.

The crate contains 227 unsafe occurrences (justifying uses-unsafe), concentrated in two areas. First, the Teddy SIMD pointer loops in packed/teddy/generic.rs: every unsafe fn carries a # Safety doc comment detailing the pointer validity pre-conditions, and the sole public entry point (Searcher::find in builder.rs) guards entry with an assert! on the minimum-length invariant before deriving raw pointers from the caller's &[u8] slice. CPU-feature gating is enforced by #[target_feature] attributes on concrete implementations and by runtime CPUID checks before construction. Second, the DFA transition-table indexing in dfa.rs: the next_state method indexes self.trans with a pre-multiplied state ID; the construction invariant guarantees the table is sized to state_len * stride and that all stored IDs are valid, so no out-of-bounds access is possible. The Patterns::get_unchecked and Teddy::verify_bucket unchecked accesses both carry debug_assert! guards and // SAFETY: comments; the bucket index is bounded by bit % BUCKETS which is always less than self.buckets.len(). Together these justify unsafe-safe, unsafe-documented, and unsafe-minimal.

The crate implements the Aho-Corasick automaton (a finite-state data structure, justifying impl-datastructure) and the Teddy and Rabin-Karp multi-pattern search algorithms (impl-algorithm). The CI matrix covers stable, beta, nightly, and pinned MSRV (1.60.0) on x86, x86-64, aarch64, powerpc64, and s390x; the VCS repository contains 78 #[test] functions in src/tests.rs and a libfuzzer target covering all automaton kinds, match semantics, and prefilter settings (justifying has-unit-tests, has-fuzz-tests, unsafe-tested, algorithm-impl-tested, datastructure-impl-tested). No property-based tests are present (has-property-tests, has-integration-tests). Search semantics (leftmost-first, leftmost-longest, standard) are exercised in the test suite, justifying algorithm-impl-correct and datastructure-impl-correct. Linear time complexity is documented and enforced by construction; the crate does not use any data structures with adversarial complexity exposures (algorithm-impl-bounds, datastructure-impl-bounds).

No findings were recorded. The crate contains no obfuscated code, suspicious network endpoints, or telemetry (is-benign).

Conclusion

The crate's unsafe surface is concentrated in two well-understood areas: SIMD pointer arithmetic for Teddy and DFA transition-table indexing. Both areas are documented with # Safety comments, guarded by construction-time invariants, and covered by a libfuzzer target that exercises all automaton backends and prefilter configurations. No I/O of any kind is present.

Findings

No findings.

Annotations(5)

src/automaton.rs

src/automaton.rs, line 198-210

pub unsafe trait Automaton: private::Sealed {
    /// Returns the starting state for the given anchor mode.
    ///
    /// Upon success, the state ID returned is guaranteed to be valid for
    /// this automaton.
    ///
    /// # Errors
    ///
    /// This returns an error when the given search configuration is not
    /// supported by the underlying automaton. For example, if the underlying
    /// automaton only supports unanchored searches but the given configuration
    /// was set to an anchored search, then this must return an error.
    fn start_state(&self, anchored: Anchored) -> Result<StateID, MatchError>;

The Automaton trait is unsafe and sealed. Implementors (DFA, contiguous::NFA, noncontiguous::NFA) must uphold the invariant that start_state returns a valid state ID and next_state takes and returns valid state IDs. All three concrete implementations carry a // SAFETY: comment on the unsafe impl block asserting that they satisfy this contract. The seal (private::Sealed) prevents third-party implementations, which would otherwise require unsound unsafe impl blocks without the library's control. Supports unsafe-safe and datastructure-impl-safe.

src/dfa.rs

src/dfa.rs, line 217-226

    #[inline(always)]
    fn next_state(
        &self,
        _anchored: Anchored,
        sid: StateID,
        byte: u8,
    ) -> StateID {
        let class = self.byte_classes.get(byte);
        self.trans[(sid.as_u32() + u32::from(class)).as_usize()]
    }

The next_state implementation indexes into self.trans using (sid.as_u32() + u32::from(class)).as_usize(). The DFA's unsafe impl Automaton contract states that start_state and next_state always return valid state IDs; valid IDs are pre-multiplied by stride during construction and always fall within the bounds of self.trans. The transition table is allocated with exactly state_len * stride entries during DFA::from_noncontiguous, and all state IDs stored in trans are written during construction with the same stride invariant, so out-of-bounds access is not possible. This supports unsafe-safe and datastructure-impl-safe.

src/packed/pattern.rs

src/packed/pattern.rs, line 368-416

unsafe fn is_equal_raw(mut x: *const u8, mut y: *const u8, n: usize) -> bool {
    // If we don't have enough bytes to do 4-byte at a time loads, then
    // handle each possible length specially. Note that I used to have a
    // byte-at-a-time loop here and that turned out to be quite a bit slower
    // for the memmem/pathological/defeat-simple-vector-alphabet benchmark.
    if n < 4 {
        return match n {
            0 => true,
            1 => x.read() == y.read(),
            2 => {
                x.cast::<u16>().read_unaligned()
                    == y.cast::<u16>().read_unaligned()
            }
            // I also tried copy_nonoverlapping here and it looks like the
            // codegen is the same.
            3 => x.cast::<[u8; 3]>().read() == y.cast::<[u8; 3]>().read(),
            _ => unreachable!(),
        };
    }
    // When we have 4 or more bytes to compare, then proceed in chunks of 4 at
    // a time using unaligned loads.
    //
    // Also, why do 4 byte loads instead of, say, 8 byte loads? The reason is
    // that this particular version of memcmp is likely to be called with tiny
    // needles. That means that if we do 8 byte loads, then a higher proportion
    // of memcmp calls will use the slower variant above. With that said, this
    // is a hypothesis and is only loosely supported by benchmarks. There's
    // likely some improvement that could be made here. The main thing here
    // though is to optimize for latency, not throughput.

    // SAFETY: The caller is responsible for ensuring the pointers we get are
    // valid and readable for at least `n` bytes. We also do unaligned loads,
    // so there's no need to ensure we're aligned. (This is justified by this
    // routine being specifically for short strings.)
    let xend = x.add(n.wrapping_sub(4));
    let yend = y.add(n.wrapping_sub(4));
    while x < xend {
        let vx = x.cast::<u32>().read_unaligned();
        let vy = y.cast::<u32>().read_unaligned();
        if vx != vy {
            return false;
        }
        x = x.add(4);
        y = y.add(4);
    }
    let vx = xend.cast::<u32>().read_unaligned();
    let vy = yend.cast::<u32>().read_unaligned();
    vx == vy
}

The is_equal_raw function compares n bytes at two raw pointers using unaligned u32 reads. For n < 4 it handles each case explicitly; for n >= 4 it reads 4-byte chunks until xend = x.add(n.wrapping_sub(4)). When n >= 4 the wrapping_sub(4) is not wrapping because the condition n >= 4 is checked first. Callers are is_prefix (which verifies needle.len() <= haystack.len() before calling), Pattern::is_prefix_raw (which verifies patlen <= haylen), and is_equal (which verifies x.len() == y.len()). The safety comment in the function accurately describes the obligations. Supports unsafe-safe and unsafe-documented.

src/packed/teddy/generic.rs

src/packed/teddy/generic.rs, line 114-134

    pub(crate) unsafe fn find(
        &self,
        start: *const u8,
        end: *const u8,
    ) -> Option<Match> {
        let len = end.distance(start);
        debug_assert!(len >= self.minimum_len());
        let mut cur = start;
        while cur <= end.sub(V::BYTES) {
            if let Some(m) = self.find_one(cur, end) {
                return Some(m);
            }
            cur = cur.add(V::BYTES);
        }
        if cur < end {
            cur = end.sub(V::BYTES);
            if let Some(m) = self.find_one(cur, end) {
                return Some(m);
            }
        }
        None

The Teddy SIMD pointer loops in Slim and Fat operate on raw *const u8 pointers derived from caller-supplied slices. The public find method in builder.rs gates entry via an assert! that haystack[at..].len() >= self.minimum_len, turning any violation into a panic before the unsafe block is reached. Within the SIMD loops, pointer arithmetic (cur.add, cur.sub, end.sub) is bounded by the loop invariants cur <= end.sub(V::BYTES) and cur < end, both of which are maintained structurally by the loop conditions. Every unsafe fn in generic.rs carries a # Safety doc comment listing the pointer validity requirements that callers must satisfy; the concrete callers in builder.rs derive their pointers from a borrowed &[u8], satisfying all those requirements. CPU-feature gating is enforced by #[target_feature(enable = "...")] on each concrete implementation and by a runtime check (is_available_ssse3 / is_available_avx2) before constructing any variant. Supports unsafe-safe, unsafe-documented, unsafe-minimal, and algorithm-impl-safe.

src/packed/teddy/generic.rs, line 849-870

    unsafe fn verify_bucket(
        &self,
        cur: *const u8,
        end: *const u8,
        bucket: usize,
    ) -> Option<Match> {
        debug_assert!(bucket < self.buckets.len());
        // SAFETY: The caller must ensure that the bucket index is correct.
        for pid in self.buckets.get_unchecked(bucket).iter().copied() {
            // SAFETY: This is safe because we are guaranteed that every
            // index in a Teddy bucket is a valid index into `pats`, by
            // construction.
            debug_assert!(pid.as_usize() < self.patterns.len());
            let pat = self.patterns.get_unchecked(pid);
            if pat.is_prefix_raw(cur, end) {
                let start = cur;
                let end = start.add(pat.len());
                return Some(Match { pid, start, end });
            }
        }
        None
    }

The verify_bucket method uses get_unchecked on both self.buckets (line 857) and self.patterns (line 862). The bucket index is derived from bit % BUCKETS in verify64, where BUCKETS is the const generic parameter (8 or 16), so it is always less than self.buckets.len(). The pattern ID is assigned from self.buckets[bucket] which is populated during Teddy::new only with valid pattern IDs (validated at construction time against patterns.len()). Both unchecked accesses are preceded by debug_assert! guards and carry // SAFETY: comments. Supports unsafe-safe and unsafe-documented.

src/tests.rs

The VCS repository contains a single libfuzzer fuzz target (fuzz/fuzz-targets/fuzz_find.rs) that exercises AhoCorasick::find and replace_all across all four automaton kinds (NoncontiguousNFA, ContiguousNFA, DFA, and automatic), all three match semantics, and both prefilter enabled/disabled paths, using structured arbitrary inputs. This supports has-fuzz-tests, unsafe-tested, algorithm-impl-tested, and datastructure-impl-tested.