Released 2026-05-18. Tags: v1.6.0.
Package: sublinear-time-solver (npm), consciousness-explorer (npm), sublinear (crates.io).
Yes, immediately, if any of these apply to you:
- You expose the MCP tools to anything other than trusted local clients. This release closes a remotely-exploitable arbitrary file-write (CWE-73) reported by @BruceJqs in issue #19. The same bug existed in two MCP tools —
export_state(consciousness-explorer) andsaveVectorToFile(main package). Both are fixed in 1.6.0. - You actually call the Neumann solver on systems with n ≥ 64. It silently diverged on larger matrices because the convergence check used the wrong residual. Now correct.
- You build on macOS Apple Silicon. The previous version had an unconditional
_rdtsc()call that wouldn't compile on arm64. Now arch-portable.
Breaking change for one specific pattern: the MCP tools export_state, import_state, saveVectorToFile, loadVectorFromFile now accept a basename only (e.g. "snapshot.json"), not an absolute path ("/tmp/snapshot.json") or a path with separators. Files go into a dedicated directory — see Upgrading below.
In plain English: sublinear-time-solver is a library for solving linear systems A·x = b faster than reading the whole matrix.
Concretely:
- You have a sparse
n × nmatrixAand a vectorb, and you want to findxsuch thatA·x = b. - Classical solvers (Gaussian elimination, LU, conjugate gradient on the full matrix) all touch every nonzero in
Aat least once, so they'reO(nnz)at best. - Sublinear solvers can compute individual entries of the solution
x(or estimates ofb · x, or other reductions) without ever materialising the full solution. For diagonally-dominant matrices this can beO(log n)per query — yes, you read that right, sub-linear in the matrix size.
This is the algorithm class from Kyng, Sachdeva 2016: Approximating the Solution to Mixed Packing and Covering LPs in Parallel Õ(ε⁻³) Time and subsequent work. It's not for every problem — only diagonally-dominant systems with cheap-to-query rows — but where it applies, it's very fast.
The package wraps a Rust core (also published as the sublinear crate) with TypeScript/Node.js bindings, a CLI, an MCP server, and a WASM build for the browser.
[CVE candidate — issue #19, CWE-73 Arbitrary File Write] Reported by BruceJin on 2026-04-17 against the consciousness-explorer MCP server.
The vulnerable pattern was:
// vulnerable: src/consciousness-explorer/index.js (1.1.1 and earlier)
async exportState(filepath) {
const state = { /* ... */ };
fs.writeFileSync(filepath, JSON.stringify(state, null, 2)); // ← attacker-controlled
}An attacker with network access to the MCP interface could call export_state with filepath = "/etc/cron.d/evil" and write arbitrary content as the MCP process. CVSS 3.1: 7.1 High (AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H; raise AV to N if the MCP is reachable via a remote bridge).
The fix introduces src/consciousness-explorer/lib/safe-path.js, a tiny defence-in-depth module that:
- Confines every state file to a dedicated directory (
~/.consciousness-explorer/stateby default, override with$CONSCIOUSNESS_EXPLORER_STATE_DIR). - Accepts basename only — rejects path separators (
/,\),..,., leading dot, NUL/control chars, oversize names (>255 bytes), Windows reserved device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9). - Re-verifies containment with
path.relativeafterpath.resolve(defence against platform-specific path-canonicalisation quirks). - Opens with
O_NOFOLLOW | O_CLOEXECmode0o600so a symlink planted at the final path component cannot redirect the I/O. - Creates the state directory at mode
0o700if missing.
The MCP tool schemas advertise the new contract via JSON Schema pattern: ^[^/\\\x00]+$, minLength: 1, maxLength: 255 so MCP clients see the constraint at tools/list time.
Defence in depth: while remediating, the same sink class was found in the main sublinear-time-solver MCP server's saveVectorToFile and loadVectorFromFile tools. Fixed identically via a TypeScript counterpart src/mcp/safe-path.ts with a separate vector directory (~/.sublinear-time-solver/vectors, override $SUBLINEAR_SOLVER_VECTOR_DIR).
14 regression tests in tests/consciousness/safe-path.test.mjs pin the contract — basename validation, traversal payload rejection, symlink-redirect refusal, state dir mode 0o700.
- Neumann solver: residual now checked against the original RHS. A pre-existing TODO in
update_residualadmitted it was comparingA·xagainst the scaled RHSD⁻¹b— i.e. computing the residual of a different equation entirely. The convergence check therefore fired against the wrong quantity, and the solver outright diverged at n ≥ 64 on diagonally-dominant test matrices. Now storesoriginal_rhsand computesr = A·x − bcorrectly. As a bonus, n=16 cases are 47% faster because the corrected residual allows correct early-exit. - Neumann: k=0 term no longer double-counted.
solutionwas initialised toD⁻¹bandcompute_next_termimmediately added another copy — a 2×2 system that should converge to[1, 1]ended at[2, 2]. - Sublinear-Neumann base case: more iterations.
solve_base_casewas hard-coded to 10 Jacobi iters, ~30 short of the typical convergence point on 2×2 test systems. Now driven bymax_recursion_depthwith a 64-iter floor. - Conjugate gradient: instrumentation correctness. Hot loops inlined every dot product and AXPY directly, so
performance_stats.dot_product_countandaxpy_countstayed at 0 the whole run. Routed through the existing instrumented helpers; SIMD/scalar dispatch is unchanged. - JL embedding:
target_dimcapped atoriginal_dim − 1. For tight ε and modest n, the rawk = O(log n / ε²)could exceednitself — a dimensional expansion dressed up as a reduction. Capped so embeddings are always strictly dimension-reducing.
TscTimestamp::now()is now arch-portable. Was unconditionallycore::arch::x86_64::_rdtsc(), socargo buildfailed on Apple Silicon. Three gated paths: x86_64 → RDTSC; aarch64 → inline-asmmrs cntvct_el0(the virtual counter register at 24 MHz on Apple Silicon); everything else →Instant::now()fallback.- Several physics validators had wrong-units or wrong-tolerance bugs (inverted division in
calculate_maximum_time, absolute tolerance of 1e-50 for ℏ = h/(2π) which is below f64 ULP at that scale, etc.). Fixed. DecoherenceTracker::dephasing_ratenow scales with temperature (was hardcoded to 1 GHz, so 10 mK cryogenic and 300 K room-temp reported identical coherence times).EntanglementValidatorgains three previously-stub methods:analyze_consciousness_time_scales,model_consciousness_network,calculate_quantum_fisher_information.
- New
.github/workflows/ci.ymlwith 4 gating jobs:cargo teston Ubuntu + macOS,fmt + clippy,safe-path regression(the #19 test suite),cargo bench --quick(proves the bench corpus compiles + runs). - New
BENCHMARK.mdwith baseline numbers. - Fresh
benches/solver_benchmarks.rs. The previous bench corpus referenced removed modules (fast_solver,core,algorithms,solver::hybrid) and would not compile. The broken files are archived underbenches/.archived/. - 23 new tests across the fixes above.
From cargo bench --bench solver_benchmarks -- --quick on a Ryzen 9 7950X / 64 GB. Test matrix is n × n diagonally dominant (5 on the diagonal, ±1 on the four nearest off-diagonals with wrap).
| Solver | n=16 | n=64 | n=256 | Throughput @ n=256 |
|---|---|---|---|---|
| Optimized CG (symmetric matrices) | 198 ns | 316 ns | 816 ns | ~314 Melem/s |
| Neumann series (general DD matrices) | 3.6 µs | 12.6 µs | 51.5 µs | ~5.0 Melem/s |
Read of the numbers: Optimized CG is 40–60× faster than Neumann across all three sizes on symmetric inputs. Use CG when you can; use Neumann when the matrix is asymmetric and CG doesn't apply. Both throughputs scale linearly with n (as expected for sparse iterative solvers).
Comparison to v1.5.0: where Neumann ran at all on the bench matrices (n ≤ 32 in the old version, because larger sizes diverged), it's ~47% faster in 1.6.0 because the corrected residual lets the solver exit as soon as it actually converges, instead of running until the iteration cap.
Full bench harness: BENCHMARK.md.
- Solve
A·x = bwith three algorithm families: Neumann series (default for general DD), Conjugate Gradient (optimized, symmetric), and adaptive random walk (variance reduction). - Estimate single entries
x[i]without materialising the full solution —O(log n)per entry on DD systems with cheap row access. - Matrix analysis: condition number, diagonal dominance score, sparsity, symmetry checks.
- Sublinear preconditioners: Johnson-Lindenstrauss dimension reduction, importance sampling, matrix sketching (
AdaptiveSampler). - MCP interface: every algorithm above is exposed as an MCP tool, so any MCP-aware client (Claude Code, Cursor, etc.) can call them natively.
- CLI (
npx sublinear-time-solver): generate test matrices, solve from JSON files, analyse properties, compare methods. - WASM: 25-qubit-equivalent matrix sizes run in the browser via the WASM bundle.
- Optional consciousness module: quantum coherence validators (Margolus-Levitin, energy-time uncertainty, decoherence tracking, entanglement), strange-loop / identity tracking. Research-oriented; not load-bearing for the solver core.
# No install required — npx pulls 1.6.0
npx sublinear-time-solver generate -t diagonally-dominant -s 1000 -o matrix.json
node -e "console.log(JSON.stringify(Array(1000).fill(1)))" > vector.json
npx sublinear-time-solver solve -m matrix.json -b vector.json -o solution.json
# Analyse a matrix
npx sublinear-time-solver analyze -m matrix.json --full
# Compare solver methods on the same system
npx sublinear-time-solver solve -m matrix.json -b vector.json --method neumann
npx sublinear-time-solver solve -m matrix.json -b vector.json --method forward-push
npx sublinear-time-solver solve -m matrix.json -b vector.json --method random-walk
# Run as MCP server (for Claude Code etc.)
npx sublinear-time-solver mcpimport { solve, generateMatrix } from "sublinear-time-solver";
const matrix = generateMatrix({ type: "diagonally-dominant", size: 1000 });
const b = Array(1000).fill(1);
const result = await solve(matrix, b, { method: "conjugate-gradient" });
console.log(result.solution); // → length-1000 Float64Array
console.log(result.iterations); // → e.g. 14
console.log(result.residual_norm); // → e.g. 3.2e-11# Cargo.toml
[dependencies]
sublinear = "0.2"use sublinear::{SparseMatrix, NeumannSolver, SolverAlgorithm, SolverOptions};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let matrix = SparseMatrix::from_triplets(
vec![(0, 0, 5.0), (0, 1, 1.0), (1, 0, 2.0), (1, 1, 7.0)],
2, 2,
)?;
let b = vec![6.0, 9.0];
let solver = NeumannSolver::new(16, 1e-8);
let result = solver.solve(&matrix, &b, &SolverOptions::default())?;
println!("solution = {:?}", result.solution);
println!("converged in {} iterations", result.iterations);
Ok(())
}// .mcp.json
{
"mcpServers": {
"sublinear": { "command": "npx", "args": ["sublinear-time-solver", "mcp"] }
}
}Then in conversation:
Generate a 5000×5000 diagonally-dominant matrix, solve A·x = b where b is all-ones, and tell me the L2 norm of x.
The model will route to generateTestMatrix, saveVectorToFile (basename only!), and solveTrueSublinear automatically.
Breaking change affects callers of these four MCP tools:
| Tool | Old behaviour (1.5.0) | New behaviour (1.6.0) |
|---|---|---|
export_state |
accepted absolute path | rejects path separators, .., leading ., etc. Basename only |
import_state |
accepted absolute path | basename only, no symlink follow |
saveVectorToFile |
accepted absolute path | basename only |
loadVectorFromFile |
accepted absolute path | basename only |
Files go into the dedicated state directory:
- consciousness-explorer:
~/.consciousness-explorer/state/(override with$CONSCIOUSNESS_EXPLORER_STATE_DIR) - main package:
~/.sublinear-time-solver/vectors/(override with$SUBLINEAR_SOLVER_VECTOR_DIR)
If you had filepath: "/tmp/snapshot.json" → change to filepath: "snapshot.json" (and optionally set $CONSCIOUSNESS_EXPLORER_STATE_DIR=/tmp if you really need /tmp).
If you had filepath: "../shared/data.json" → restructure the layout so files live inside the state directory, or set the env var to that directory and use a basename.
Everything else is wire-compatible. The library and CLI APIs are unchanged.
The interesting bit is the double-check against traversal payloads. Path validation is famously hard because of platform-specific quirks: NTFS is case-insensitive, macOS is case-preserving-but-insensitive by default, ext4 follows symlinks differently than tmpfs, etc.
The safe-path module belt-and-braces:
// Step 1: structural validation (cheap, catches 99% of attacks)
assertSafeBasename(name); // rejects /, \, .., ., leading ., NUL, ctrl chars,
// Windows reserved names, oversize
// Step 2: realpath containment (catches Mac/Windows quirks)
const candidate = path.resolve(baseAbs, name);
const rel = path.relative(baseAbs, candidate);
if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${sep}`)) {
throw new SafePathError(`resolved path escapes state dir`);
}
// Step 3: O_NOFOLLOW at I/O time (catches TOCTOU symlink races)
const fd = fs.openSync(absPath, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW
| O_CLOEXEC, 0o600);The third step is what makes this resistant to time-of-check-to-time-of-use races, where an attacker who can write inside the state directory could replace a file with a symlink between the validate-and-open steps. O_NOFOLLOW makes open() itself fail if the final path component is a symlink, closing the window.
The tests exercise all three layers: structural rejection (basename-only payloads), containment rejection (../../etc/passwd), and symlink rejection (a planted symlink inside the state dir pointing at /tmp/victim).
Honest scope-setting:
- Not a CVE yet. A GitHub Security Advisory citing CWE-73 and the three published versions will follow once CVE coordination with @BruceJqs is complete. The fix itself is shipped.
- Not a rewrite. This is a focused security + correctness release. The architecture is unchanged; the MCP server still exposes the same tool surface (with a tighter input contract).
- Not a performance breakthrough. The benchmarks are baselines, not records. The CG implementation is well-tuned but not micro-optimised (no AVX-512, no GPU). The Neumann series is faster than before because it actually converges where it used to diverge.
- Not a quantum-computing library. The
quantum::module is a set of validators that check physical bounds (Margolus-Levitin, uncertainty, decoherence) for nanosecond-scale operations. Useful for sanity-checking timing budgets in temporal-prediction code; not a state-vector simulator.
- @BruceJqs for the disclosure of issue #19 (Apr 17, 2026), including a reproducible PoC via mcp-inspector. The fix would have shipped weeks later without his report.
- The Kyng/Sachdeva 2016 and Andoni/Krauthgamer/Pogrow 2018 papers for the sublinear-solver theory.
- The Rust crypto and security communities for
O_NOFOLLOWbest practices.
- 📦 npm: https://www.npmjs.com/package/sublinear-time-solver
- 📦 npm: https://www.npmjs.com/package/consciousness-explorer
- 📦 crates.io: https://crates.io/crates/sublinear
- 📖 CHANGELOG: https://github.com/ruvnet/sublinear-time-solver/blob/main/CHANGELOG.md
- 📊 BENCHMARK: https://github.com/ruvnet/sublinear-time-solver/blob/main/BENCHMARK.md
- 🔐 Security issue (resolved): ruvnet/sublinear-time-solver#19
- 🛠 Repository: https://github.com/ruvnet/sublinear-time-solver
🤖 Release shepherded with claude-flow.