Seven bugs. All silent. All invisible on Linux CI. One corrupted the heap on every over-aligned allocation, another overflowed the stack on a 10,000-node expression tree, and a third aborted the process pre-allocating memory Linux would have lazily shrugged off. None of them needed a single line of unsafe to trigger — they needed Windows.
Today we released SciRS2 0.6.3 — a Windows-compatibility hardening release that closes seven crash-and-corruption bugs that only (or mostly) manifested on Windows, plus two numerical-correctness fixes in signal processing found while chasing those platform differences across the codebase.
SciRS2 stays what it has always been: no C, no Fortran, no CUDA Toolkit, no system BLAS — a single static binary (or WASM) that compiles and runs everywhere. 0.6.3 makes “everywhere” include Windows without the asterisk: the heap corruption, stack overflows, and process aborts that only showed up there are gone.
Why SciRS2 0.6.3 is a game changer
Cross-platform correctness bugs are the worst kind precisely because they don’t show up where you’re looking:
- An over-aligned buffer allocated by hand and freed through a container that assumes the type’s natural alignment — undefined behavior that glibc quietly tolerates and Windows’ allocator does not.
- A compiler-derived recursive
Dropimpl on a tree-shaped type, invisible until someone builds a deep enough tree on a platform with a 1 MB default thread stack instead of Linux’s 8 MB. - “Free” pre-allocation that costs nothing under Linux’s overcommit, because Windows commits the whole reservation up front and aborts when it can’t.
- A path validator that flags the drive-letter colon in
C:\Users\...as an invalid character, rejecting every absolute Windows path by construction. - Least-squares solved via normal equations, which silently returns an arbitrary huge-norm answer instead of erroring the moment the design matrix is rank-deficient.
SciRS2 0.6.3 ends all of that:
- Heap corruption, fixed in two places.
scirs2-spatial’sDistancePoolandscirs2-stats’sAdaptiveMemoryManagerboth had alloc/dealloc mismatches that corrupted the heap on Windows (STATUS_HEAP_CORRUPTION) while running fine on Linux. - Stack overflow, fixed.
scirs2-symbolic’sEmlNodenow has an explicit iterativeDropinstead of a compiler-derived recursive one. - Process abort, fixed.
scirs2-transform’s PCA memory pool no longer eagerly reserves gigabytes it doesn’t need. - Path validation, fixed.
scirs2-coreno longer rejects every absolute Windows path. - Numerical correctness, fixed. ESPRIT phase estimation and N4SID system identification in
scirs2-signalboth go through corrected, SVD-based solves now.
Technical Deep Dive: four bugs Linux CI could never have caught
The alignment mismatch, precisely. DistancePool::create_aligned_buffer and create_numa_aware_buffer allocated via System.alloc at a 64-byte alignment, then wrapped the raw pointer in a Box<[f64]> — whose Drop deallocates assuming f64‘s natural 8-byte alignment. That mismatch between how memory was allocated and how it was freed is undefined behavior full stop; glibc’s allocator happens to tolerate it, but Windows’ allocator returns an offset pointer with a bookkeeping header for over-aligned requests, so freeing at the wrong base corrupts the heap. Both methods now allocate through plain Vec/Box<[f64]>, at no measurable cost — nothing in the module actually depended on 64-byte alignment. scirs2-stats’s AdaptiveMemoryManager had the same shape of bug from a different angle: infer_deallocation_strategy re-derived the free strategy from the configured allocation_strategy, which is wrong whenever the config is Adaptive — allocate resolves Adaptive to a concrete per-call strategy like Pool, so deallocate could free through the wrong allocator entirely. allocate now records the resolved strategy per pointer, and deallocate looks it up instead of guessing.
The stack overflow, in the one place iteration wasn’t already the rule. Every tree traversal in scirs2-symbolic‘s expression-tree module was already written iteratively, specifically to avoid stack blowouts on deep trees — except Drop, which the compiler derives recursively and which no amount of iterative traversal code elsewhere can fix. A 10,000-node left-chain expression going out of scope fit comfortably in Linux’s 8 MB default thread stack and blew straight through Windows’ 1 MB default (STATUS_STACK_OVERFLOW). EmlNode now has an explicit iterative Drop that dismantles the tree via a worklist, descending only into children it uniquely owns (Arc::try_unwrap-able).
The overcommit illusion. scirs2-transform’s AdvancedMemoryPool::prewarm_common_sizes eagerly allocated up to 25 spare copies of every common PCA matrix size just to construct an AdvancedPCA — one 50,000×500 entry alone was 25 × 200 MB, roughly 5.5 GB in total. Linux’s overcommit and lazily-faulted zero pages hid that cost almost entirely; Windows backs the whole reservation up front and aborted the process outright. Pre-warming is now capped by a 64 MB total budget, spent smallest-size-first with a per-size copy cap — sizes that don’t fit the budget still allocate normally on first use.
Numerical correctness: when a “solved” system is actually singular. eigenvalues_francis_qr’s double-shift QR — the core of ESPRIT phase estimation — had two bugs in francis_double_step: the bulge term read a structurally-zero matrix entry instead of the correct sub-diagonal one, and the right-multiply loop stopped one row short, leaving a sub-diagonal entry un-rotated so the step silently stopped being a similarity transform. The deflation driver also now always searches for the active block from the bottom down, instead of tracking a position across iterations that could drive a step across an interior zero and corrupt both sides of a block boundary. Separately, n4sid_estimate solved its least-squares steps via normal equations (solve(XᵀX, XᵀY)), which squares the condition number — for any input that isn’t persistently exciting of order 2i (a single sinusoid, the most common smoke-test input, excites only two directions regardless of record length), XᵀX is singular and the LU-based solve silently returned an arbitrary huge-norm result instead of erroring. Both solves now go through a new pseudoinverse_product: minimum-norm least squares from a relative-tolerance-truncated thin SVD.
Getting Started
The path-validation fix, usable today with no feature flags:
cargo add scirs2-core
use scirs2_core::validation::cross_platform::validate_path;
fn main() {
// Before 0.6.3: the leading drive specifier (`C:`) was scanned along
// with the rest of the path, so the colon tripped the invalid-character
// check and every absolute Windows path failed validation.
assert!(validate_path(r"C:\Users\kitasan\data.csv").is_ok());
// Still correctly rejected -- these characters are illegal anywhere
// in a Windows path component, drive letter or not.
assert!(validate_path(r"C:\Users\kitasan\bad?name.csv").is_err());
}
What’s New in 0.6.3
Fixed
scirs2-signal: ESPRIT’s Francis double-shift QR eigenvalue drift, and a deflation driver that could corrupt block boundaries.scirs2-signal:n4sid_estimate’s normal-equations solve silently returning bogus solutions for rank-deficient inputs — now an SVD-based minimum-norm solve via a newpseudoinverse_product.scirs2-spatial:DistancePool’s aligned-buffer alloc/dealloc mismatch corrupting the heap on Windows.scirs2-stats:AdaptiveMemoryManagerfreeingAdaptive-configured allocations through the wrong strategy.scirs2-symbolic:EmlNode’s recursiveDropglue overflowing the stack on deep expression trees.scirs2-transform:AdvancedMemoryPool::prewarm_common_sizeseagerly reserving ~5.5 GB, aborting the process on Windows.scirs2-core:CrossPlatformValidator::validate_windows_pathrejecting every absolute Windows path over its own drive-letter colon.scirs2-core:profiling::memory_profiling::read_os_memory_infohad no real Windows backend — now backed byK32GetProcessMemoryInfo(kernel32) reporting realWorkingSetSize/PagefileUsage.tools/cargo-scirs2-policy: exempt-path matching compared against\-separated Windows paths against/-separated patterns and never matched — paths are now normalized before comparison.
Changed
- Dependency bumps:
oxifft0.3.2→0.4.1,oxicuda-*0.5.1→0.5.3,oxiz0.2.4→0.3.0.
See CHANGELOG.md [0.6.3] for the complete list.
Tips
- Audit any hand-rolled aligned allocation that gets freed through
Box<[T]>/Vec<T>. Those containers’DropassumesT‘s natural alignment — pairing an over-alignedallocwith a default-aligneddeallocis undefined behavior that Linux’s allocator happens to tolerate and Windows’ does not. - If you have a deep, recursively-
Drop’d tree type — parser ASTs, expression trees, linked structures — give it an explicit iterativeDrop. Iterative traversal everywhere else in your code doesn’t help; the compiler still derivesDroprecursively unless you write it by hand. - Don’t trust Linux’s overcommit to make “eager pre-allocation” free. Any cache-warming logic that reserves memory it may never touch should be budgeted, not assumed harmless — Windows will make you pay for the whole reservation up front.
- If you validate or construct paths for cross-platform code, test explicitly against
C:\...inputs. A Windows-only path-format regression will never surface in Linux CI no matter how thorough the rest of your suite is. - Prefer an SVD-based pseudoinverse over normal equations for any least-squares solve where the design matrix could be rank-deficient. Normal equations square the condition number, and a singular
XᵀXcan silently return a huge-norm “solution” instead of erroring. - Re-run any ESPRIT- or N4SID-based system-identification pipeline built before 0.6.3. The corrected Francis QR and the new pseudoinverse-based solve can change results that previously looked converged but weren’t.
This is the foundation
SciRS2 0.6.3 is the sovereign scientific-computing layer of the COOLJAPAN ecosystem — and a release that closes silent corruption and silent numerical drift in the same cycle matters precisely because so much builds on top of it:
- NumRS2 — NumPy-compatible N-dimensional arrays in pure Rust.
- PandRS — Pandas-compatible DataFrames.
- OptiRS — advanced ML optimizers extending SciRS2.
- ToRSh — a PyTorch-compatible deep-learning framework.
- SkleaRS — a scikit-learn-compatible ML library.
- TrustformeRS — Hugging Face Transformers in pure Rust.
Every one of these inherits SciRS2’s memory-management and numerical layers along with everything else — a heap corruption bug in a pooled allocator is one less crash a downstream training job hits three hours into a Windows CI run for reasons nobody can reproduce on their own machine.
Repository: https://github.com/cool-japan/scirs
Star the repo if you’d rather find out about a platform-specific bug from a fixed CHANGELOG entry than from a Windows machine that corrupted its own heap at 3am.
The era of “it works on my machine” is over. Pure Rust scientific computing is here — fast, safe, and correct on every platform it claims to support.
— KitaSan at COOLJAPAN OÜ July 27, 2026