Skip to content

Latest commit

 

History

History
212 lines (189 loc) · 12.6 KB

SECURITY.md

File metadata and controls

212 lines (189 loc) · 12.6 KB

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

  • 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, MatMul and 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, MatMul and scans from arbitrary bytes, and compares every result element by element with an oracle written in the test from NumPy's definitions (Python's slice.indices, the broadcasting rule, ...). The library must reject exactly the calls the oracle rejects and never panic.

  • Decoding takes bytes from anywhere. UnmarshalBinary refuses, with an error wrapping ErrFormat, 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.

Audit log

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, govulncheck listed 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 and FuzzOps on 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, and sumVX reading 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. TestPoolDoesNotRetainLastJob checks the data is collectable once parallelFor returns. 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 Map function, 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. govulncheck reports 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, and FuzzOps ran 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 WORD encodings Go's assembler still lacks, are unchanged line for line.

2026-10-04 (v0.4.0).

  • FuzzOps ran 47 million executions without a divergence. A first version that only checked an array against its own Copy was blind: with Squeeze deliberately 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 in testdata/fuzz/FuzzOps.
  • It found two places where the library and NumPy differ. Slice with fewer indices than axes was an error; it now takes the rest whole, as NumPy does. MaskSelect accepts a mask that broadcasts to the array, which NumPy rejects; this is kept as a documented extension.
  • SumAxis/ProdAxis along a zero-length axis now return 0s/1s as NumPy does (they were errors, unlike Sum/Prod of an empty array). This makes the memory caveat above concrete, and it is documented there.

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.