Skip to content

Instantly share code, notes, and snippets.

@ruvnet
Created May 18, 2026 23:16
Show Gist options
  • Select an option

  • Save ruvnet/342518ef950348c376bc7c04ffeb5337 to your computer and use it in GitHub Desktop.

Select an option

Save ruvnet/342518ef950348c376bc7c04ffeb5337 to your computer and use it in GitHub Desktop.
sublinear-time-solver 1.6.0 — security fix (#19 CWE-73 path traversal), solver correctness overhaul, full CI bring-up

🚀 sublinear-time-solver 1.6.0 — security fix, correctness overhaul, full CI

Released 2026-05-18. Tags: v1.6.0. Package: sublinear-time-solver (npm), consciousness-explorer (npm), sublinear (crates.io).

TL;DR — should you upgrade?

Yes, immediately, if any of these apply to you:

  1. 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) and saveVectorToFile (main package). Both are fixed in 1.6.0.
  2. 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.
  3. 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.


What does this thing actually do?

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 × n matrix A and a vector b, and you want to find x such that A·x = b.
  • Classical solvers (Gaussian elimination, LU, conjugate gradient on the full matrix) all touch every nonzero in A at least once, so they're O(nnz) at best.
  • Sublinear solvers can compute individual entries of the solution x (or estimates of b · x, or other reductions) without ever materialising the full solution. For diagonally-dominant matrices this can be O(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.


What's new in 1.6.0

🔒 Security (the big one)

[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/state by 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.relative after path.resolve (defence against platform-specific path-canonicalisation quirks).
  • Opens with O_NOFOLLOW | O_CLOEXEC mode 0o600 so a symlink planted at the final path component cannot redirect the I/O.
  • Creates the state directory at mode 0o700 if 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.

🧮 Solver correctness

  • Neumann solver: residual now checked against the original RHS. A pre-existing TODO in update_residual admitted it was comparing A·x against the scaled RHS D⁻¹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 stores original_rhs and computes r = A·x − b correctly. 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. solution was initialised to D⁻¹b and compute_next_term immediately 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_case was hard-coded to 10 Jacobi iters, ~30 short of the typical convergence point on 2×2 test systems. Now driven by max_recursion_depth with a 64-iter floor.
  • Conjugate gradient: instrumentation correctness. Hot loops inlined every dot product and AXPY directly, so performance_stats.dot_product_count and axpy_count stayed at 0 the whole run. Routed through the existing instrumented helpers; SIMD/scalar dispatch is unchanged.
  • JL embedding: target_dim capped at original_dim − 1. For tight ε and modest n, the raw k = O(log n / ε²) could exceed n itself — a dimensional expansion dressed up as a reduction. Capped so embeddings are always strictly dimension-reducing.

⚡ Quantum / temporal validators

  • TscTimestamp::now() is now arch-portable. Was unconditionally core::arch::x86_64::_rdtsc(), so cargo build failed on Apple Silicon. Three gated paths: x86_64 → RDTSC; aarch64 → inline-asm mrs 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_rate now scales with temperature (was hardcoded to 1 GHz, so 10 mK cryogenic and 300 K room-temp reported identical coherence times).
  • EntanglementValidator gains three previously-stub methods: analyze_consciousness_time_scales, model_consciousness_network, calculate_quantum_fisher_information.

🏗 Infrastructure

  • New .github/workflows/ci.yml with 4 gating jobs: cargo test on Ubuntu + macOS, fmt + clippy, safe-path regression (the #19 test suite), cargo bench --quick (proves the bench corpus compiles + runs).
  • New BENCHMARK.md with 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 under benches/.archived/.
  • 23 new tests across the fixes above.

Benchmarks

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.


Capabilities

  • Solve A·x = b with 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.

Quick start

CLI

# 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 mcp

Programmatic — Node.js

import { 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

Programmatic — Rust

# 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 from Claude Code (or any MCP client)

// .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.


Upgrading from 1.5.x

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.


Technical details — how the safe-path module works

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).


What this release is NOT

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.

Acknowledgements

  • @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_NOFOLLOW best practices.

Links

🤖 Release shepherded with claude-flow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment