// Copyright 2026 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build goexperiment.simd && (amd64 || arm64) && linux package simd_test import ( "fmt" "path/filepath" "reflect" "runtime" "runtime/debug" "simd/archsimd" "strings" "syscall" "testing" "unsafe" ) // The tests in this file check that the Load*Part functions access only the // elements of their slice argument. They place the slice at the start and at // the end of a page that lies between two inaccessible pages, so that any // access outside the slice faults. See also bytes/boundary_test.go. func TestLoadPartPageBoundary128(t *testing.T) { testLoadPartPageBoundary(t, archsimd.LoadInt8x16Part) testLoadPartPageBoundary(t, archsimd.LoadInt16x8Part) testLoadPartPageBoundary(t, archsimd.LoadInt32x4Part) testLoadPartPageBoundary(t, archsimd.LoadInt64x2Part) testLoadPartPageBoundary(t, archsimd.LoadUint8x16Part) testLoadPartPageBoundary(t, archsimd.LoadUint16x8Part) testLoadPartPageBoundary(t, archsimd.LoadUint32x4Part) testLoadPartPageBoundary(t, archsimd.LoadUint64x2Part) testLoadPartPageBoundary(t, archsimd.LoadFloat32x4Part) testLoadPartPageBoundary(t, archsimd.LoadFloat64x2Part) } // HasLenAndStore is implemented by the vector types returned by the Load*Part // functions. type HasLenAndStore[T number] interface { Len() int Store(s []T) } // testLoadPartPageBoundary runs a subtest that checks that load, a Load*Part // function, reads only the elements of its argument and loads them correctly. // It tries every slice length from 1 to the vector length, with the slice at // each end of a page whose neighbors are inaccessible. func testLoadPartPageBoundary[T number, V HasLenAndStore[T]](t *testing.T, load func(s []T) (V, int)) { name := runtime.FuncForPC(reflect.ValueOf(load).Pointer()).Name() name = name[strings.LastIndexByte(name, '.')+1:] t.Run(name, func(t *testing.T) { var zero V n := zero.Len() size := int(unsafe.Sizeof(T(0))) page := guardedPage(t) for l := 1; l <= n; l++ { for _, off := range []int{0, len(page) - l*size} { s := unsafe.Slice((*T)(unsafe.Pointer(&page[off])), l) for i := range s { s[i] = T(i + 1) } v, got, fault := loadCatchingFault(load, s) if fault != nil { start := uintptr(unsafe.Pointer(&s[0])) where := "past the end of" if fault.addr < start { where = "before the start of" } t.Fatalf("%s read %s a %d-element slice at [%#x, %#x): fault at %#x in %s", name, where, l, start, start+uintptr(l*size), fault.addr, fault.loc) } if got != l { t.Errorf("%s(s) with len(s) = %d returned %d, want %d", name, l, got, l) } gotElems := make([]T, n) v.Store(gotElems) wantElems := make([]T, n) copy(wantElems, s) checkSlicesLogInput(t, gotElems, wantElems, 0.0, func() { t.Helper(); t.Logf("len(s) = %d", l) }) } } }) } // guardedPage returns a page of memory that is immediately preceded and // followed by inaccessible pages. func guardedPage(t *testing.T) []byte { t.Helper() size := syscall.Getpagesize() mem, err := syscall.Mmap(-1, 0, 3*size, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_ANON|syscall.MAP_PRIVATE) if err != nil { t.Fatalf("mmap failed: %v", err) } t.Cleanup(func() { if err := syscall.Munmap(mem); err != nil { t.Errorf("munmap failed: %v", err) } }) if err := syscall.Mprotect(mem[:size], syscall.PROT_NONE); err != nil { t.Fatalf("mprotect of low page failed: %v", err) } if err := syscall.Mprotect(mem[2*size:], syscall.PROT_NONE); err != nil { t.Fatalf("mprotect of high page failed: %v", err) } return mem[size : 2*size : 2*size] } // A faultInfo describes a memory fault that the runtime turned into a panic. type faultInfo struct { addr uintptr // the address whose access faulted loc string // the function and source line that made the access } // loadCatchingFault returns load(s). If load faults, it returns a description // of the fault instead of crashing. func loadCatchingFault[T number, V any](load func(s []T) (V, int), s []T) (v V, n int, fault *faultInfo) { old := debug.SetPanicOnFault(true) defer debug.SetPanicOnFault(old) defer func() { r := recover() if r == nil { return } err, ok := r.(interface{ Addr() uintptr }) if !ok { panic(r) } fault = &faultInfo{addr: err.Addr(), loc: faultLocation()} }() v, n = load(s) return v, n, nil } // faultLocation describes the memory access that caused the panic in // progress: the function and source line that made it, followed by the // callers inside package archsimd, which end with the function that the test // called. A deferred function must call faultLocation while that panic is // being handled, when the faulting frame is still on the stack. func faultLocation() string { pcs := make([]uintptr, 64) frames := runtime.CallersFrames(pcs[:runtime.Callers(1, pcs)]) // Skip to runtime.sigpanic. The frame below it made the access. for { frame, more := frames.Next() if !more { return "unknown location" } if frame.Function == "runtime.sigpanic" { break } } var locs []string for { frame, more := frames.Next() if len(locs) > 0 && !strings.HasPrefix(frame.Function, "simd/archsimd.") { break } locs = append(locs, fmt.Sprintf("%s (%s:%d)", frame.Function, filepath.Base(frame.File), frame.Line)) if !more { break } } return strings.Join(locs, ", called from ") }