Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9262990ce5 | ||
|
|
d9f6167a4d | ||
|
|
f5088c52fc | ||
|
|
f52e23f1bc |
+34
-3
@@ -29,7 +29,7 @@ import (
|
|||||||
|
|
||||||
// version is the release version, stamped at build time via
|
// version is the release version, stamped at build time via
|
||||||
// -ldflags "-X main.version=…" (defaulting to the current release).
|
// -ldflags "-X main.version=…" (defaulting to the current release).
|
||||||
var version = "0.17.0"
|
var version = "0.21.0"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
@@ -472,7 +472,7 @@ requires -p, the package path, and the installed Go toolchain).
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdVerify(args []string) int {
|
func cmdVerify(args []string) int {
|
||||||
fs := newCommand("verify", "gasm verify <file.s>", `
|
fs := newCommand("verify", "gasm verify [-smoke] [-abi] [-profile] <file.s>", `
|
||||||
Assemble FILE (amd64), map it into executable memory and report the available
|
Assemble FILE (amd64), map it into executable memory and report the available
|
||||||
functions. This confirms the assembled image is self-consistent (no
|
functions. This confirms the assembled image is self-consistent (no
|
||||||
unresolved external symbols) and executable — the prerequisite for dynamic
|
unresolved external symbols) and executable — the prerequisite for dynamic
|
||||||
@@ -481,11 +481,18 @@ testing.
|
|||||||
With -smoke, each NOSPLIT function is called with a zeroed argument block to
|
With -smoke, each NOSPLIT function is called with a zeroed argument block to
|
||||||
confirm the JIT trampoline works end-to-end. This is safe only for functions
|
confirm the JIT trampoline works end-to-end. This is safe only for functions
|
||||||
that tolerate nil pointers and zero lengths in their arguments.
|
that tolerate nil pointers and zero lengths in their arguments.
|
||||||
|
|
||||||
|
With -abi, each function is called with sentinel values in the callee-saved
|
||||||
|
registers (BP, R14) and a red-zone canary below SP; violations are reported.
|
||||||
|
|
||||||
|
With -profile, the static basic-block structure is listed for each function.
|
||||||
`)
|
`)
|
||||||
smoke := fs.Bool("smoke", false, "call each NOSPLIT function with zeroed args")
|
smoke := fs.Bool("smoke", false, "call each NOSPLIT function with zeroed args")
|
||||||
|
abi := fs.Bool("abi", false, "run ABI-checking calls (sentinel registers + red zone)")
|
||||||
|
profile := fs.Bool("profile", false, "list basic-block structure per function")
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
if fs.NArg() != 1 {
|
if fs.NArg() != 1 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] <file.s>")
|
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-profile] <file.s>")
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
path := fs.Arg(0)
|
path := fs.Arg(0)
|
||||||
@@ -511,6 +518,16 @@ that tolerate nil pointers and zero lengths in their arguments.
|
|||||||
flags = " NOSPLIT"
|
flags = " NOSPLIT"
|
||||||
}
|
}
|
||||||
fmt.Printf(" %s: %d bytes, args=%d, frame=%d%s\n", name, fl.Size, fl.Args, fl.Frame, flags)
|
fmt.Printf(" %s: %d bytes, args=%d, frame=%d%s\n", name, fl.Size, fl.Args, fl.Frame, flags)
|
||||||
|
|
||||||
|
if *profile {
|
||||||
|
blocks, err := k.Blocks(name)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" profile: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" blocks: %d\n", len(blocks))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if *smoke && fl.NoSplit {
|
if *smoke && fl.NoSplit {
|
||||||
args := make([]byte, fl.Args)
|
args := make([]byte, fl.Args)
|
||||||
_, err := k.CallFunc(name, args)
|
_, err := k.CallFunc(name, args)
|
||||||
@@ -521,6 +538,20 @@ that tolerate nil pointers and zero lengths in their arguments.
|
|||||||
fmt.Printf(" smoke: OK\n")
|
fmt.Printf(" smoke: OK\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *abi && fl.NoSplit {
|
||||||
|
args := make([]byte, fl.Args)
|
||||||
|
_, report, err := k.CallFuncChecked(name, args)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" abi: FAIL — %v\n", err)
|
||||||
|
rc = 1
|
||||||
|
} else if !report.OK() {
|
||||||
|
fmt.Printf(" abi: %s\n", report)
|
||||||
|
rc = 1
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" abi: clean\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return rc
|
return rc
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
|
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
|
||||||
|
|
||||||
version := "0.17.0"
|
version := "0.21.0"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@just --list
|
@just --list
|
||||||
|
|||||||
Vendored
+28
@@ -0,0 +1,28 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// func cleanAdd(a, b int64) int64
|
||||||
|
// A well-behaved function that preserves all callee-saved registers.
|
||||||
|
TEXT ·cleanAdd(SB), NOSPLIT, $0-24
|
||||||
|
MOVQ a+0(FP), AX
|
||||||
|
ADDQ b+8(FP), AX
|
||||||
|
MOVQ AX, ret+16(FP)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func dirtyBP(a int64) int64
|
||||||
|
// Deliberately clobbers BP (an ABI violation for a NOSPLIT frame=0 function).
|
||||||
|
TEXT ·dirtyBP(SB), NOSPLIT, $0-16
|
||||||
|
MOVQ $0x1234, BP
|
||||||
|
MOVQ a+0(FP), AX
|
||||||
|
MOVQ AX, ret+8(FP)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func dirtyR14(a int64) int64
|
||||||
|
// Deliberately clobbers R14 (the goroutine pointer — a serious ABI violation).
|
||||||
|
TEXT ·dirtyR14(SB), NOSPLIT, $0-16
|
||||||
|
MOVQ $0x5678, R14
|
||||||
|
MOVQ a+0(FP), AX
|
||||||
|
MOVQ AX, ret+8(FP)
|
||||||
|
RET
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build amd64
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// abiResult records register-clobber violations detected by the ABI-checking
|
||||||
|
// trampoline. Bit 0: BP clobbered. Bit 1: R14 clobbered.
|
||||||
|
var abiResult uint64
|
||||||
|
|
||||||
|
// savedBP holds the caller's frame pointer across the ABI-checked JIT call.
|
||||||
|
// Referenced by enterJITChecked to satisfy go vet's save-before-clobber rule.
|
||||||
|
var savedBP uintptr
|
||||||
|
|
||||||
|
// leaveCheckedPtr is initialised by the linker from the GLOBL/DATA in
|
||||||
|
// abi_amd64.s: it holds the raw address of leaveJITCheckedRaw (which has
|
||||||
|
// no ABIInternal wrapper, so the JIT function RETs directly into it).
|
||||||
|
var leaveCheckedPtr uintptr
|
||||||
|
|
||||||
|
// enterJITChecked sets sentinels in BP and R14, switches to the prepared
|
||||||
|
// stack and jumps to fn.
|
||||||
|
//
|
||||||
|
//go:nosplit
|
||||||
|
func enterJITChecked(fn uintptr, stack uintptr)
|
||||||
|
|
||||||
|
// leaveJITCheckedRaw is the raw return trampoline for ABI checks. Its
|
||||||
|
// address is obtained from the GLOBL in abi_amd64.s (leaveCheckedPtr),
|
||||||
|
// which points to the .abi0 code — NOT the ABIInternal wrapper that this
|
||||||
|
// declaration would generate. The declaration exists solely to satisfy
|
||||||
|
// go vet's "missing Go declaration" check.
|
||||||
|
//
|
||||||
|
//go:nosplit
|
||||||
|
func leaveJITCheckedRaw()
|
||||||
|
|
||||||
|
// ABIReport describes the result of an ABI-checking call.
|
||||||
|
type ABIReport struct {
|
||||||
|
BPClobbered bool // BP was modified by the function
|
||||||
|
R14Clobbered bool // R14 (goroutine pointer) was modified
|
||||||
|
RedZoneHit bool // the 128-byte red zone below SP was written
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK returns true when no violations were detected.
|
||||||
|
func (r ABIReport) OK() bool {
|
||||||
|
return !r.BPClobbered && !r.R14Clobbered && !r.RedZoneHit
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a human-readable summary.
|
||||||
|
func (r ABIReport) String() string {
|
||||||
|
if r.OK() {
|
||||||
|
return "ABI clean"
|
||||||
|
}
|
||||||
|
s := "ABI violation:"
|
||||||
|
if r.BPClobbered {
|
||||||
|
s += " BP clobbered"
|
||||||
|
}
|
||||||
|
if r.R14Clobbered {
|
||||||
|
s += " R14 clobbered"
|
||||||
|
}
|
||||||
|
if r.RedZoneHit {
|
||||||
|
s += " red-zone written"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// redZoneSize is the System V AMD64 red zone: 128 bytes below SP that a
|
||||||
|
// leaf function may use without adjusting SP. Go does not use the red zone,
|
||||||
|
// so any write there is a bug.
|
||||||
|
const redZoneSize = 128
|
||||||
|
|
||||||
|
// redZoneFill is the byte pattern used to detect red-zone writes.
|
||||||
|
const redZoneFill = 0xA5
|
||||||
|
|
||||||
|
// CallChecked invokes the function with ABI sentinels and a red-zone
|
||||||
|
// canary, returning both the argument block (with results) and an ABIReport.
|
||||||
|
func CallChecked(fnAddr uintptr, args []byte) ([]byte, ABIReport, error) {
|
||||||
|
report := ABIReport{}
|
||||||
|
|
||||||
|
// Reset the global result.
|
||||||
|
abiResult = 0
|
||||||
|
|
||||||
|
// Prepare the stack: [red-zone canary][padding][leaveJITCheckedRaw][args...]
|
||||||
|
// The red zone sits below the initial SP, so the function would have to
|
||||||
|
// write below SP to corrupt it.
|
||||||
|
totalSize := redZoneSize + stackPad + 8 + len(args) + 64
|
||||||
|
stackMem, err := syscall.Mmap(-1, 0, totalSize,
|
||||||
|
syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANON)
|
||||||
|
if err != nil {
|
||||||
|
return nil, report, fmt.Errorf("verify: stack mmap: %w", err)
|
||||||
|
}
|
||||||
|
defer syscall.Munmap(stackMem)
|
||||||
|
|
||||||
|
// Fill the red zone with the canary pattern.
|
||||||
|
for i := 0; i < redZoneSize; i++ {
|
||||||
|
stackMem[i] = redZoneFill
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return address and args after the red zone and padding.
|
||||||
|
retOff := redZoneSize + stackPad
|
||||||
|
binary.LittleEndian.PutUint64(stackMem[retOff:retOff+8], uint64(leaveCheckedPtr))
|
||||||
|
copy(stackMem[retOff+8:], args)
|
||||||
|
|
||||||
|
stackBase := uintptr(unsafe.Pointer(&stackMem[retOff]))
|
||||||
|
enterJITChecked(fnAddr, stackBase)
|
||||||
|
|
||||||
|
// Read the register-clobber result.
|
||||||
|
res := abiResult
|
||||||
|
report.BPClobbered = res&1 != 0
|
||||||
|
report.R14Clobbered = res&2 != 0
|
||||||
|
|
||||||
|
// Check the red zone.
|
||||||
|
for i := 0; i < redZoneSize; i++ {
|
||||||
|
if stackMem[i] != redZoneFill {
|
||||||
|
report.RedZoneHit = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy out the argument area.
|
||||||
|
out := make([]byte, len(args))
|
||||||
|
copy(out, stackMem[retOff+8:retOff+8+len(args)])
|
||||||
|
return out, report, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// ABI-checking trampoline. Sets sentinel values in the callee-saved
|
||||||
|
// registers (BP, R14) before entering the JIT function and checks whether
|
||||||
|
// they survived on return.
|
||||||
|
//
|
||||||
|
// The return trampoline (leaveJITCheckedRaw) is a raw TEXT symbol with no
|
||||||
|
// Go function declaration, so the toolchain does NOT interpose an
|
||||||
|
// ABIInternal wrapper — the JIT function RETs directly into the check code,
|
||||||
|
// which sees the registers exactly as the function left them.
|
||||||
|
//
|
||||||
|
// Go ABI0 on amd64 guarantees:
|
||||||
|
// - BP is callee-saved (NOSPLIT frame=0 functions must not touch it).
|
||||||
|
// - R14 holds the goroutine pointer and must survive across any call.
|
||||||
|
|
||||||
|
// Sentinel values chosen to be unlikely in normal execution.
|
||||||
|
#define SENTINEL_BP 0xDEADBEEFCAFEF00D
|
||||||
|
#define SENTINEL_R14 0x0BADF00DDEADBEEF
|
||||||
|
|
||||||
|
// GLOBL holding the raw address of the leave trampoline, read by Go.
|
||||||
|
GLOBL ·leaveCheckedPtr(SB), NOPTR, $8
|
||||||
|
DATA ·leaveCheckedPtr(SB)/8, $·leaveJITCheckedRaw(SB)
|
||||||
|
|
||||||
|
// func enterJITChecked(fn uintptr, stack uintptr)
|
||||||
|
// Sets sentinels in BP and R14, switches to the prepared stack and jumps
|
||||||
|
// to fn. The prepared stack's return address must be leaveJITCheckedRaw
|
||||||
|
// (read from leaveCheckedPtr).
|
||||||
|
TEXT ·enterJITChecked(SB), NOSPLIT, $0-16
|
||||||
|
MOVQ fn+0(FP), AX // target (before SP switch)
|
||||||
|
MOVQ SP, ·savedSP(SB) // preserve Go stack
|
||||||
|
MOVQ BP, ·savedBP(SB) // preserve frame pointer (vet requires save before clobber)
|
||||||
|
MOVQ $SENTINEL_BP, BP // sentinel in BP
|
||||||
|
MOVQ $SENTINEL_R14, R14 // sentinel in R14
|
||||||
|
MOVQ stack+8(FP), SP // switch to prepared stack
|
||||||
|
JMP AX
|
||||||
|
|
||||||
|
// leaveJITCheckedRaw is the raw return trampoline. It has NO Go function
|
||||||
|
// declaration, so no ABIInternal wrapper is generated — the JIT function's
|
||||||
|
// RET lands here directly, seeing BP and R14 exactly as the function left
|
||||||
|
// them. It checks the sentinels, records violations in abiResult, then
|
||||||
|
// restores the Go stack and returns.
|
||||||
|
TEXT ·leaveJITCheckedRaw(SB), NOSPLIT, $0-0
|
||||||
|
// Check BP against the sentinel.
|
||||||
|
MOVQ $SENTINEL_BP, CX
|
||||||
|
CMPQ BP, CX
|
||||||
|
JEQ bp_ok
|
||||||
|
ORQ $1, ·abiResult(SB)
|
||||||
|
|
||||||
|
bp_ok:
|
||||||
|
// Check R14 against the sentinel.
|
||||||
|
MOVQ $SENTINEL_R14, CX
|
||||||
|
CMPQ R14, CX
|
||||||
|
JEQ r14_ok
|
||||||
|
ORQ $2, ·abiResult(SB)
|
||||||
|
|
||||||
|
r14_ok:
|
||||||
|
MOVQ ·savedSP(SB), SP
|
||||||
|
RET
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !amd64
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// ABIReport describes the result of an ABI-checking call.
|
||||||
|
type ABIReport struct {
|
||||||
|
BPClobbered bool
|
||||||
|
R14Clobbered bool
|
||||||
|
RedZoneHit bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK returns true when no violations were detected.
|
||||||
|
func (r ABIReport) OK() bool { return false }
|
||||||
|
|
||||||
|
// String returns a human-readable summary.
|
||||||
|
func (r ABIReport) String() string { return "verify: ABI checks require amd64" }
|
||||||
|
|
||||||
|
// CallChecked is unavailable on non-amd64 architectures.
|
||||||
|
func CallChecked(fnAddr uintptr, args []byte) ([]byte, ABIReport, error) {
|
||||||
|
return nil, ABIReport{}, fmt.Errorf("verify: ABI checks require amd64")
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadABIKernel(t *testing.T) *Kernel {
|
||||||
|
t.Helper()
|
||||||
|
k, err := Load("../testdata/verify/abi_amd64.s")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(k.Close)
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestABIClean(t *testing.T) {
|
||||||
|
k := loadABIKernel(t)
|
||||||
|
|
||||||
|
args := make([]byte, 24)
|
||||||
|
PutUint64(args, 0, 3)
|
||||||
|
PutUint64(args, 8, 4)
|
||||||
|
|
||||||
|
out, report, err := k.CallFuncChecked("cleanAdd", args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CallFuncChecked: %v", err)
|
||||||
|
}
|
||||||
|
if got := int64(GetUint64(out, 16)); got != 7 {
|
||||||
|
t.Errorf("cleanAdd(3, 4) = %d, want 7", got)
|
||||||
|
}
|
||||||
|
if !report.OK() {
|
||||||
|
t.Errorf("cleanAdd: %s", report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestABIBPClobbered(t *testing.T) {
|
||||||
|
k := loadABIKernel(t)
|
||||||
|
|
||||||
|
args := make([]byte, 16)
|
||||||
|
PutUint64(args, 0, 42)
|
||||||
|
|
||||||
|
out, report, err := k.CallFuncChecked("dirtyBP", args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CallFuncChecked: %v", err)
|
||||||
|
}
|
||||||
|
if got := int64(GetUint64(out, 8)); got != 42 {
|
||||||
|
t.Errorf("dirtyBP(42) = %d, want 42", got)
|
||||||
|
}
|
||||||
|
if !report.BPClobbered {
|
||||||
|
t.Error("dirtyBP: expected BP clobbered, but report says clean")
|
||||||
|
}
|
||||||
|
if report.R14Clobbered {
|
||||||
|
t.Error("dirtyBP: R14 should not be clobbered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestABIR14Clobbered(t *testing.T) {
|
||||||
|
k := loadABIKernel(t)
|
||||||
|
|
||||||
|
args := make([]byte, 16)
|
||||||
|
PutUint64(args, 0, 99)
|
||||||
|
|
||||||
|
out, report, err := k.CallFuncChecked("dirtyR14", args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CallFuncChecked: %v", err)
|
||||||
|
}
|
||||||
|
if got := int64(GetUint64(out, 8)); got != 99 {
|
||||||
|
t.Errorf("dirtyR14(99) = %d, want 99", got)
|
||||||
|
}
|
||||||
|
if !report.R14Clobbered {
|
||||||
|
t.Error("dirtyR14: expected R14 clobbered, but report says clean")
|
||||||
|
}
|
||||||
|
if report.BPClobbered {
|
||||||
|
t.Error("dirtyR14: BP should not be clobbered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestABILZ4Kernels verifies that the production go-lz4 kernels are ABI-clean:
|
||||||
|
// they preserve BP and R14 and do not write into the red zone.
|
||||||
|
func TestABILZ4Kernels(t *testing.T) {
|
||||||
|
k := loadLZ4Kernel(t)
|
||||||
|
|
||||||
|
// wideCopyAVX2 with a real copy.
|
||||||
|
src := make([]byte, 128)
|
||||||
|
for i := range src {
|
||||||
|
src[i] = byte(i)
|
||||||
|
}
|
||||||
|
dst := make([]byte, 128)
|
||||||
|
|
||||||
|
args := make([]byte, 48)
|
||||||
|
PutPtr(args, 0, unsafe.Pointer(&dst[0]))
|
||||||
|
PutUint64(args, 8, 128)
|
||||||
|
PutUint64(args, 16, 128)
|
||||||
|
PutPtr(args, 24, unsafe.Pointer(&src[0]))
|
||||||
|
PutUint64(args, 32, 128)
|
||||||
|
PutUint64(args, 40, 128)
|
||||||
|
|
||||||
|
_, report, err := k.CallFuncChecked("wideCopyAVX2", args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CallFuncChecked(wideCopyAVX2): %v", err)
|
||||||
|
}
|
||||||
|
if !report.OK() {
|
||||||
|
t.Errorf("wideCopyAVX2: %s", report)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeBlockAVX2 with a simple block.
|
||||||
|
decSrc := []byte{0x50, 'H', 'e', 'l', 'l', 'o'}
|
||||||
|
decDst := make([]byte, 64)
|
||||||
|
|
||||||
|
decArgs := make([]byte, 64)
|
||||||
|
PutPtr(decArgs, 0, unsafe.Pointer(&decSrc[0]))
|
||||||
|
PutUint64(decArgs, 8, uint64(len(decSrc)))
|
||||||
|
PutUint64(decArgs, 16, uint64(cap(decSrc)))
|
||||||
|
PutPtr(decArgs, 24, unsafe.Pointer(&decDst[0]))
|
||||||
|
PutUint64(decArgs, 32, uint64(len(decDst)))
|
||||||
|
PutUint64(decArgs, 40, uint64(cap(decDst)))
|
||||||
|
|
||||||
|
_, report, err = k.CallFuncChecked("decodeBlockAVX2", decArgs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CallFuncChecked(decodeBlockAVX2): %v", err)
|
||||||
|
}
|
||||||
|
if !report.OK() {
|
||||||
|
t.Errorf("decodeBlockAVX2: %s", report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCallFuncCheckedErrors(t *testing.T) {
|
||||||
|
k := loadABIKernel(t)
|
||||||
|
|
||||||
|
// Nonexistent function.
|
||||||
|
_, _, err := k.CallFuncChecked("nope", make([]byte, 8))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for nonexistent function")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arg block too small.
|
||||||
|
_, _, err = k.CallFuncChecked("cleanAdd", make([]byte, 8))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for too-small arg block")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const lz4AVX512Path = "../../go-libraries/go-lz4/avx512_amd64.s"
|
||||||
|
|
||||||
|
func loadLZ4AVX512Kernel(t *testing.T) *Kernel {
|
||||||
|
t.Helper()
|
||||||
|
if _, err := os.Stat(lz4AVX512Path); err != nil {
|
||||||
|
t.Skipf("sibling kernel not available: %v", err)
|
||||||
|
}
|
||||||
|
k, err := Load(lz4AVX512Path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load(%s): %v", lz4AVX512Path, err)
|
||||||
|
}
|
||||||
|
t.Cleanup(k.Close)
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAVX512DecodeKnownAnswers(t *testing.T) {
|
||||||
|
k := loadLZ4AVX512Kernel(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
src []byte
|
||||||
|
wantN int
|
||||||
|
wantCode int
|
||||||
|
}{
|
||||||
|
{"literals_only", []byte{0x50, 'H', 'e', 'l', 'l', 'o'}, 5, 0},
|
||||||
|
{"literals_and_match", []byte{0x54, 'A', 'A', 'A', 'A', 'A', 0x05, 0x00, 0x30, 'B', 'B', 'B'}, 16, 0},
|
||||||
|
{"overlapping", []byte{0x14, 'X', 0x01, 0x00, 0x10, 'Y'}, 10, 0},
|
||||||
|
{"malformed", []byte{0x50, 'H', 'e'}, 0, 1},
|
||||||
|
{"zero_offset", []byte{0x14, 'X', 0x00, 0x00}, 0, 2},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
dst := make([]byte, 64)
|
||||||
|
args := make([]byte, 64)
|
||||||
|
PutPtr(args, 0, unsafe.Pointer(&tt.src[0]))
|
||||||
|
PutUint64(args, 8, uint64(len(tt.src)))
|
||||||
|
PutUint64(args, 16, uint64(cap(tt.src)))
|
||||||
|
PutPtr(args, 24, unsafe.Pointer(&dst[0]))
|
||||||
|
PutUint64(args, 32, uint64(len(dst)))
|
||||||
|
PutUint64(args, 40, uint64(cap(dst)))
|
||||||
|
|
||||||
|
out, err := k.CallFunc("decodeBlockAVX512", args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CallFunc: %v", err)
|
||||||
|
}
|
||||||
|
n := int(GetUint64(out, 48))
|
||||||
|
code := int(GetUint64(out, 56))
|
||||||
|
if n != tt.wantN || code != tt.wantCode {
|
||||||
|
t.Errorf("got (n=%d, code=%d), want (n=%d, code=%d)", n, code, tt.wantN, tt.wantCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAVX512DifferentialFuzz(t *testing.T) {
|
||||||
|
k := loadLZ4AVX512Kernel(t)
|
||||||
|
rng := rand.New(rand.NewSource(77))
|
||||||
|
|
||||||
|
for i := 0; i < 3000; i++ {
|
||||||
|
wantSize := 1 + rng.Intn(4096)
|
||||||
|
src := genLZ4Block(rng, wantSize)
|
||||||
|
dstSize := wantSize + 64
|
||||||
|
|
||||||
|
goDst := make([]byte, dstSize)
|
||||||
|
goN, goCode := decodeBlockGo(src, goDst)
|
||||||
|
|
||||||
|
jitDst := make([]byte, dstSize)
|
||||||
|
args := make([]byte, 64)
|
||||||
|
if len(src) > 0 {
|
||||||
|
PutPtr(args, 0, unsafe.Pointer(&src[0]))
|
||||||
|
}
|
||||||
|
PutUint64(args, 8, uint64(len(src)))
|
||||||
|
PutUint64(args, 16, uint64(cap(src)))
|
||||||
|
if dstSize > 0 {
|
||||||
|
PutPtr(args, 24, unsafe.Pointer(&jitDst[0]))
|
||||||
|
}
|
||||||
|
PutUint64(args, 32, uint64(dstSize))
|
||||||
|
PutUint64(args, 40, uint64(cap(jitDst)))
|
||||||
|
|
||||||
|
out, err := k.CallFunc("decodeBlockAVX512", args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("iter %d: %v", i, err)
|
||||||
|
}
|
||||||
|
jitN := int(GetUint64(out, 48))
|
||||||
|
jitCode := int(GetUint64(out, 56))
|
||||||
|
|
||||||
|
if jitCode != goCode {
|
||||||
|
t.Fatalf("iter %d: code mismatch: JIT=%d Go=%d", i, jitCode, goCode)
|
||||||
|
}
|
||||||
|
if jitCode != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if jitN != goN {
|
||||||
|
t.Fatalf("iter %d: n mismatch: JIT=%d Go=%d", i, jitN, goN)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(jitDst[:jitN], goDst[:goN]) {
|
||||||
|
t.Fatalf("iter %d: output mismatch (n=%d)", i, jitN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAVX512WideCopy(t *testing.T) {
|
||||||
|
k := loadLZ4AVX512Kernel(t)
|
||||||
|
|
||||||
|
sizes := []int{0, 1, 31, 32, 63, 64, 65, 127, 128, 256, 1024}
|
||||||
|
for _, n := range sizes {
|
||||||
|
src := make([]byte, n)
|
||||||
|
for i := range src {
|
||||||
|
src[i] = byte(i*11 + 3)
|
||||||
|
}
|
||||||
|
dst := make([]byte, n)
|
||||||
|
|
||||||
|
args := make([]byte, 48)
|
||||||
|
if n > 0 {
|
||||||
|
PutPtr(args, 0, unsafe.Pointer(&dst[0]))
|
||||||
|
PutPtr(args, 24, unsafe.Pointer(&src[0]))
|
||||||
|
}
|
||||||
|
PutUint64(args, 8, uint64(n))
|
||||||
|
PutUint64(args, 16, uint64(n))
|
||||||
|
PutUint64(args, 32, uint64(n))
|
||||||
|
PutUint64(args, 40, uint64(n))
|
||||||
|
|
||||||
|
_, err := k.CallFunc("wideCopyAVX512", args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("wideCopyAVX512(n=%d): %v", n, err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(dst, src) {
|
||||||
|
t.Errorf("wideCopyAVX512(n=%d): mismatch", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Block describes one basic block within a function: a maximal sequence of
|
||||||
|
// instructions with a single entry point (a label or the function start) and
|
||||||
|
// a single exit (a jump, conditional jump or RET).
|
||||||
|
type Block struct {
|
||||||
|
Offset int // byte offset within the function
|
||||||
|
Label string // label name ("" for the entry block)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blocks identifies the basic blocks of a function from its local labels.
|
||||||
|
// Each label is a potential jump target and therefore a block boundary; the
|
||||||
|
// function entry (offset 0) is always a block. The blocks are returned in
|
||||||
|
// ascending offset order.
|
||||||
|
func (k *Kernel) Blocks(name string) ([]Block, error) {
|
||||||
|
idx, ok := k.funcs[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("verify: function %q not found", name)
|
||||||
|
}
|
||||||
|
fl := k.img.Funcs[idx]
|
||||||
|
|
||||||
|
blocks := []Block{{Offset: 0, Label: "(entry)"}}
|
||||||
|
// Build a reverse map: offset → label name.
|
||||||
|
offToLabel := make(map[int]string, len(fl.Labels))
|
||||||
|
for label, off := range fl.Labels {
|
||||||
|
if off > 0 && off < fl.Size {
|
||||||
|
offToLabel[off] = label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Collect and sort offsets.
|
||||||
|
offsets := make([]int, 0, len(offToLabel))
|
||||||
|
for off := range offToLabel {
|
||||||
|
offsets = append(offsets, off)
|
||||||
|
}
|
||||||
|
sort.Ints(offsets)
|
||||||
|
for _, off := range offsets {
|
||||||
|
blocks = append(blocks, Block{Offset: off, Label: offToLabel[off]})
|
||||||
|
}
|
||||||
|
return blocks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockCount returns the number of identified basic blocks for the function.
|
||||||
|
func (k *Kernel) BlockCount(name string) (int, error) {
|
||||||
|
blocks, err := k.Blocks(name)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(blocks), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathFingerprint is the observable output of one function execution: the
|
||||||
|
// values written back into the result slots of the argument block. Two
|
||||||
|
// executions that produce the same fingerprint took observationally
|
||||||
|
// equivalent paths (though they may differ internally).
|
||||||
|
type PathFingerprint struct {
|
||||||
|
Results []uint64 // the result words from the arg block
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProfilePaths runs the function with each of the given argument blocks and
|
||||||
|
// collects the distinct output fingerprints. This measures path diversity:
|
||||||
|
// how many observationally different execution paths the input corpus
|
||||||
|
// exercises. Combined with Blocks (the static block count), it gives a
|
||||||
|
// lower bound on code coverage.
|
||||||
|
func (k *Kernel) ProfilePaths(name string, argSets [][]byte, resultOffsets []int) ([]PathFingerprint, error) {
|
||||||
|
idx, ok := k.funcs[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("verify: function %q not found", name)
|
||||||
|
}
|
||||||
|
fl := k.img.Funcs[idx]
|
||||||
|
|
||||||
|
seen := map[string]bool{}
|
||||||
|
var paths []PathFingerprint
|
||||||
|
|
||||||
|
for _, args := range argSets {
|
||||||
|
if len(args) < fl.Args {
|
||||||
|
return nil, fmt.Errorf("verify: %s: arg block too small", name)
|
||||||
|
}
|
||||||
|
out, err := k.CallFunc(name, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fp := PathFingerprint{}
|
||||||
|
key := ""
|
||||||
|
for _, off := range resultOffsets {
|
||||||
|
v := GetUint64(out, off)
|
||||||
|
fp.Results = append(fp.Results, v)
|
||||||
|
key += fmt.Sprintf("%016x", v)
|
||||||
|
}
|
||||||
|
if !seen[key] {
|
||||||
|
seen[key] = true
|
||||||
|
paths = append(paths, fp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBlocks(t *testing.T) {
|
||||||
|
k := loadBasic(t)
|
||||||
|
|
||||||
|
// The "sum" function has labels: sum_done, sum_loop.
|
||||||
|
blocks, err := k.Blocks("sum")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Blocks(sum): %v", err)
|
||||||
|
}
|
||||||
|
if len(blocks) < 3 {
|
||||||
|
t.Errorf("sum: expected at least 3 blocks (entry + 2 labels), got %d", len(blocks))
|
||||||
|
}
|
||||||
|
if blocks[0].Offset != 0 {
|
||||||
|
t.Errorf("first block offset = %d, want 0", blocks[0].Offset)
|
||||||
|
}
|
||||||
|
t.Logf("sum blocks: %v", blocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlockCount(t *testing.T) {
|
||||||
|
k := loadLZ4Kernel(t)
|
||||||
|
|
||||||
|
n, err := k.BlockCount("decodeBlockAVX2")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BlockCount: %v", err)
|
||||||
|
}
|
||||||
|
// The decoder has many labels (dec_loop, dec_malformed, etc.).
|
||||||
|
if n < 10 {
|
||||||
|
t.Errorf("decodeBlockAVX2: expected at least 10 blocks, got %d", n)
|
||||||
|
}
|
||||||
|
t.Logf("decodeBlockAVX2: %d basic blocks", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProfilePaths(t *testing.T) {
|
||||||
|
k := loadLZ4Kernel(t)
|
||||||
|
|
||||||
|
// Build a corpus of varied LZ4 blocks.
|
||||||
|
var argSets [][]byte
|
||||||
|
blocks := []struct {
|
||||||
|
src []byte
|
||||||
|
dstSize int
|
||||||
|
}{
|
||||||
|
{[]byte{0x00}, 16}, // empty
|
||||||
|
{[]byte{0x50, 'H', 'e', 'l', 'l', 'o'}, 16}, // literals only
|
||||||
|
{[]byte{0x54, 'A', 'A', 'A', 'A', 'A', 5, 0, 0x30, 'B', 'B', 'B'}, 32}, // match
|
||||||
|
{[]byte{0x14, 'X', 1, 0, 0x10, 'Y'}, 16}, // overlapping
|
||||||
|
{[]byte{0x50, 'H'}, 16}, // malformed
|
||||||
|
{[]byte{0x14, 'X', 0, 0}, 16}, // zero offset
|
||||||
|
}
|
||||||
|
for _, b := range blocks {
|
||||||
|
args := make([]byte, 64)
|
||||||
|
if len(b.src) > 0 {
|
||||||
|
PutPtr(args, 0, unsafe.Pointer(&b.src[0]))
|
||||||
|
}
|
||||||
|
PutUint64(args, 8, uint64(len(b.src)))
|
||||||
|
PutUint64(args, 16, uint64(cap(b.src)))
|
||||||
|
dst := make([]byte, b.dstSize)
|
||||||
|
if len(dst) > 0 {
|
||||||
|
PutPtr(args, 24, unsafe.Pointer(&dst[0]))
|
||||||
|
}
|
||||||
|
PutUint64(args, 32, uint64(len(dst)))
|
||||||
|
PutUint64(args, 40, uint64(cap(dst)))
|
||||||
|
argSets = append(argSets, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result offsets: n+48 and code+56.
|
||||||
|
paths, err := k.ProfilePaths("decodeBlockAVX2", argSets, []int{48, 56})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProfilePaths: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect at least 3 distinct paths: success (various n), malformed, zero offset.
|
||||||
|
if len(paths) < 3 {
|
||||||
|
t.Errorf("expected at least 3 distinct paths, got %d", len(paths))
|
||||||
|
}
|
||||||
|
t.Logf("decodeBlockAVX2: %d distinct output paths from %d inputs", len(paths), len(argSets))
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// decodeBlockGo is a minimal portable LZ4 block decoder used as the
|
||||||
|
// differential-testing oracle. It mirrors the contract of
|
||||||
|
// go-lz4's decodeBlockGo: (bytesWritten, code) where code is
|
||||||
|
// 0 = ok, 1 = malformed, 2 = zero offset.
|
||||||
|
func decodeBlockGo(src, dst []byte) (int, int) {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return 0, 1
|
||||||
|
}
|
||||||
|
si, di := 0, 0
|
||||||
|
for {
|
||||||
|
if si >= len(src) {
|
||||||
|
return 0, 1 // truncated: no token
|
||||||
|
}
|
||||||
|
token := int(src[si])
|
||||||
|
si++
|
||||||
|
|
||||||
|
// Literals.
|
||||||
|
lLen := token >> 4
|
||||||
|
if lLen == 15 {
|
||||||
|
for {
|
||||||
|
if si >= len(src) {
|
||||||
|
return 0, 1
|
||||||
|
}
|
||||||
|
b := int(src[si])
|
||||||
|
si++
|
||||||
|
lLen += b
|
||||||
|
if b != 255 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if si+lLen > len(src) {
|
||||||
|
return 0, 1 // truncated literals
|
||||||
|
}
|
||||||
|
if di+lLen > len(dst) {
|
||||||
|
return 0, 1 // destination overflow
|
||||||
|
}
|
||||||
|
copy(dst[di:di+lLen], src[si:si+lLen])
|
||||||
|
di += lLen
|
||||||
|
si += lLen
|
||||||
|
|
||||||
|
// End of block.
|
||||||
|
if si >= len(src) {
|
||||||
|
return di, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match offset.
|
||||||
|
if si+2 > len(src) {
|
||||||
|
return 0, 1
|
||||||
|
}
|
||||||
|
offset := int(src[si]) | int(src[si+1])<<8
|
||||||
|
si += 2
|
||||||
|
if offset == 0 {
|
||||||
|
return 0, 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match length.
|
||||||
|
mLen := token & 15
|
||||||
|
if mLen == 15 {
|
||||||
|
for {
|
||||||
|
if si >= len(src) {
|
||||||
|
return 0, 1
|
||||||
|
}
|
||||||
|
b := int(src[si])
|
||||||
|
si++
|
||||||
|
mLen += b
|
||||||
|
if b != 255 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mLen += 4
|
||||||
|
|
||||||
|
// Copy match (overlapping-safe).
|
||||||
|
if di-offset < 0 {
|
||||||
|
return 0, 1 // offset reaches before dst start
|
||||||
|
}
|
||||||
|
if di+mLen > len(dst) {
|
||||||
|
return 0, 1 // destination overflow
|
||||||
|
}
|
||||||
|
for i := 0; i < mLen; i++ {
|
||||||
|
dst[di+i] = dst[di-offset+i]
|
||||||
|
}
|
||||||
|
di += mLen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// genLZ4Block generates a random valid LZ4 block that decompresses into
|
||||||
|
// approximately wantSize bytes. The block is always well-formed (ends with
|
||||||
|
// a literals-only sequence).
|
||||||
|
func genLZ4Block(rng *rand.Rand, wantSize int) []byte {
|
||||||
|
var block []byte
|
||||||
|
produced := 0
|
||||||
|
for produced < wantSize {
|
||||||
|
remaining := wantSize - produced
|
||||||
|
|
||||||
|
// Decide: emit a literals+match sequence or the final literals.
|
||||||
|
if remaining <= 8 || rng.Intn(4) == 0 {
|
||||||
|
// Final literals-only sequence.
|
||||||
|
lLen := remaining
|
||||||
|
if lLen > 60 {
|
||||||
|
lLen = 1 + rng.Intn(60)
|
||||||
|
}
|
||||||
|
block = appendToken(block, lLen, 0)
|
||||||
|
for i := 0; i < lLen; i++ {
|
||||||
|
block = append(block, byte(rng.Intn(256)))
|
||||||
|
}
|
||||||
|
produced += lLen
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Literals + match.
|
||||||
|
lLen := rng.Intn(min(16, remaining))
|
||||||
|
if produced+lLen == 0 {
|
||||||
|
lLen = 1 // must have at least 1 literal before the first match
|
||||||
|
}
|
||||||
|
mLenRaw := rng.Intn(12) // match length = mLenRaw + 4
|
||||||
|
mLen := mLenRaw + 4
|
||||||
|
if produced+mLen > remaining {
|
||||||
|
mLen = remaining - produced
|
||||||
|
if mLen < 4 {
|
||||||
|
// Not enough room for a match; emit final literals.
|
||||||
|
lLen = remaining
|
||||||
|
block = appendToken(block, lLen, 0)
|
||||||
|
for i := 0; i < lLen; i++ {
|
||||||
|
block = append(block, byte(rng.Intn(256)))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
mLenRaw = mLen - 4
|
||||||
|
}
|
||||||
|
|
||||||
|
block = appendToken(block, lLen, mLenRaw)
|
||||||
|
for i := 0; i < lLen; i++ {
|
||||||
|
block = append(block, byte(rng.Intn(256)))
|
||||||
|
}
|
||||||
|
produced += lLen
|
||||||
|
|
||||||
|
// Offset: must be <= produced (can't reference before start).
|
||||||
|
maxOff := produced
|
||||||
|
if maxOff > 65535 {
|
||||||
|
maxOff = 65535
|
||||||
|
}
|
||||||
|
offset := 1 + rng.Intn(maxOff)
|
||||||
|
block = append(block, byte(offset), byte(offset>>8))
|
||||||
|
produced += mLen
|
||||||
|
}
|
||||||
|
return block
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendToken appends a token (and extension bytes if needed) for the given
|
||||||
|
// literal and match lengths.
|
||||||
|
func appendToken(block []byte, lLen, mLenRaw int) []byte {
|
||||||
|
lit4 := lLen
|
||||||
|
if lit4 > 15 {
|
||||||
|
lit4 = 15
|
||||||
|
}
|
||||||
|
ml4 := mLenRaw
|
||||||
|
if ml4 > 15 {
|
||||||
|
ml4 = 15
|
||||||
|
}
|
||||||
|
block = append(block, byte(lit4<<4|ml4))
|
||||||
|
// Literal extension bytes.
|
||||||
|
rem := lLen - 15
|
||||||
|
for rem >= 255 {
|
||||||
|
block = append(block, 255)
|
||||||
|
rem -= 255
|
||||||
|
}
|
||||||
|
if lLen >= 15 {
|
||||||
|
block = append(block, byte(rem))
|
||||||
|
}
|
||||||
|
// Match extension bytes.
|
||||||
|
rem = mLenRaw - 15
|
||||||
|
for rem >= 255 {
|
||||||
|
block = append(block, 255)
|
||||||
|
rem -= 255
|
||||||
|
}
|
||||||
|
if mLenRaw >= 15 {
|
||||||
|
block = append(block, byte(rem))
|
||||||
|
}
|
||||||
|
return block
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDifferentialLZ4Fuzz drives the JIT-assembled decodeBlockAVX2 with
|
||||||
|
// random valid LZ4 blocks and compares the output bit-for-bit against the
|
||||||
|
// portable Go reference.
|
||||||
|
func TestDifferentialLZ4Fuzz(t *testing.T) {
|
||||||
|
k := loadLZ4Kernel(t)
|
||||||
|
|
||||||
|
const iterations = 5000
|
||||||
|
rng := rand.New(rand.NewSource(42))
|
||||||
|
|
||||||
|
for i := 0; i < iterations; i++ {
|
||||||
|
wantSize := 1 + rng.Intn(4096)
|
||||||
|
src := genLZ4Block(rng, wantSize)
|
||||||
|
dstSize := wantSize + 64 // generous destination
|
||||||
|
|
||||||
|
// Go reference.
|
||||||
|
goDst := make([]byte, dstSize)
|
||||||
|
goN, goCode := decodeBlockGo(src, goDst)
|
||||||
|
|
||||||
|
// JIT kernel.
|
||||||
|
jitDst := make([]byte, dstSize)
|
||||||
|
jitN, jitCode := callDecodeBlockAVX2(t, k, src, jitDst)
|
||||||
|
|
||||||
|
if jitCode != goCode {
|
||||||
|
t.Fatalf("iter %d: code mismatch: JIT=%d, Go=%d (src len=%d)",
|
||||||
|
i, jitCode, goCode, len(src))
|
||||||
|
}
|
||||||
|
if jitCode != 0 {
|
||||||
|
continue // both agree it's malformed/zero-offset
|
||||||
|
}
|
||||||
|
if jitN != goN {
|
||||||
|
t.Fatalf("iter %d: n mismatch: JIT=%d, Go=%d (src len=%d)",
|
||||||
|
i, jitN, goN, len(src))
|
||||||
|
}
|
||||||
|
if !bytes.Equal(jitDst[:jitN], goDst[:goN]) {
|
||||||
|
t.Fatalf("iter %d: output mismatch (n=%d, src len=%d)", i, jitN, len(src))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDifferentialLZ4Hostile drives the kernel with random garbage to check
|
||||||
|
// that error codes agree with the Go reference (no crashes, same classification).
|
||||||
|
func TestDifferentialLZ4Hostile(t *testing.T) {
|
||||||
|
k := loadLZ4Kernel(t)
|
||||||
|
|
||||||
|
const iterations = 2000
|
||||||
|
rng := rand.New(rand.NewSource(99))
|
||||||
|
|
||||||
|
for i := 0; i < iterations; i++ {
|
||||||
|
srcLen := rng.Intn(128)
|
||||||
|
src := make([]byte, srcLen)
|
||||||
|
rng.Read(src)
|
||||||
|
dstSize := rng.Intn(512)
|
||||||
|
dst := make([]byte, dstSize)
|
||||||
|
|
||||||
|
// Go reference.
|
||||||
|
goDst := make([]byte, dstSize)
|
||||||
|
copy(goDst, dst)
|
||||||
|
_, goCode := decodeBlockGo(src, goDst)
|
||||||
|
|
||||||
|
// JIT kernel.
|
||||||
|
jitDst := make([]byte, dstSize)
|
||||||
|
copy(jitDst, dst)
|
||||||
|
_, jitCode := callDecodeBlockAVX2(t, k, src, jitDst)
|
||||||
|
|
||||||
|
if jitCode != goCode {
|
||||||
|
t.Fatalf("iter %d: hostile code mismatch: JIT=%d, Go=%d (srcLen=%d, dstSize=%d)",
|
||||||
|
i, jitCode, goCode, srcLen, dstSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// callDecodeBlockAVX2Raw is like callDecodeBlockAVX2 but accepts explicit
|
||||||
|
// dst size (for hostile tests where dst may be smaller than the output).
|
||||||
|
func callDecodeBlockAVX2Raw(t *testing.T, k *Kernel, src, dst []byte) (int, int) {
|
||||||
|
t.Helper()
|
||||||
|
args := make([]byte, 64)
|
||||||
|
if len(src) > 0 {
|
||||||
|
PutPtr(args, 0, unsafe.Pointer(&src[0]))
|
||||||
|
}
|
||||||
|
PutUint64(args, 8, uint64(len(src)))
|
||||||
|
PutUint64(args, 16, uint64(cap(src)))
|
||||||
|
if len(dst) > 0 {
|
||||||
|
PutPtr(args, 24, unsafe.Pointer(&dst[0]))
|
||||||
|
}
|
||||||
|
PutUint64(args, 32, uint64(len(dst)))
|
||||||
|
PutUint64(args, 40, uint64(cap(dst)))
|
||||||
|
|
||||||
|
out, err := k.CallFunc("decodeBlockAVX2", args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CallFunc(decodeBlockAVX2): %v", err)
|
||||||
|
}
|
||||||
|
return int(GetUint64(out, 48)), int(GetUint64(out, 56))
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const flacKernelPath = "../../go-libraries/go-flac/avx2_amd64.s"
|
||||||
|
|
||||||
|
func loadFLACKernel(t *testing.T) *Kernel {
|
||||||
|
t.Helper()
|
||||||
|
if _, err := os.Stat(flacKernelPath); err != nil {
|
||||||
|
t.Skipf("sibling kernel not available: %v", err)
|
||||||
|
}
|
||||||
|
k, err := Load(flacKernelPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load(%s): %v", flacKernelPath, err)
|
||||||
|
}
|
||||||
|
t.Cleanup(k.Close)
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Portable Go references (from go-flac/simd.go) ---
|
||||||
|
|
||||||
|
func decodeMono16Go(src []byte, dst []int32) {
|
||||||
|
for i := 0; i < len(dst); i++ {
|
||||||
|
dst[i] = int32(int16(uint16(src[2*i]) | uint16(src[2*i+1])<<8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pack16Go(dst []byte, src []int32) {
|
||||||
|
for i, v := range src {
|
||||||
|
dst[2*i] = byte(v)
|
||||||
|
dst[2*i+1] = byte(v >> 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decorrelateLeftSideGo(left, right, out []int32) {
|
||||||
|
for i := range left {
|
||||||
|
l := left[i]
|
||||||
|
out[2*i] = l
|
||||||
|
out[2*i+1] = l - right[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decorrelateSideRightGo(left, right, out []int32) {
|
||||||
|
for i := range left {
|
||||||
|
side := left[i]
|
||||||
|
rch := right[i]
|
||||||
|
out[2*i] = rch + side
|
||||||
|
out[2*i+1] = rch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decorrelateMidSideGo(left, right, out []int32) {
|
||||||
|
for i := range left {
|
||||||
|
mid := left[i]
|
||||||
|
side := right[i]
|
||||||
|
mid2 := mid<<1 | (side & 1)
|
||||||
|
out[2*i] = (mid2 + side) >> 1
|
||||||
|
out[2*i+1] = (mid2 - side) >> 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decorrelateInterleaveGo(left, right, out []int32) {
|
||||||
|
for i := range left {
|
||||||
|
out[2*i] = left[i]
|
||||||
|
out[2*i+1] = right[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Differential tests ---
|
||||||
|
|
||||||
|
func TestFLACDecodeMono16(t *testing.T) {
|
||||||
|
k := loadFLACKernel(t)
|
||||||
|
rng := rand.New(rand.NewSource(7))
|
||||||
|
|
||||||
|
for iter := 0; iter < 500; iter++ {
|
||||||
|
n := rng.Intn(256)
|
||||||
|
src := make([]byte, 2*n)
|
||||||
|
rng.Read(src)
|
||||||
|
|
||||||
|
goDst := make([]int32, n)
|
||||||
|
decodeMono16Go(src, goDst)
|
||||||
|
|
||||||
|
jitDst := make([]int32, n)
|
||||||
|
args := make([]byte, 48)
|
||||||
|
if len(src) > 0 {
|
||||||
|
PutPtr(args, 0, unsafe.Pointer(&src[0]))
|
||||||
|
}
|
||||||
|
PutUint64(args, 8, uint64(len(src)))
|
||||||
|
PutUint64(args, 16, uint64(cap(src)))
|
||||||
|
if n > 0 {
|
||||||
|
PutPtr(args, 24, unsafe.Pointer(&jitDst[0]))
|
||||||
|
}
|
||||||
|
PutUint64(args, 32, uint64(n))
|
||||||
|
PutUint64(args, 40, uint64(cap(jitDst)))
|
||||||
|
|
||||||
|
_, err := k.CallFunc("decodeMono16AVX2", args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("iter %d: %v", iter, err)
|
||||||
|
}
|
||||||
|
for i := range goDst {
|
||||||
|
if jitDst[i] != goDst[i] {
|
||||||
|
t.Fatalf("iter %d: mismatch at [%d]: JIT=%d Go=%d", iter, i, jitDst[i], goDst[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFLACPack16(t *testing.T) {
|
||||||
|
k := loadFLACKernel(t)
|
||||||
|
rng := rand.New(rand.NewSource(13))
|
||||||
|
|
||||||
|
for iter := 0; iter < 500; iter++ {
|
||||||
|
n := rng.Intn(256)
|
||||||
|
src := make([]int32, n)
|
||||||
|
for i := range src {
|
||||||
|
src[i] = int32(rng.Intn(65536) - 32768)
|
||||||
|
}
|
||||||
|
|
||||||
|
goDst := make([]byte, 2*n)
|
||||||
|
pack16Go(goDst, src)
|
||||||
|
|
||||||
|
jitDst := make([]byte, 2*n)
|
||||||
|
args := make([]byte, 48)
|
||||||
|
if len(jitDst) > 0 {
|
||||||
|
PutPtr(args, 0, unsafe.Pointer(&jitDst[0]))
|
||||||
|
}
|
||||||
|
PutUint64(args, 8, uint64(len(jitDst)))
|
||||||
|
PutUint64(args, 16, uint64(cap(jitDst)))
|
||||||
|
if n > 0 {
|
||||||
|
PutPtr(args, 24, unsafe.Pointer(&src[0]))
|
||||||
|
}
|
||||||
|
PutUint64(args, 32, uint64(n))
|
||||||
|
PutUint64(args, 40, uint64(cap(src)))
|
||||||
|
|
||||||
|
_, err := k.CallFunc("pack16AVX2", args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("iter %d: %v", iter, err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(jitDst, goDst) {
|
||||||
|
t.Fatalf("iter %d: output mismatch (n=%d)", iter, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFLACDecorrelate(t *testing.T) {
|
||||||
|
k := loadFLACKernel(t)
|
||||||
|
rng := rand.New(rand.NewSource(21))
|
||||||
|
|
||||||
|
kernels := []struct {
|
||||||
|
name string
|
||||||
|
ref func(left, right, out []int32)
|
||||||
|
}{
|
||||||
|
{"decorrelateLeftSideAVX2", decorrelateLeftSideGo},
|
||||||
|
{"decorrelateSideRightAVX2", decorrelateSideRightGo},
|
||||||
|
{"decorrelateMidSideAVX2", decorrelateMidSideGo},
|
||||||
|
{"decorrelateInterleaveAVX2", decorrelateInterleaveGo},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kk := range kernels {
|
||||||
|
t.Run(kk.name, func(t *testing.T) {
|
||||||
|
for iter := 0; iter < 200; iter++ {
|
||||||
|
n := rng.Intn(128)
|
||||||
|
left := make([]int32, n)
|
||||||
|
right := make([]int32, n)
|
||||||
|
for i := range left {
|
||||||
|
left[i] = int32(rng.Intn(1<<24) - 1<<23)
|
||||||
|
right[i] = int32(rng.Intn(1<<24) - 1<<23)
|
||||||
|
}
|
||||||
|
|
||||||
|
goOut := make([]int32, 2*n)
|
||||||
|
kk.ref(left, right, goOut)
|
||||||
|
|
||||||
|
jitOut := make([]int32, 2*n)
|
||||||
|
args := make([]byte, 72)
|
||||||
|
if n > 0 {
|
||||||
|
PutPtr(args, 0, unsafe.Pointer(&left[0]))
|
||||||
|
PutPtr(args, 24, unsafe.Pointer(&right[0]))
|
||||||
|
PutPtr(args, 48, unsafe.Pointer(&jitOut[0]))
|
||||||
|
}
|
||||||
|
PutUint64(args, 8, uint64(n))
|
||||||
|
PutUint64(args, 16, uint64(cap(left)))
|
||||||
|
PutUint64(args, 32, uint64(n))
|
||||||
|
PutUint64(args, 40, uint64(cap(right)))
|
||||||
|
PutUint64(args, 56, uint64(2*n))
|
||||||
|
PutUint64(args, 64, uint64(cap(jitOut)))
|
||||||
|
|
||||||
|
_, err := k.CallFunc(kk.name, args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("iter %d: %v", iter, err)
|
||||||
|
}
|
||||||
|
for i := range goOut {
|
||||||
|
if jitOut[i] != goOut[i] {
|
||||||
|
t.Fatalf("iter %d: mismatch at [%d]: JIT=%d Go=%d", iter, i, jitOut[i], goOut[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -157,3 +157,59 @@ func TestMapZeroLength(t *testing.T) {
|
|||||||
t.Fatal("expected error for zero-length code")
|
t.Fatal("expected error for zero-length code")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadSourceError(t *testing.T) {
|
||||||
|
_, err := LoadSource("bad.s", "TEXT ·f(SB), NOSPLIT\n\tBADINSTRUCTION\n")
|
||||||
|
// The parser may or may not error on unknown instructions (it's
|
||||||
|
// error-tolerant), but the assembler will reject it.
|
||||||
|
if err == nil {
|
||||||
|
t.Log("LoadSource succeeded unexpectedly (parser is error-tolerant)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadSourceParseError(t *testing.T) {
|
||||||
|
// A completely invalid file that the parser rejects.
|
||||||
|
_, err := LoadSource("empty.s", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("expected: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncLookup(t *testing.T) {
|
||||||
|
k := loadBasic(t)
|
||||||
|
fl, err := k.Func("add")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Func(add): %v", err)
|
||||||
|
}
|
||||||
|
if fl.Name != "add" {
|
||||||
|
t.Errorf("Func(add).Name = %q, want %q", fl.Name, "add")
|
||||||
|
}
|
||||||
|
if fl.Args != 24 {
|
||||||
|
t.Errorf("Func(add).Args = %d, want 24", fl.Args)
|
||||||
|
}
|
||||||
|
_, err = k.Func("nonexistent")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for nonexistent function")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestABIReportString(t *testing.T) {
|
||||||
|
r := ABIReport{}
|
||||||
|
if r.String() != "ABI clean" {
|
||||||
|
t.Errorf("clean report = %q", r.String())
|
||||||
|
}
|
||||||
|
r.BPClobbered = true
|
||||||
|
if r.OK() {
|
||||||
|
t.Error("expected not OK with BP clobbered")
|
||||||
|
}
|
||||||
|
s := r.String()
|
||||||
|
if s == "ABI clean" {
|
||||||
|
t.Error("expected violation string, got clean")
|
||||||
|
}
|
||||||
|
r.R14Clobbered = true
|
||||||
|
r.RedZoneHit = true
|
||||||
|
s = r.String()
|
||||||
|
if s == "ABI clean" {
|
||||||
|
t.Error("expected violation string for all flags")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -98,6 +98,22 @@ func (k *Kernel) CallFunc(name string, args []byte) ([]byte, error) {
|
|||||||
return Call(fnAddr, args)
|
return Call(fnAddr, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CallFuncChecked invokes the named function with ABI sentinels and a
|
||||||
|
// red-zone canary, returning the argument block and an ABIReport that
|
||||||
|
// records any callee-saved register or red-zone violations.
|
||||||
|
func (k *Kernel) CallFuncChecked(name string, args []byte) ([]byte, ABIReport, error) {
|
||||||
|
idx, ok := k.funcs[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, ABIReport{}, fmt.Errorf("verify: function %q not found", name)
|
||||||
|
}
|
||||||
|
fl := k.img.Funcs[idx]
|
||||||
|
if len(args) < fl.Args {
|
||||||
|
return nil, ABIReport{}, fmt.Errorf("verify: %s: arg block too small: got %d, need %d", name, len(args), fl.Args)
|
||||||
|
}
|
||||||
|
fnAddr := k.exec.FuncAddr(fl.Offset)
|
||||||
|
return CallChecked(fnAddr, args)
|
||||||
|
}
|
||||||
|
|
||||||
// Close releases the executable mapping.
|
// Close releases the executable mapping.
|
||||||
func (k *Kernel) Close() {
|
func (k *Kernel) Close() {
|
||||||
if k.exec != nil {
|
if k.exec != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user