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.
go-ndarray is pure Go (CGO_ENABLED=0) except for its SIMD kernels, which are
Plan 9 assembly generated by go-asmgen
for amd64, arm64, ppc64le, loong64, riscv64 and s390x. 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.TestWrappersRefuseShortOperandspasses each wrapper an operand one element short and requires the panic. -
The kernels stay inside their slices.
TestKernelsStayInsideTheirSlicesandTestGemmMicroStaysInsideItsOperands(internal/kernels/fence_test.go, Linux and macOS) map each operand flush against aPROT_NONEpage, 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
intis an error wrappingErrShapeMismatch, even when a zero dimension makes it empty, as in NumPy. This applies to the shapes operations compute:MatMul,Dot,Inner,Concatenate,Stack, broadcasting andLinspace.Outerhas 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.TestEmptyArraysWithHugeAxesReturnPromptlycovers concatenation, the axis reductions and the scans. -
Memory is the caller's to bound. Validation guarantees a shape is representable, not that it fits in memory, and a Go program that runs out of memory dies (NumPy raises
MemoryError). Broadcasting,Outer,MatMuland summing the empty axis of(0, n)produce more than their operands hold; with untrusted shapes, check the result's size first. -
Fuzzed against an oracle.
FuzzOps(fuzz_test.go) drives slices, reshapes, transposes, squeezes, broadcasting arithmetic, axis reductions,Take,MaskSelect, concatenation,MatMuland scans from arbitrary bytes, and compares every result element by element with an oracle written in the test from NumPy's definitions (Python'sslice.indices, the broadcasting rule, ...). The library must reject exactly the calls the oracle rejects and never panic. -
Decoding takes bytes from anywhere.
UnmarshalBinaryrefuses, with an error wrappingErrFormat, any input whose shape the bytes that follow do not account for exactly, before it allocates; an array it accepts holds no more than it was given.FuzzUnmarshalBinary(marshal_fuzz_test.go) requires that, that it never panics, and that re-encoding is a fixed point.
The minimum Go version (go in go.mod) is kept on a release that fixes the
standard library's known vulnerabilities; govulncheck ./... reports none.
2026-10-11 (after v0.11.0, tests only). v0.10.0 added UnmarshalBinary,
whose input may come from a file, a notebook or the network; no entry covered
it. It checks the magic and the dtype, bounds the rank at 64 and each
dimension at maxSize, validates the shape, and compares the data length with
the element count by dividing, not multiplying, so a complex128 shape cannot
overflow the comparison. Only then does it allocate, so the allocation is the
size of the input. FuzzUnmarshalBinary holds it to that, never panicking
and re-encoding to a fixed point. It ran 11 million inputs on an M-series Mac
with no failure. A copy that accepted fewer bytes than the shape needs failed
it in 0.09 s (6 bytes decoded as 48 bools), so the fuzzer can see the defect
it guards against. govulncheck ./... at go1.27.2 with go-fft v0.23.0 reports
none.
2026-10-09 (v0.9.1). New s390x maxVXE/minVXE kernels, gated on
AT_HWCAP VXRS_EXT (z14 and later), each behind its wrapper: from eight
elements up, with no pointer arithmetic beyond len(a). On the real z17 they
pass TestMaxMinSIMD, the specials-in-every-lane test, the fence tests and the
parallel stress test. A copy using mode 0 (IEEE maxNum, which returns the
number when the other operand is a NaN) fails there, so the NaN rule is
checked, not assumed.
2026-10-09 (v0.9.0).
- The Go floor is 1.27.2. At 1.27.1,
govulnchecklisted standard-library vulnerabilities fixed in 1.27.2 (GO-2026-6604, -6612, -6613, -6617, among others), none of them reachable from this library. At 1.27.2 it reports none, for the library or for its generator module. - The loong64 miscompile is not fixed in 1.27.2. An earlier entry said
golang/go#81000 was fixed in 1.27.2; it was not. The backport
(golang/go#81147) is on the 1.27.3 milestone. On real hardware (cfarm401,
Loongson 3C5000L), the upstream reproducer (
go-gfx/gfx./color,TestSkimageLabToRGBOutOfGamut) FAILS when built with go1.27.1 and with go1.27.2 alike. The evidence that this library does not hit the pattern stands as before: the suite andFuzzOpson that machine. Raise the floor to 1.27.3 when it ships. - The s390x kernels now run on real hardware: an IBM z17, through the
LinuxONE Community Cloud (
/proc/sysinfo: Type 9175, a z/VM 7.3.0 guest with 8 vCPUs in a shared LPAR). The whole suite and the fence tests pass, andsumVXreading one element past the end faults (4 KiB pages). The new 4×8 GEMM micro-kernel is held to the same checks: it is guarded by its wrapper and fenced, and a sabotaged copy that drops tile row 3 fails (MatMul m=50 k=70 n=90 [270]: 0 != -249).
2026-10-08 (v0.8.0). New s390x vector-facility kernels, held to the same
guards (wrappers check each kernel's reach; the PROT_NONE fence tests now
include s390x). ⚠ They were validated under emulation only (Docker's
qemu-s390x locally, qemu-s390x in CI): the whole suite and the fence tests
pass, and two sabotaged kernels (a subtraction with swapped operands, a dot
that does not advance b) both failed. No s390x host was reachable. Found on
the way: hwcapFrom read /proc/self/auxv as little-endian; on big-endian
s390x that would have read the wrong word, so it now uses the machine's byte
order, and useVX comes out true under emulation as it should.
2026-10-08 (v0.7.0). New riscv64 RVV kernels. They are held to the same
guards: each wrapper checks the reach of its kernel (TestWrappersRefuseShortOperands),
and the PROT_NONE fence tests pass on a real SpacemiT X60 with V (cfarm95), as
does the whole suite. On a riscv64 CPU without V (cfarm94, SiFive U74) the same
build takes the scalar path, since V is gated on AT_HWCAP, and passes too. As a
positive control for the fence on this hardware, sumRVV reading one element
past the end faults (4 KiB pages). The
tests were checked against two sabotaged kernels, a subtraction with swapped
operands and a dot that does not advance b; both failed on the real hardware.
2026-10-07 (after v0.6.1, tests only). TestParallelStress runs the
parallel kernels from 6 goroutines at once while another keeps changing
GOMAXPROCS, with the thresholds lowered so that tiny arrays go parallel. The
pool is contended, and some calls take the fork fallback. Every result is
compared with the serial kernel on integer data, so the comparison is exact.
A lost block hangs the call rather than corrupting it, so the test also fails,
by name, when a call does not return in time. Both kinds of sabotage were
caught: a pool that skips the first block of each run (hang) and a fork
fallback that drops the last element (wrong value). It passes under -race
and on real POWER9 (59,154 calls in 5 runs), X-Gene arm64 (65,798) and
Loongson 3C5000L (36,174).
2026-10-07 (v0.6.1). An audit of the helper pool added in v0.6.0 found two defects. Both are fixed, each with a test that fails without the fix.
- Retention. The pool kept the last job, whose closure holds the
operation's arrays, so the arrays of the last parallel operation could not
be collected until another one replaced it: up to the size of the largest
array, for as long as the program ran.
TestPoolDoesNotRetainLastJobchecks the data is collectable onceparallelForreturns. A first version of that test passed on the defective code, because its closure captured a variable the test then set to nil. - Writes after a recovered panic. Since v0.6.0 the caller runs blocks
itself. If one panicked there, for example a
Mapfunction, the panic propagated while the helpers went on writing. A caller that recovered could find its arrays changing under it. The caller now stops handing out blocks and waits for the running ones before the panic propagates (TestPoolCallerPanicStopsTheHelpers). A panic on a helper goroutine still ends the program, as it did before v0.6.0;Map's documentation says so.
2026-10-06 (v0.5.1).
- The Go floor is 1.27.1 since v0.5.0.
govulncheckreports no vulnerability for the library or for its generator module. - go1.27.0 and go1.27.1 miscompile one loong64 pattern
(golang/go#81000: a byte-sized
inlined result spilled with an 8-byte store, clobbering the stack slot next
to it),
fixed in 1.27.2— wrong, corrected 2026-10-09: the backport (golang/go#81147) moved to the 1.27.3 milestone, and go1.27.2 still fails the upstream reproducer on cfarm401 (see the v0.9.0 entry). On a real Loongson 3C5000L with LASX (cfarm401), built with go1.27.1, the whole suite passes, including the fence tests, andFuzzOpsran 52.7 million executions natively without a divergence from its oracle. That is evidence that this library does not hit the pattern, not proof. Raise the floor to 1.27.2 once it is released. - go-asmgen v0.16.0 regenerates the ppc64le and loong64 kernels with
different comments only. The instructions, and the
WORDencodings Go's assembler still lacks, are unchanged line for line.
2026-10-04 (v0.4.0).
FuzzOpsran 47 million executions without a divergence. A first version that only checked an array against its ownCopywas blind: withSqueezedeliberately given wrong strides it found nothing in 31 million executions, because a wrong view reads the same wrong values both ways. The oracle version finds the same sabotage in 11 seconds; its failing input is kept intestdata/fuzz/FuzzOps.- It found two places where the library and NumPy differ.
Slicewith fewer indices than axes was an error; it now takes the rest whole, as NumPy does.MaskSelectaccepts a mask that broadcasts to the array, which NumPy rejects; this is kept as a documented extension. SumAxis/ProdAxisalong a zero-length axis now return 0s/1s as NumPy does (they were errors, unlikeSum/Prodof an empty array). This makes the memory caveat above concrete, and it is documented there.
2026-10-04 (v0.3.0).
govulncheckfound no reachable vulnerability. Atgo 1.26.4, 10 standard library vulnerabilities were in modules the library requires but did not call;go 1.26.6fixes all of them.- Found and fixed:
Concatenatesummed the axis lengths without an overflow check, so 17 arrays of shape(2^59, 0)gave a negative dimension.MatMulof(2^32, 0)by(0, 2^32)returned a(2^32, 2^32)array whoseSize()was 0. A product wrapping to −1 panicked inmakeslice.Linspacewith a count above the limit panicked inmakeslice.Stackof 9 arrays(2^58, 0)returned(9, 2^58, 0), whichNewrejects.- 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
MatMul256²,Dot1DandAddInto16 Ki within ±2%, inside the run-to-run spread.