Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/cover.out
*.out
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

[![Docs](https://img.shields.io/badge/docs-mkdocs--material-013243)](https://go-ndarray.github.io/docs/)
[![License](https://img.shields.io/badge/license-BSD--3--Clause-blue)](LICENSE)
[![Go](https://img.shields.io/badge/go-1.26.4%2B-00ADD8)](https://go.dev/dl/)
[![Go](https://img.shields.io/badge/go-1.26.6%2B-00ADD8)](https://go.dev/dl/)
[![Status](https://img.shields.io/badge/status-numpy%20parity%20(float64)-9a6700)](docs/plan-ndarray.md)

**A pure-Go (CGO=0) NumPy-style N-dimensional array library.** Row-major
Expand Down Expand Up @@ -106,7 +106,8 @@ pure-Go `gonum` 4–10× (**[BENCHMARKS.md](BENCHMARKS.md)**). It is a
> the suite on amd64, arm64, 386, and riscv64/loong64/ppc64le/s390x/arm under
> qemu, on Linux, macOS and Windows, and compiles it for every `GOOS/GOARCH`
> pair Go supports; v0.1.0 and v0.2.0 were also run on real amd64, arm64,
> ppc64le, riscv64 and loong64 hardware. See **[docs/plan-ndarray.md](docs/plan-ndarray.md)**
> ppc64le, riscv64 and loong64 hardware, v0.3.0 on real amd64, arm64, ppc64le
> and loong64. See **[docs/plan-ndarray.md](docs/plan-ndarray.md)**
> for the roadmap (more dtypes, more SIMD targets).

## Why this module?
Expand All @@ -117,8 +118,25 @@ pure-Go `gonum` 4–10× (**[BENCHMARKS.md](BENCHMARKS.md)**). It is a
arch is therefore a durable foundation. The numeric loops live in
`internal/kernels`, each with a pure-Go reference implementation;
[go-asmgen](https://github.com/go-asmgen)-generated SIMD kernels replace them
on amd64 and arm64 today, behind the same API and held to the same tests, and
the other 64-bit targets are next.
on amd64, arm64, ppc64le and loong64 today, behind the same API and held to the
same tests; riscv64 and s390x are next.

## Untrusted input

Shapes, indices and data can come from someone else (a file, a request), so
every operation answers a shape it cannot represent with an error wrapping
`ErrShapeMismatch` instead of a panic or a wrong array. That covers results as
well as inputs: `(2^32, 0) @ (0, 2^32)` holds no data, yet its result would
have 2^64 elements, so `MatMul` refuses it, as NumPy does. The one exception
is `Outer`, which has no error result: it panics when its result is too big,
which takes operands of more than 2^30 elements each. An empty array whose
other axis is huge, `(2^58, 0)`, costs nothing to reduce, concatenate or scan.

The assembly kernels take raw pointers. Their Go wrappers check every operand
the kernel reads or writes against the length it is given, so a mistake is an
index panic in Go, never an access outside a slice; a test places each operand
flush against an inaccessible page to prove the kernels themselves stay inside
(see [SECURITY.md](SECURITY.md)).

## Example

Expand Down
67 changes: 67 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Security

## Reporting

Report a vulnerability privately through GitHub's **Report a vulnerability**
button on this repository (Security → Advisories). Please do not open a public
issue for it.

## What the library guarantees

go-ndarray is pure Go (`CGO_ENABLED=0`) except for its SIMD kernels, which are
Plan 9 assembly generated by [go-asmgen](https://github.com/go-asmgen/asmgen)
for amd64, arm64, ppc64le and loong64. Those kernels are the only code Go's
bounds checks do not cover, so they are held to the following:

- **Every wrapper checks the reach of its kernel.** Before an assembly call, the
Go wrapper indexes the last element of every operand the kernel reads or
writes (`internal/kernels/{reduce,gemm}_<arch>.go`). A caller's mistake is an
index-out-of-range panic in Go, never an access outside a slice.
`TestWrappersRefuseShortOperands` passes each wrapper an operand one element
short and requires the panic.
- **The kernels stay inside their slices.** `TestKernelsStayInsideTheirSlices`
and `TestGemmMicroStaysInsideItsOperands` (`internal/kernels/fence_test.go`,
Linux and macOS) map each operand flush against a `PROT_NONE` page, once
ending at it and once starting after one, for every length from 1 to 70 and
every GEMM depth from 1 to 9. A read or write one element past either end is a
fault. The fence itself was checked by a kernel deliberately reading one
element too far, which faults on amd64 (4 KiB pages), arm64 (16 KiB), loong64
(16 KiB) and ppc64le (64 KiB). Prefetch instructions may point past the end:
they are hints and do not fault.
- **Shapes are validated, results included.** A shape whose element count, or
whose size in bytes, does not fit in an `int` is an error wrapping
`ErrShapeMismatch`, even when a zero dimension makes it empty, as in NumPy.
This applies to the shapes operations compute: `MatMul`, `Dot`, `Inner`,
`Concatenate`, `Stack`, broadcasting and `Linspace`. `Outer` has no error
result and panics instead.
- **Empty arrays cost nothing.** Operations on an empty array with a huge axis,
such as `(2^58, 0)`, return at once instead of walking that axis.
`TestEmptyArraysWithHugeAxesReturnPromptly` covers concatenation, the axis
reductions and the scans.

The minimum Go version (`go` in `go.mod`) is kept on a release that fixes the
standard library's known vulnerabilities; `govulncheck ./...` reports none.

## Audit log

**2026-10-04 (v0.3.0).**

- `govulncheck` found no reachable vulnerability. At `go 1.26.4`, 10 standard
library vulnerabilities were in modules the library requires but did not
call; `go 1.26.6` fixes all of them.
- Found and fixed:
- `Concatenate` summed the axis lengths without an overflow check, so 17
arrays of shape `(2^59, 0)` gave a **negative** dimension.
- `MatMul` of `(2^32, 0)` by `(0, 2^32)` returned a `(2^32, 2^32)` array whose
`Size()` was 0. A product wrapping to −1 panicked in `makeslice`.
- `Linspace` with a count above the limit panicked in `makeslice`.
- `Stack` of 9 arrays `(2^58, 0)` returned `(9, 2^58, 0)`, which `New`
rejects.
- Reducing `(2^58, 0)` over axis 0, concatenating it along its last axis, or
scanning `(0, 2^58)` walked the 2^58-long axis: effectively a hang.
- Hardened: the operand checks in front of every assembly call (above). The
fence test found no out-of-bounds access in any kernel, on real amd64
(Zen 3), arm64, ppc64le (POWER9) and loong64 (3C5000L, with LASX).
- Cost: none measurable. A single-core A/B on Zen 3 (10 interleaved rounds)
measured `MatMul` 256², `Dot1D` and `AddInto` 16 Ki within ±2%, inside the
run-to-run spread.
2 changes: 1 addition & 1 deletion benchmarks/verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestDumpForVerify(t *testing.T) {
put(tag+".mid", flat.At(flat.Size()/2))
}

x, y := vec(1 << 16), vec(1 << 16)
x, y := vec(1<<16), vec(1<<16)
r, _ := x.Add(y)
putArr("add", r)
r, _ = x.Mul(y)
Expand Down
Loading
Loading