COOLJAPAN
← All posts

SciRS2 0.6.3 Released — Heap Corruption, Stack Overflows, and Silent Numerical Drift on Windows, All Fixed

SciRS2 0.6.3 fixes seven Windows-only crash-and-corruption bugs invisible on Linux CI: an alloc/dealloc alignment mismatch that corrupted the heap, a recursive Drop impl that overflowed the 1MB default stack, a ~5.5GB eager-allocation process abort, and a path validator that rejected every C:\ path — plus two numerical-correctness fixes in ESPRIT and N4SID system identification. Pure Rust, Apache-2.0.

release scirs2 rust scientific-computing windows memory-safety correctness

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:

SciRS2 0.6.3 ends all of that:

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 Adaptiveallocate 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

Changed

See CHANGELOG.md [0.6.3] for the complete list.

Tips

  1. Audit any hand-rolled aligned allocation that gets freed through Box<[T]>/Vec<T>. Those containers’ Drop assumes T‘s natural alignment — pairing an over-aligned alloc with a default-aligned dealloc is undefined behavior that Linux’s allocator happens to tolerate and Windows’ does not.
  2. If you have a deep, recursively-Drop’d tree type — parser ASTs, expression trees, linked structures — give it an explicit iterative Drop. Iterative traversal everywhere else in your code doesn’t help; the compiler still derives Drop recursively unless you write it by hand.
  3. 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.
  4. 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.
  5. 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ᵀX can silently return a huge-norm “solution” instead of erroring.
  6. 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:

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

↑ Back to all posts