A wrong-answer bug that agreed with the correct answer at exactly 45 degrees. That is precisely specific enough to hide from every test that didn’t happen to probe that angle.
Today we released OxiFFT 0.4.2 — a correctness release. Its headline fix: every non-AVX2 x86_64 CPU running OxiFFT’s SIMD path was computing wrong FFT results, not slightly-imprecise ones. It shipped that way because the project’s primary development host is aarch64, there is no CI, and this was the first time the suite ran on x86_64 at all.
No C. No Fortran. No FFTW. OxiFFT is a Pure Rust FFT library — this release doesn’t change that, it changes what the x86_64 SIMD tier actually computes.
Why OxiFFT 0.4.2 is a game changer
cargo nextest run -p oxifft --all-features --target x86_64-apple-darwin (via Rosetta 2, which exposes SSE3 and no AVX) failed 93 of 1447 tests — errors of magnitude 10-100 against a 1e-10 tolerance, cascading through chirp_z, conv, the DFT codelets and solvers, rdft::solvers::r2r, signal::{hilbert,resample}, sparse, and the size-coverage sweeps. Everything that bottoms out in the shared SIMD butterfly engine was affected. Two independent defects in the SSE3 kernel every non-AVX2 x86_64 CPU takes:
- Wrong complex-multiply lane order. The twiddle product shuffled its second partial product into the wrong slot before
_mm_addsub_pd, computing[v_re·cos − v_im·cos, v_re·sin + v_im·sin]instead of[v_re·cos − v_im·sin, v_re·sin + v_im·cos]. The two expressions agree only wherecos == sin— 45 degrees — which is exactly why the smallest radix-2 stages masked the bug for so long. - Twiddle recurrence drift. The kernel advanced twiddles with
w *= w_stepper butterfly, accumulating roughlyhalf_m · EPSILONof error per stage — invisible against a loose scalar comparison, but enough to push Bluestein’s chirp round-trip past its tight 1e-13 gate at specific sizes.
OxiFFT 0.4.2 ends both: the shuffle is removed (addsub’s operand was already in the correct lane order), and the kernel now reads the same precomputed twiddle table the AVX2 and NEON kernels already used. x86_64 is now 1455/1455 passing, aarch64 stays green at 1765/1765, and a new absolute-accuracy test (test_simd_butterfly_matches_naive_dft, checked against a directly evaluated DFT rather than the equally-drifting scalar recurrence) pins it down.
Stated plainly, because it matters: Rosetta 2 only exercises the SSE2/SSE3 tier. Every AVX/AVX2/AVX-512 code path has still never executed on real hardware — no AVX2-capable machine is available to this project. One function, dit_butterflies_avx2 (the tier a real post-2013 desktop or server takes), was hand-audited lane-by-lane as part of this fix and checks out. Don’t read “x86_64 fixed” as “the AVX tier is verified.”
Technical Deep Dive: everything else in 0.4.2
- Miri-driven pointer-provenance fix. The first Miri run over the crate’s unsafe surface flagged
api::parallel::RawPtr’sptr as usize/usize as *mut Tround trip — it strips provenance, so derived pointer arithmetic was no longer provably in-bounds under Stacked/Tree Borrows.RawPtrnow carries*mut u8directly; Miri is clean overapi::parallel,api::memory,api::plan,kernel::complex_mul,dft::problem, anddft::plan(98 tests). - Wisdom-string hardening. Wisdom is attacker-influenceable input — importable from a string, file, or system path — and an entry is only
(size, name, cost). A planted(1024 "nop" 1.0)used to makeexecute_inplacesilently return the input unchanged as an “FFT result”; a planted(6 "ct-dit" 1.0)drove the radix-2 engine past its debug-only size guard. Nine more solver-name arms are now re-validated against the transform size the same way"composite"/"rader"already were. - AVX-512 entry-point bounds checks. Six
pubdispatch_hand_avx512_size{16,32,64}_{f32,f64}functions handed any-length safe slices to raw-pointer codelets that unconditionally read/wrote exactly N elements — an out-of-bounds access reachable with nounsafein the caller. Each nowassert_eq!s the slice length first. ThreadPool::parallel_for’s “exactly once per index” invariant is no longer just trusted. A new one-shotIndexClaimstable makes every raw-pointer partitioning site (row/column/fiber splits) degrade to incomplete results instead of out-of-bounds writes or aliased&mutif a pool implementation ever violates the contract.
Getting Started
[dependencies]
oxifft = { version = "0.4.3", features = ["sparse", "streaming", "signal"] }
use oxifft::{fft, ifft, Complex};
let data = vec![Complex::new(1.0, 0.0); 256];
let spectrum = fft(&data)?;
let recovered = ifft(&spectrum)?;
What’s New in 0.4.2
- Fixed: the x86_64 SSE3 wrong-answer SIMD bug above; a precomputed-twiddle-table overrun for
n > 65536that now falls back to the scalar path instead of panicking;GuruPlan’s negative-offset check promoted fromdebug_assert!toassert!so release builds report the same diagnostic as debug;ThreadPool::parallel_for_chunks/parallel_splitno longer divide by zero or overflow on degenerate arguments. - Security: the Miri pointer-provenance fix and wisdom-string hardening above;
DftProblem::sz/vecszare no longer public fields (nowpub(crate)with read-only accessors, closing a path where safe code could enlargeszpast the buffer anunsafeconstructor was promised);api::parallel’s extent computations are now overflow-checked. - Added:
oxifft-adapter-mpi’sMpiFlags::transposed_inis now implemented across all slab plan families (verified with realmpirunat 1-4 ranks); the WebAssemblysimd128backend is now actually reachable from transform dispatch instead of silently falling back to scalar; a 5th fuzz target (wisdom_measure_roundtrip) exercising the wisdom-gating path. - Documentation: SVE and WebAssembly SIMD claims corrected across README/PROJECT_STATUS/oxifft.md — SVE now reads a real vector length from
/proc/sys/abi/sve_default_vector_lengthon aarch64 Linux instead of a hardcoded 0; newSECURITY.md.
Tips
- If you build with default features on x86_64 without AVX2, upgrade now. This isn’t a performance release — it’s the difference between correct and silently wrong FFT output on that tier.
- AVX2/AVX-512 users: the audit trail is honest about what’s unverified.
dit_butterflies_avx2was hand-audited and checks out; the rest of the AVX/AVX-512 surface has never run on real hardware. If you’re on that tier and can run the test suite, the maintainers would want to know either way. - Never load wisdom from an untrusted source without treating it as attacker input — this release closes two concrete ways a crafted wisdom string could reach either a no-op “success” or an undersized-transform panic, but the underlying lesson (wisdom = external input) doesn’t go away.
- If you use
oxifft-adapter-mpi,transposed_inon the inverse now works — the standard FFTW-MPItransposed_outforward +transposed_ininverse idiom (avoids onealltoallvper round trip) is finally expressible; checklocal_footprints()(new this release) if you need to size a single in-place buffer for asymmetric flag combinations. n > 65536requests that used to panic now silently take the scalar path — correct, but slower; if you’re hitting that size regularly, it’s worth knowing why rather than being surprised by a performance change.
This is the foundation
Correct SIMD arithmetic and Miri-clean unsafe code matter most for whatever builds numerically on top. SciRS2, NumRS2, ToRSh, TrustFormers, SkLearS, VoiRS, OxiONNX, OxiMedia, and Kizzasi all pin oxifft for FFT/DFT/DCT transforms.
Repository: https://github.com/cool-japan/oxifft
Star the repo if “we ran it on the other architecture for the first time and found a wrong-answer bug” is the kind of honesty you want from a library you depend on.
The era of SIMD kernels nobody ran on the second architecture is over. Pure Rust FFT that’s fast, safe, and sovereign — is here.
— KitaSan at COOLJAPAN OÜ August 6, 2026