fix(verify): arm64 stack save, adaptive canary and host gating
Assisted-by: GLM 5.3
This commit is contained in:
+36
-6
@@ -16,21 +16,51 @@ import (
|
|||||||
// canary below SP, returning both the argument block (with results) and an
|
// canary below SP, returning both the argument block (with results) and an
|
||||||
// ABIReport.
|
// ABIReport.
|
||||||
//
|
//
|
||||||
|
// The callee is assumed to declare no local frame ($0 in its TEXT
|
||||||
|
// directive); use CallCheckedFrame, or Kernel.CallFuncChecked which reads
|
||||||
|
// the frame from the image, for a callee with a frame. A callee whose
|
||||||
|
// frame extends past the fixed call margin would otherwise trip the canary
|
||||||
|
// with perfectly legal writes.
|
||||||
|
//
|
||||||
|
// Not safe for concurrent use: only one JIT call may be in flight at a
|
||||||
|
// time, the trampolines keep the saved registers in package globals.
|
||||||
|
//
|
||||||
// The architecture-specific parts live in abi_<arch>.s: enterJITChecked
|
// The architecture-specific parts live in abi_<arch>.s: enterJITChecked
|
||||||
// plants sentinels in the registers the Go ABI fixes across calls (the
|
// plants sentinels in the registers the Go ABI fixes across calls (the
|
||||||
// frame pointer and the goroutine pointer) before switching to the
|
// frame pointer and the goroutine pointer) before switching to the
|
||||||
// prepared stack, and the raw return trampoline leaveJITCheckedRaw
|
// prepared stack, and the raw return trampoline leaveJITCheckedRaw
|
||||||
// compares them and records violations in abiResult.
|
// compares them and records violations in abiResult.
|
||||||
func CallChecked(fnAddr uintptr, args []byte) ([]byte, ABIReport, error) {
|
func CallChecked(fnAddr uintptr, args []byte) ([]byte, ABIReport, error) {
|
||||||
|
return CallCheckedFrame(fnAddr, args, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallCheckedFrame is CallChecked with the canary gap sized for a callee
|
||||||
|
// that declares a local frame of frame bytes: the canary window is placed
|
||||||
|
// below the deepest write a legal kernel may make, its own frame plus the
|
||||||
|
// call margin, so only writes that go below the declared frame trip it.
|
||||||
|
// Callers that already hold the function layout pass asm.FuncLayout.Frame.
|
||||||
|
//
|
||||||
|
// Not safe for concurrent use: only one JIT call may be in flight at a
|
||||||
|
// time, the trampolines keep the saved registers in package globals.
|
||||||
|
func CallCheckedFrame(fnAddr uintptr, args []byte, frame int) ([]byte, ABIReport, error) {
|
||||||
report := ABIReport{}
|
report := ABIReport{}
|
||||||
|
|
||||||
// Reset the global result.
|
// Reset the global result.
|
||||||
abiResult = 0
|
abiResult = 0
|
||||||
|
|
||||||
// Prepare the stack: [canary][padding][leaveJITCheckedRaw][args...]
|
// The gap between the canary window and the entry stack pointer must
|
||||||
// The canary sits below the initial SP, so the function would have to
|
// cover every write a legal kernel makes. Two parts:
|
||||||
// write below SP to corrupt it.
|
// - frame: the callee's declared local frame, which the ABI lets it
|
||||||
totalSize := redZoneSize + stackPad + 8 + len(args) + 64
|
// write anywhere in [SP-frame, SP).
|
||||||
|
// - stackPad (64): the call margin. A kernel may CALL another
|
||||||
|
// function, which pushes a return address below the frame and runs
|
||||||
|
// a small prologue of its own; 64 bytes covers both. Callees'
|
||||||
|
// own frames are not accounted: a kernel calling deep into other
|
||||||
|
// frames can write below this gap without detection.
|
||||||
|
pad := stackPad + frame
|
||||||
|
|
||||||
|
// Prepare the stack: [canary][frame gap][leaveJITCheckedRaw][args...]
|
||||||
|
totalSize := redZoneSize + pad + 8 + len(args) + 64
|
||||||
stackMem, err := syscall.Mmap(-1, 0, totalSize,
|
stackMem, err := syscall.Mmap(-1, 0, totalSize,
|
||||||
syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANON)
|
syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -43,8 +73,8 @@ func CallChecked(fnAddr uintptr, args []byte) ([]byte, ABIReport, error) {
|
|||||||
stackMem[i] = redZoneFill
|
stackMem[i] = redZoneFill
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return address and args after the canary and padding.
|
// Return address and args after the canary and the frame gap.
|
||||||
retOff := redZoneSize + stackPad
|
retOff := redZoneSize + pad
|
||||||
binary.LittleEndian.PutUint64(stackMem[retOff:retOff+8], uint64(leaveCheckedPtr))
|
binary.LittleEndian.PutUint64(stackMem[retOff:retOff+8], uint64(leaveCheckedPtr))
|
||||||
copy(stackMem[retOff+8:], args)
|
copy(stackMem[retOff+8:], args)
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -36,7 +36,8 @@ DATA ·leaveCheckedPtr(SB)/8, $·leaveJITCheckedRaw(SB)
|
|||||||
TEXT ·enterJITChecked(SB), NOSPLIT, $0-16
|
TEXT ·enterJITChecked(SB), NOSPLIT, $0-16
|
||||||
MOVD fn+0(FP), R0 // target (before SP switch)
|
MOVD fn+0(FP), R0 // target (before SP switch)
|
||||||
MOVD R30, savedLR(SB) // save link register
|
MOVD R30, savedLR(SB) // save link register
|
||||||
MOVD R3, savedSP(SB) // save Go stack pointer
|
MOVD RSP, R3 // save Go stack pointer: R3 relays the value, the
|
||||||
|
MOVD R3, savedSP(SB) // arm64 assembler cannot store RSP to memory
|
||||||
MOVD R29, savedFP(SB) // save frame pointer (vet requires save before clobber)
|
MOVD R29, savedFP(SB) // save frame pointer (vet requires save before clobber)
|
||||||
MOVD g, savedG(SB) // save g
|
MOVD g, savedG(SB) // save g
|
||||||
MOVD $SENTINEL_FP, R29 // sentinel in the frame pointer
|
MOVD $SENTINEL_FP, R29 // sentinel in the frame pointer
|
||||||
@@ -85,8 +86,9 @@ restore:
|
|||||||
|
|
||||||
// Package-level storage for the saved frame pointer. Like savedSP and
|
// Package-level storage for the saved frame pointer. Like savedSP and
|
||||||
// savedLR in trampoline_arm64.s, this is assembly-side state: the amd64
|
// savedLR in trampoline_arm64.s, this is assembly-side state: the amd64
|
||||||
// checked trampoline saves the caller's frame pointer for vet's sake and
|
// checked trampoline saves and restores the caller's frame pointer too
|
||||||
// never restores it, and this file mirrors that.
|
// (leaveJITCheckedRaw restores BP before returning), and this file mirrors
|
||||||
|
// that.
|
||||||
GLOBL savedFP(SB), NOPTR, $8
|
GLOBL savedFP(SB), NOPTR, $8
|
||||||
|
|
||||||
GLOBL savedG(SB), NOPTR, $8
|
GLOBL savedG(SB), NOPTR, $8
|
||||||
|
|||||||
@@ -64,3 +64,14 @@ g_ok:
|
|||||||
JMP (R1) // return to Go caller
|
JMP (R1) // return to Go caller
|
||||||
|
|
||||||
GLOBL savedG(SB), NOPTR, $8
|
GLOBL savedG(SB), NOPTR, $8
|
||||||
|
|
||||||
|
// leaveRawAddr holds the raw .abi0 address of leaveJIT, read by
|
||||||
|
// call_loong64.go in preference to reflect.ValueOf(leaveJIT), which returns
|
||||||
|
// the address of the ABIInternal wrapper the linker interposes: the
|
||||||
|
// wrapper's prologue clobbers the saved-register window the JIT call
|
||||||
|
// depends on. It lives here rather than beside leaveJIT in
|
||||||
|
// trampoline_loong64.s because that file is assembled by gasm itself in
|
||||||
|
// the ground-truth byte tests, and gasm cannot yet encode a symbol-valued
|
||||||
|
// DATA word.
|
||||||
|
GLOBL ·leaveRawAddr(SB), NOPTR, $8
|
||||||
|
DATA ·leaveRawAddr(SB)/8, $·leaveJIT(SB)
|
||||||
|
|||||||
@@ -11,3 +11,8 @@ import "fmt"
|
|||||||
func CallChecked(fnAddr uintptr, args []byte) ([]byte, ABIReport, error) {
|
func CallChecked(fnAddr uintptr, args []byte) ([]byte, ABIReport, error) {
|
||||||
return nil, ABIReport{}, fmt.Errorf("verify: ABI checks are not supported on this architecture")
|
return nil, ABIReport{}, fmt.Errorf("verify: ABI checks are not supported on this architecture")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CallCheckedFrame is unavailable on unsupported architectures.
|
||||||
|
func CallCheckedFrame(fnAddr uintptr, args []byte, frame int) ([]byte, ABIReport, error) {
|
||||||
|
return nil, ABIReport{}, fmt.Errorf("verify: ABI checks are not supported on this architecture")
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ func (r ABIReport) String() string {
|
|||||||
|
|
||||||
// redZoneSize is the canary window below the prepared stack pointer. On
|
// redZoneSize is the canary window below the prepared stack pointer. On
|
||||||
// amd64 it is the System V red zone; on every architecture a Go function
|
// amd64 it is the System V red zone; on every architecture a Go function
|
||||||
// must not write below SP, so any corruption there is a bug.
|
// must not write below its own declared frame, so the canary sits below
|
||||||
|
// the frame plus the call margin (see CallCheckedFrame) and any corruption
|
||||||
|
// there is a bug.
|
||||||
const redZoneSize = 128
|
const redZoneSize = 128
|
||||||
|
|
||||||
// redZoneFill is the byte pattern used to detect writes below SP.
|
// redZoneFill is the byte pattern used to detect writes below SP.
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
package verify
|
package verify
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadABIKernel(t *testing.T) *Kernel {
|
func loadABIKernel(t *testing.T) *Kernel {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
requireHost(t, "amd64")
|
||||||
k, err := Load("../testdata/verify/abi_amd64.s")
|
k, err := Load("../testdata/verify/abi_amd64.s")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Load: %v", err)
|
t.Fatalf("Load: %v", err)
|
||||||
@@ -93,3 +95,85 @@ func TestCallFuncCheckedErrors(t *testing.T) {
|
|||||||
t.Fatal("expected error for too-small arg block")
|
t.Fatal("expected error for too-small arg block")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadCanaryKernel writes an inline kernel pair that exercises the canary
|
||||||
|
// geometry against declared frames: frameLocal owns a $96 local frame and
|
||||||
|
// writes its lowest local (96 bytes below the entry stack pointer, well
|
||||||
|
// past the 64-byte call margin a frame-0 kernel gets); belowFrame writes
|
||||||
|
// 136 bytes below its own frame, deep into the canary window.
|
||||||
|
func loadCanaryKernel(t *testing.T) *Kernel {
|
||||||
|
t.Helper()
|
||||||
|
requireHost(t, "amd64")
|
||||||
|
src := `#include "textflag.h"
|
||||||
|
|
||||||
|
// func frameLocal(x int64) int64
|
||||||
|
TEXT ·frameLocal(SB), NOSPLIT, $96-16
|
||||||
|
MOVQ x+0(FP), AX
|
||||||
|
MOVQ AX, l-96(SP)
|
||||||
|
MOVQ l-96(SP), AX
|
||||||
|
MOVQ AX, ret+8(FP)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func belowFrame(x int64) int64
|
||||||
|
TEXT ·belowFrame(SB), NOSPLIT, $96-16
|
||||||
|
MOVQ $1, -136(SP)
|
||||||
|
MOVQ x+0(FP), AX
|
||||||
|
MOVQ AX, ret+8(FP)
|
||||||
|
RET
|
||||||
|
`
|
||||||
|
file := t.TempDir() + "/canary_amd64.s"
|
||||||
|
if err := os.WriteFile(file, []byte(src), 0o644); err != nil {
|
||||||
|
t.Fatalf("write kernel: %v", err)
|
||||||
|
}
|
||||||
|
k, err := Load(file)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(k.Close)
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCallCheckedFrameLegal checks that a kernel whose declared frame
|
||||||
|
// extends below the fixed 64-byte margin does not trip the canary: the
|
||||||
|
// protected gap must adapt to the frame the TEXT directive declares.
|
||||||
|
func TestCallCheckedFrameLegal(t *testing.T) {
|
||||||
|
k := loadCanaryKernel(t)
|
||||||
|
|
||||||
|
args := make([]byte, 16)
|
||||||
|
PutUint64(args, 0, 42)
|
||||||
|
|
||||||
|
out, report, err := k.CallFuncChecked("frameLocal", args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CallFuncChecked(frameLocal): %v", err)
|
||||||
|
}
|
||||||
|
if got := int64(GetUint64(out, 8)); got != 42 {
|
||||||
|
t.Errorf("frameLocal(42) = %d, want 42", got)
|
||||||
|
}
|
||||||
|
if report.RedZoneHit {
|
||||||
|
t.Error("frameLocal: writing its own $96 frame must not count as a red-zone hit")
|
||||||
|
}
|
||||||
|
if !report.OK() {
|
||||||
|
t.Errorf("frameLocal: %s", report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCallCheckedBelowFrameCaught checks the other side of the adaptive
|
||||||
|
// gap: a kernel that writes below its own frame by more than the call
|
||||||
|
// margin must still be reported.
|
||||||
|
func TestCallCheckedBelowFrameCaught(t *testing.T) {
|
||||||
|
k := loadCanaryKernel(t)
|
||||||
|
|
||||||
|
args := make([]byte, 16)
|
||||||
|
PutUint64(args, 0, 42)
|
||||||
|
|
||||||
|
out, report, err := k.CallFuncChecked("belowFrame", args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CallFuncChecked(belowFrame): %v", err)
|
||||||
|
}
|
||||||
|
if got := int64(GetUint64(out, 8)); got != 42 {
|
||||||
|
t.Errorf("belowFrame(42) = %d, want 42", got)
|
||||||
|
}
|
||||||
|
if !report.RedZoneHit {
|
||||||
|
t.Error("belowFrame: expected RedZoneHit for a write below the declared frame")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+50
-11
@@ -20,7 +20,11 @@ type BufSpec struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ParseBufSpec parses a "name:size:pattern[,name:size:pattern]" spec string
|
// ParseBufSpec parses a "name:size:pattern[,name:size:pattern]" spec string
|
||||||
// into individual buffer specs. Empty input yields an empty slice.
|
// into individual buffer specs. Empty input yields an empty slice. The
|
||||||
|
// size must be a plain decimal number over the whole field and the pattern
|
||||||
|
// must be a known name or a valid hex blob, so typos fail here with the
|
||||||
|
// offending spec in the message rather than silently allocating a zeroed
|
||||||
|
// buffer.
|
||||||
func ParseBufSpec(spec string) ([]BufSpec, error) {
|
func ParseBufSpec(spec string) ([]BufSpec, error) {
|
||||||
if spec == "" {
|
if spec == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -31,15 +35,38 @@ func ParseBufSpec(spec string) ([]BufSpec, error) {
|
|||||||
if len(fields) != 3 {
|
if len(fields) != 3 {
|
||||||
return nil, fmt.Errorf("verify: invalid buffer spec %q (expected name:size:pattern)", part)
|
return nil, fmt.Errorf("verify: invalid buffer spec %q (expected name:size:pattern)", part)
|
||||||
}
|
}
|
||||||
var size int
|
// strconv.Atoi parses the whole field, unlike fmt.Sscanf which
|
||||||
if _, err := fmt.Sscanf(fields[1], "%d", &size); err != nil || size <= 0 {
|
// accepts trailing garbage ("1024abc" parsed as 1024).
|
||||||
|
size, err := strconv.Atoi(fields[1])
|
||||||
|
if err != nil || size <= 0 {
|
||||||
return nil, fmt.Errorf("verify: invalid buffer size %q in %q", fields[1], part)
|
return nil, fmt.Errorf("verify: invalid buffer size %q in %q", fields[1], part)
|
||||||
}
|
}
|
||||||
|
if err := validPattern(fields[2]); err != nil {
|
||||||
|
return nil, fmt.Errorf("verify: invalid pattern %q in %q: %v", fields[2], part, err)
|
||||||
|
}
|
||||||
out = append(out, BufSpec{Name: fields[0], Size: size, Pattern: fields[2]})
|
out = append(out, BufSpec{Name: fields[0], Size: size, Pattern: fields[2]})
|
||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validPattern checks one buffer pattern: a known name, or a non-empty hex
|
||||||
|
// blob. fillBuffer enforces the same rule again at fill time for specs
|
||||||
|
// that were not built by ParseBufSpec.
|
||||||
|
func validPattern(pattern string) error {
|
||||||
|
switch pattern {
|
||||||
|
case "zero", "ones", "seq":
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, err := hex.DecodeString(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("not a known pattern (zero, ones, seq) and not hex: %w", err)
|
||||||
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return fmt.Errorf("not a known pattern (zero, ones, seq) and the hex blob is empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// allocatedBuf is one live buffer in a pool.
|
// allocatedBuf is one live buffer in a pool.
|
||||||
type allocatedBuf struct {
|
type allocatedBuf struct {
|
||||||
spec BufSpec
|
spec BufSpec
|
||||||
@@ -60,11 +87,15 @@ type BufPool struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Alloc allocates and fills the buffers described by specs. The returned
|
// Alloc allocates and fills the buffers described by specs. The returned
|
||||||
// pool must be kept alive until every call using it has returned.
|
// pool must be kept alive until every call using it has returned. An
|
||||||
|
// unknown pattern name or invalid hex blob is an error rather than a
|
||||||
|
// silently zeroed buffer.
|
||||||
func (p *BufPool) Alloc(specs []BufSpec) error {
|
func (p *BufPool) Alloc(specs []BufSpec) error {
|
||||||
for _, s := range specs {
|
for _, s := range specs {
|
||||||
data := make([]byte, s.Size+safetyMargin)
|
data := make([]byte, s.Size+safetyMargin)
|
||||||
fillBuffer(data, s.Pattern)
|
if err := fillBuffer(data, s.Pattern); err != nil {
|
||||||
|
return fmt.Errorf("verify: buffer %q: %w", s.Name, err)
|
||||||
|
}
|
||||||
p.bufs = append(p.bufs, allocatedBuf{spec: s, data: data})
|
p.bufs = append(p.bufs, allocatedBuf{spec: s, data: data})
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -128,8 +159,10 @@ func (p *BufPool) matchBuf(name string) *allocatedBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fillBuffer fills buf with the named pattern: "zero" (no-op, already zeroed),
|
// fillBuffer fills buf with the named pattern: "zero" (no-op, already zeroed),
|
||||||
// "ones" (0xFF), "seq" (i mod 256), or a hex blob repeated to fill.
|
// "ones" (0xFF), "seq" (i mod 256), or a hex blob repeated to fill. An
|
||||||
func fillBuffer(buf []byte, pattern string) {
|
// unknown pattern name or undecodable hex returns an error instead of
|
||||||
|
// leaving the buffer silently zeroed.
|
||||||
|
func fillBuffer(buf []byte, pattern string) error {
|
||||||
switch pattern {
|
switch pattern {
|
||||||
case "zero":
|
case "zero":
|
||||||
// Already zeroed by make.
|
// Already zeroed by make.
|
||||||
@@ -142,12 +175,18 @@ func fillBuffer(buf []byte, pattern string) {
|
|||||||
buf[i] = byte(i)
|
buf[i] = byte(i)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if data, err := hex.DecodeString(pattern); err == nil && len(data) > 0 {
|
data, err := hex.DecodeString(pattern)
|
||||||
for i := range buf {
|
if err != nil {
|
||||||
buf[i] = data[i%len(data)]
|
return fmt.Errorf("unknown pattern (want zero, ones, seq or hex): %w", err)
|
||||||
}
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return fmt.Errorf("unknown pattern (want zero, ones, seq or hex): empty hex blob")
|
||||||
|
}
|
||||||
|
for i := range buf {
|
||||||
|
buf[i] = data[i%len(data)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyScalarArgs writes user-supplied scalar argument values into an ABI0
|
// ApplyScalarArgs writes user-supplied scalar argument values into an ABI0
|
||||||
|
|||||||
@@ -115,6 +115,48 @@ func TestParseBufSpec(t *testing.T) {
|
|||||||
t.Error("ParseBufSpec(\"dst:0:zero\") should error on zero size")
|
t.Error("ParseBufSpec(\"dst:0:zero\") should error on zero size")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("trailing-garbage-size", func(t *testing.T) {
|
||||||
|
// fmt.Sscanf("%d") accepted "1024abc" as 1024; the strict parse
|
||||||
|
// must reject the whole field.
|
||||||
|
if _, err := ParseBufSpec("dst:1024abc:zero"); err == nil {
|
||||||
|
t.Error("ParseBufSpec(\"dst:1024abc:zero\") should reject a size with trailing garbage")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("hex-size", func(t *testing.T) {
|
||||||
|
if _, err := ParseBufSpec("dst:0x40:zero"); err == nil {
|
||||||
|
t.Error("ParseBufSpec(\"dst:0x40:zero\") should reject a non-decimal size")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unknown-pattern", func(t *testing.T) {
|
||||||
|
if _, err := ParseBufSpec("dst:64:wibble"); err == nil {
|
||||||
|
t.Error("ParseBufSpec(\"dst:64:wibble\") should reject an unknown pattern")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bad-hex-pattern", func(t *testing.T) {
|
||||||
|
if _, err := ParseBufSpec("dst:64:zz"); err == nil {
|
||||||
|
t.Error("ParseBufSpec(\"dst:64:zz\") should reject undecodable hex")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty-pattern", func(t *testing.T) {
|
||||||
|
if _, err := ParseBufSpec("dst:64:"); err == nil {
|
||||||
|
t.Error("ParseBufSpec(\"dst:64:\") should reject an empty pattern")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("hex-pattern", func(t *testing.T) {
|
||||||
|
specs, err := ParseBufSpec("dst:6:deadbeef")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(specs) != 1 || specs[0].Pattern != "deadbeef" {
|
||||||
|
t.Errorf("ParseBufSpec(\"dst:6:deadbeef\") = %+v", specs)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBufPoolBuildArgs(t *testing.T) {
|
func TestBufPoolBuildArgs(t *testing.T) {
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ const stackPad = 64
|
|||||||
//
|
//
|
||||||
// The function must be NOSPLIT (no stack growth) and must not reference
|
// The function must be NOSPLIT (no stack growth) and must not reference
|
||||||
// external symbols, the image is self-contained.
|
// external symbols, the image is self-contained.
|
||||||
|
//
|
||||||
|
// Not safe for concurrent use: only one JIT call may be in flight at a
|
||||||
|
// time, the trampolines keep the saved registers in package globals
|
||||||
|
// (savedSP and friends).
|
||||||
func Call(fnAddr uintptr, args []byte) ([]byte, error) {
|
func Call(fnAddr uintptr, args []byte) ([]byte, error) {
|
||||||
// Prepare the stack: [padding][leaveJIT addr][args...]
|
// Prepare the stack: [padding][leaveJIT addr][args...]
|
||||||
stackSize := stackPad + 8 + len(args) + 64 // padding + ret + args + safety
|
stackSize := stackPad + 8 + len(args) + 64 // padding + ret + args + safety
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ const stackPad = 64
|
|||||||
// Call invokes the assembled function at fnAddr with the given ABI0 argument
|
// Call invokes the assembled function at fnAddr with the given ABI0 argument
|
||||||
// block. On arm64, the ABI0 convention places arguments on the stack starting
|
// block. On arm64, the ABI0 convention places arguments on the stack starting
|
||||||
// at [SP+8] (same as amd64 ABI0 in this project).
|
// at [SP+8] (same as amd64 ABI0 in this project).
|
||||||
|
//
|
||||||
|
// Not safe for concurrent use: only one JIT call may be in flight at a
|
||||||
|
// time, the trampolines keep the saved registers in package globals.
|
||||||
func Call(fnAddr uintptr, args []byte) ([]byte, error) {
|
func Call(fnAddr uintptr, args []byte) ([]byte, error) {
|
||||||
stackSize := stackPad + 8 + len(args) + 64
|
stackSize := stackPad + 8 + len(args) + 64
|
||||||
stackMem, err := syscall.Mmap(-1, 0, stackSize,
|
stackMem, err := syscall.Mmap(-1, 0, stackSize,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ package verify
|
|||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
@@ -19,15 +18,20 @@ func enterJIT(fn uintptr, stack uintptr)
|
|||||||
//go:nosplit
|
//go:nosplit
|
||||||
func leaveJIT()
|
func leaveJIT()
|
||||||
|
|
||||||
var leaveJITAddr uintptr
|
// leaveRawAddr is the raw ABI0 address of leaveJIT, handed over by the
|
||||||
|
// GLOBL/DATA in abi_loong64.s (reflect would return the interposed
|
||||||
|
// ABIInternal wrapper instead: its prologue clobbers the saved-register
|
||||||
|
// window the JIT call depends on).
|
||||||
|
var leaveRawAddr uintptr
|
||||||
|
|
||||||
func init() {
|
var leaveJITAddr = leaveRawAddr
|
||||||
leaveJITAddr = reflect.ValueOf(leaveJIT).Pointer()
|
|
||||||
}
|
|
||||||
|
|
||||||
const stackPad = 64
|
const stackPad = 64
|
||||||
|
|
||||||
// Call invokes the assembled function at fnAddr with the given ABI0 argument block.
|
// Call invokes the assembled function at fnAddr with the given ABI0 argument block.
|
||||||
|
//
|
||||||
|
// Not safe for concurrent use: only one JIT call may be in flight at a
|
||||||
|
// time, the trampolines keep the saved registers in package globals.
|
||||||
func Call(fnAddr uintptr, args []byte) ([]byte, error) {
|
func Call(fnAddr uintptr, args []byte) ([]byte, error) {
|
||||||
stackSize := stackPad + 8 + len(args) + 64
|
stackSize := stackPad + 8 + len(args) + 64
|
||||||
stackMem, err := syscall.Mmap(-1, 0, stackSize,
|
stackMem, err := syscall.Mmap(-1, 0, stackSize,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ package verify
|
|||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
// Call is unavailable on non-amd64 architectures.
|
// Call is unavailable off the four supported architectures.
|
||||||
func Call(fnAddr uintptr, args []byte) ([]byte, error) {
|
func Call(fnAddr uintptr, args []byte) ([]byte, error) {
|
||||||
return nil, fmt.Errorf("verify: JIT execution requires amd64")
|
return nil, fmt.Errorf("verify: JIT execution requires amd64, arm64, riscv64 or loong64")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ func init() {
|
|||||||
const stackPad = 64
|
const stackPad = 64
|
||||||
|
|
||||||
// Call invokes the assembled function at fnAddr with the given ABI0 argument block.
|
// Call invokes the assembled function at fnAddr with the given ABI0 argument block.
|
||||||
|
//
|
||||||
|
// Not safe for concurrent use: only one JIT call may be in flight at a
|
||||||
|
// time, the trampolines keep the saved registers in package globals.
|
||||||
func Call(fnAddr uintptr, args []byte) ([]byte, error) {
|
func Call(fnAddr uintptr, args []byte) ([]byte, error) {
|
||||||
stackSize := stackPad + 8 + len(args) + 64
|
stackSize := stackPad + 8 + len(args) + 64
|
||||||
stackMem, err := syscall.Mmap(-1, 0, stackSize,
|
stackMem, err := syscall.Mmap(-1, 0, stackSize,
|
||||||
|
|||||||
+80
-2
@@ -4,8 +4,8 @@
|
|||||||
package verify
|
package verify
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"math/rand"
|
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
func loadBasicKernel(t *testing.T) *Kernel {
|
func loadBasicKernel(t *testing.T) *Kernel {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
requireHost(t, "amd64")
|
||||||
k, err := Load("../testdata/verify/basic_amd64.s")
|
k, err := Load("../testdata/verify/basic_amd64.s")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Load: %v", err)
|
t.Fatalf("Load: %v", err)
|
||||||
@@ -71,7 +72,7 @@ func TestGenDualArgsEntryReplayable(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("parseFuncSig failed")
|
t.Fatal("parseFuncSig failed")
|
||||||
}
|
}
|
||||||
_, _, bufs, entry := genDualArgs(rand.New(rand.NewSource(1)), sig, 24)
|
_, _, bufs, entry := genDualArgs(newRNG(1), sig, 24)
|
||||||
if len(entry.Args) != 2 || entry.Args[0].Kind != "int" {
|
if len(entry.Args) != 2 || entry.Args[0].Kind != "int" {
|
||||||
t.Fatalf("unexpected entry: %+v", entry)
|
t.Fatalf("unexpected entry: %+v", entry)
|
||||||
}
|
}
|
||||||
@@ -96,11 +97,88 @@ func entryInt(t *testing.T, a CorpusArg) int64 {
|
|||||||
return int64(v)
|
return int64(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// strProbeSrc is a kernel that consumes an ABI0 string header: it
|
||||||
|
// dereferences the data pointer (proving it points at live memory) and
|
||||||
|
// returns len(s) + s[0] when the string is non-empty, len(s) otherwise.
|
||||||
|
const strProbeSrc = `#include "textflag.h"
|
||||||
|
|
||||||
|
// func strProbe(s string) int64
|
||||||
|
TEXT ·strProbe(SB), NOSPLIT, $0-24
|
||||||
|
MOVQ s_base+0(FP), SI
|
||||||
|
MOVQ s_len+8(FP), CX
|
||||||
|
XORQ AX, AX
|
||||||
|
TESTQ CX, CX
|
||||||
|
JZ probe_done
|
||||||
|
MOVB (SI), AL
|
||||||
|
ADDQ AX, CX
|
||||||
|
|
||||||
|
probe_done:
|
||||||
|
MOVQ CX, ret+16(FP)
|
||||||
|
RET
|
||||||
|
`
|
||||||
|
|
||||||
|
func loadStrProbeKernel(t *testing.T) *Kernel {
|
||||||
|
t.Helper()
|
||||||
|
requireHost(t, "amd64")
|
||||||
|
file := t.TempDir() + "/strprobe_amd64.s"
|
||||||
|
if err := os.WriteFile(file, []byte(strProbeSrc), 0o644); err != nil {
|
||||||
|
t.Fatalf("write kernel: %v", err)
|
||||||
|
}
|
||||||
|
k, err := Load(file)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(k.Close)
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStringParamRoundTrip pins the ABI0 string marshalling end to end:
|
||||||
|
// genDualArgs lays a string parameter out as a two-word header pointing at
|
||||||
|
// a live buffer, the corpus entry records it, and ReplayEntry rebuilds an
|
||||||
|
// equivalent header.
|
||||||
|
func TestStringParamRoundTrip(t *testing.T) {
|
||||||
|
k := loadStrProbeKernel(t)
|
||||||
|
|
||||||
|
sig, ok := parseFuncSig("// func strProbe(s string) int64")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("parseFuncSig failed")
|
||||||
|
}
|
||||||
|
args, _, bufs, entry := genDualArgs(newRNG(7), sig, 24)
|
||||||
|
if len(entry.Args) != 1 || entry.Args[0].Kind != "string" {
|
||||||
|
t.Fatalf("unexpected entry: %+v", entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := k.CallFunc("strProbe", args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CallFunc: %v", err)
|
||||||
|
}
|
||||||
|
live := int64(GetUint64(out, 16))
|
||||||
|
want := int64(entry.Args[0].Len)
|
||||||
|
if d, err := hex.DecodeString(entry.Args[0].Data); err != nil {
|
||||||
|
t.Fatalf("entry data: %v", err)
|
||||||
|
} else if len(d) > 0 {
|
||||||
|
want += int64(d[0])
|
||||||
|
}
|
||||||
|
if live != want {
|
||||||
|
t.Errorf("live call = %d, want %d (len + first byte)", live, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
replayed, err := k.ReplayEntry("strProbe", entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReplayEntry: %v", err)
|
||||||
|
}
|
||||||
|
if got := int64(GetUint64(replayed, 16)); got != want {
|
||||||
|
t.Errorf("replayed call = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
runtime.KeepAlive(bufs)
|
||||||
|
}
|
||||||
|
|
||||||
// TestFuzzHookSavesFailures fuzzes add against the go-tool-asm build of a
|
// TestFuzzHookSavesFailures fuzzes add against the go-tool-asm build of a
|
||||||
// sub kernel with the same signature, so every iteration mismatches (safely:
|
// sub kernel with the same signature, so every iteration mismatches (safely:
|
||||||
// both kernels read only their own arguments) and the hook must record
|
// both kernels read only their own arguments) and the hook must record
|
||||||
// replayable entries.
|
// replayable entries.
|
||||||
func TestFuzzHookSavesFailures(t *testing.T) {
|
func TestFuzzHookSavesFailures(t *testing.T) {
|
||||||
|
requireHost(t, "amd64")
|
||||||
src := `#include "textflag.h"
|
src := `#include "textflag.h"
|
||||||
|
|
||||||
// func add(a, b int) int
|
// func add(a, b int) int
|
||||||
|
|||||||
+3
-3
@@ -5,7 +5,7 @@ package verify
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Block describes one basic block within a function: a maximal sequence of
|
// Block describes one basic block within a function: a maximal sequence of
|
||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
// a single exit (a jump, conditional jump or RET).
|
// a single exit (a jump, conditional jump or RET).
|
||||||
type Block struct {
|
type Block struct {
|
||||||
Offset int // byte offset within the function
|
Offset int // byte offset within the function
|
||||||
Label string // label name ("" for the entry block)
|
Label string // label name ("(entry)" for the function entry block)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blocks identifies the basic blocks of a function from its local labels.
|
// Blocks identifies the basic blocks of a function from its local labels.
|
||||||
@@ -40,7 +40,7 @@ func (k *Kernel) Blocks(name string) ([]Block, error) {
|
|||||||
for off := range offToLabel {
|
for off := range offToLabel {
|
||||||
offsets = append(offsets, off)
|
offsets = append(offsets, off)
|
||||||
}
|
}
|
||||||
sort.Ints(offsets)
|
slices.Sort(offsets)
|
||||||
for _, off := range offsets {
|
for _, off := range offsets {
|
||||||
blocks = append(blocks, Block{Offset: off, Label: offToLabel[off]})
|
blocks = append(blocks, Block{Offset: off, Label: offToLabel[off]})
|
||||||
}
|
}
|
||||||
|
|||||||
+110
-11
@@ -6,7 +6,7 @@ package verify
|
|||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand/v2"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -14,6 +14,28 @@ import (
|
|||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// newRNG builds the deterministic generator for a fuzz seed. PCG seeds
|
||||||
|
// with two 64-bit words; deriving the second from the first keeps one seed
|
||||||
|
// one stream and rules out the all-zero seed. The sequences differ from
|
||||||
|
// the retired math/rand ones for the same seed, but remain reproducible
|
||||||
|
// run to run, which is the property the fuzzers rely on.
|
||||||
|
func newRNG(seed int64) *rand.Rand {
|
||||||
|
lo := uint64(seed)
|
||||||
|
return rand.New(rand.NewPCG(lo, ^lo))
|
||||||
|
}
|
||||||
|
|
||||||
|
// fillRandom fills buf from rng, eight bytes per draw. math/rand/v2
|
||||||
|
// dropped Read from *rand.Rand, and this loop keeps the byte sequence a
|
||||||
|
// pure function of the generator state.
|
||||||
|
func fillRandom(rng *rand.Rand, buf []byte) {
|
||||||
|
for off := 0; off < len(buf); off += 8 {
|
||||||
|
v := rng.Uint64()
|
||||||
|
for j := 0; j < 8 && off+j < len(buf); j++ {
|
||||||
|
buf[off+j] = byte(v >> (8 * uint(j)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FuzzResult reports the outcome of a differential fuzz campaign for one
|
// FuzzResult reports the outcome of a differential fuzz campaign for one
|
||||||
// function.
|
// function.
|
||||||
type FuzzResult struct {
|
type FuzzResult struct {
|
||||||
@@ -43,9 +65,9 @@ func (r FuzzResult) String() string {
|
|||||||
|
|
||||||
// CorpusArg is one replayable argument of a corpus entry.
|
// CorpusArg is one replayable argument of a corpus entry.
|
||||||
type CorpusArg struct {
|
type CorpusArg struct {
|
||||||
Kind string `json:"kind"` // "slice", "ptr", "int", "scalar"
|
Kind string `json:"kind"` // "slice", "string", "ptr", "int", "scalar"
|
||||||
Len int `json:"len,omitempty"` // slice: declared length in elements
|
Len int `json:"len,omitempty"` // slice: declared length in elements; string: length in bytes
|
||||||
Data string `json:"data,omitempty"` // slice/ptr: hex-encoded buffer content
|
Data string `json:"data,omitempty"` // slice/string/ptr: hex-encoded buffer content
|
||||||
Value string `json:"value,omitempty"` // int/scalar: decimal value
|
Value string `json:"value,omitempty"` // int/scalar: decimal value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +197,9 @@ func ExtractSignatures(src string) map[string]funcSig {
|
|||||||
//
|
//
|
||||||
// The signature comment must appear immediately above the TEXT directive
|
// The signature comment must appear immediately above the TEXT directive
|
||||||
// in the source (the conventional Go assembly layout).
|
// in the source (the conventional Go assembly layout).
|
||||||
|
//
|
||||||
|
// Not safe for concurrent use: only one JIT call may be in flight at a
|
||||||
|
// time, the trampolines keep the saved registers in package globals.
|
||||||
func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations int, seed int64) FuzzResult {
|
func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations int, seed int64) FuzzResult {
|
||||||
return k.FuzzFuncHook(name, sig, goCode, iterations, seed, nil)
|
return k.FuzzFuncHook(name, sig, goCode, iterations, seed, nil)
|
||||||
}
|
}
|
||||||
@@ -182,10 +207,13 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
|
|||||||
// FuzzFuncHook is FuzzFunc with a hook invoked for every failing input (a
|
// FuzzFuncHook is FuzzFunc with a hook invoked for every failing input (a
|
||||||
// crash or a mismatch), receiving a replayable corpus entry. A nil hook
|
// crash or a mismatch), receiving a replayable corpus entry. A nil hook
|
||||||
// behaves exactly like FuzzFunc.
|
// behaves exactly like FuzzFunc.
|
||||||
|
//
|
||||||
|
// Not safe for concurrent use: only one JIT call may be in flight at a
|
||||||
|
// time, the trampolines keep the saved registers in package globals.
|
||||||
func (k *Kernel) FuzzFuncHook(name string, sig funcSig, goCode []byte, iterations int, seed int64, onSave func(CorpusEntry)) FuzzResult {
|
func (k *Kernel) FuzzFuncHook(name string, sig funcSig, goCode []byte, iterations int, seed int64, onSave func(CorpusEntry)) FuzzResult {
|
||||||
result := FuzzResult{Func: name, Iterations: iterations}
|
result := FuzzResult{Func: name, Iterations: iterations}
|
||||||
|
|
||||||
rng := rand.New(rand.NewSource(seed))
|
rng := newRNG(seed)
|
||||||
|
|
||||||
// Map the Go-assembled code into a second executable region.
|
// Map the Go-assembled code into a second executable region.
|
||||||
goExec, err := Map(goCode)
|
goExec, err := Map(goCode)
|
||||||
@@ -203,6 +231,18 @@ func (k *Kernel) FuzzFuncHook(name string, sig funcSig, goCode []byte, iteration
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The result comparison below slices the parameter area off the
|
||||||
|
// argument block; a // func comment declaring more parameter bytes
|
||||||
|
// than the TEXT frame carries would slice past its end and panic.
|
||||||
|
// Fail the whole campaign with a clear message instead.
|
||||||
|
if ps := paramsSize(sig); ps > fl.Args {
|
||||||
|
result.Mismatches = iterations
|
||||||
|
result.FirstFail = fmt.Sprintf(
|
||||||
|
"signature declares %d parameter bytes, but the TEXT frame of %s carries %d argument bytes",
|
||||||
|
ps, name, fl.Args)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
for i := range iterations {
|
for i := range iterations {
|
||||||
// Generate inputs and build TWO independent arg blocks (one per
|
// Generate inputs and build TWO independent arg blocks (one per
|
||||||
// version) so that functions which write to their arguments
|
// version) so that functions which write to their arguments
|
||||||
@@ -276,7 +316,7 @@ func genDualArgs(rng *rand.Rand, sig funcSig, argSize int) (gasmArgs, goArgs []b
|
|||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(p.typ, "[]"):
|
case strings.HasPrefix(p.typ, "[]"):
|
||||||
elemSize := elemSizeFor(p.typ)
|
elemSize := elemSizeFor(p.typ)
|
||||||
n := 1 + rng.Intn(127)
|
n := 1 + rng.IntN(127)
|
||||||
var declaredLen int
|
var declaredLen int
|
||||||
if sliceIdx == 0 {
|
if sliceIdx == 0 {
|
||||||
declaredLen = n
|
declaredLen = n
|
||||||
@@ -290,7 +330,7 @@ func genDualArgs(rng *rand.Rand, sig funcSig, argSize int) (gasmArgs, goArgs []b
|
|||||||
// Two independent buffers with identical random content.
|
// Two independent buffers with identical random content.
|
||||||
buf1 := make([]byte, bufBytes)
|
buf1 := make([]byte, bufBytes)
|
||||||
buf2 := make([]byte, bufBytes)
|
buf2 := make([]byte, bufBytes)
|
||||||
rng.Read(buf1[:n*elemSize])
|
fillRandom(rng, buf1[:n*elemSize])
|
||||||
copy(buf2, buf1)
|
copy(buf2, buf1)
|
||||||
bufs = append(bufs, buf1, buf2)
|
bufs = append(bufs, buf1, buf2)
|
||||||
putPtr(gasmArgs, off, unsafe.Pointer(&buf1[0]))
|
putPtr(gasmArgs, off, unsafe.Pointer(&buf1[0]))
|
||||||
@@ -309,13 +349,35 @@ func genDualArgs(rng *rand.Rand, sig funcSig, argSize int) (gasmArgs, goArgs []b
|
|||||||
Data: hex.EncodeToString(buf1[:n*elemSize]),
|
Data: hex.EncodeToString(buf1[:n*elemSize]),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
case p.typ == "string":
|
||||||
|
// An ABI0 string is a two-word header (data pointer +
|
||||||
|
// length); a random pointer would fault kernels that read
|
||||||
|
// the string, so the header points at a real buffer with
|
||||||
|
// the same safety margin slices get.
|
||||||
|
n := 1 + rng.IntN(127)
|
||||||
|
buf1 := make([]byte, n+8192)
|
||||||
|
buf2 := make([]byte, n+8192)
|
||||||
|
fillRandom(rng, buf1[:n])
|
||||||
|
copy(buf2, buf1)
|
||||||
|
bufs = append(bufs, buf1, buf2)
|
||||||
|
putPtr(gasmArgs, off, unsafe.Pointer(&buf1[0]))
|
||||||
|
putPtr(goArgs, off, unsafe.Pointer(&buf2[0]))
|
||||||
|
putU64(gasmArgs, off+8, uint64(n))
|
||||||
|
putU64(goArgs, off+8, uint64(n))
|
||||||
|
off += 16
|
||||||
|
entry.Args = append(entry.Args, CorpusArg{
|
||||||
|
Kind: "string",
|
||||||
|
Len: n,
|
||||||
|
Data: hex.EncodeToString(buf1[:n]),
|
||||||
|
})
|
||||||
|
|
||||||
case strings.HasPrefix(p.typ, "*["):
|
case strings.HasPrefix(p.typ, "*["):
|
||||||
nElem := arrayLen(p.typ)
|
nElem := arrayLen(p.typ)
|
||||||
elem := elemSizeFor("[]" + p.typ[strings.Index(p.typ, "]")+1:])
|
elem := elemSizeFor("[]" + p.typ[strings.Index(p.typ, "]")+1:])
|
||||||
size := max(nElem*elem, 8)
|
size := max(nElem*elem, 8)
|
||||||
buf1 := make([]byte, size)
|
buf1 := make([]byte, size)
|
||||||
buf2 := make([]byte, size)
|
buf2 := make([]byte, size)
|
||||||
rng.Read(buf1)
|
fillRandom(rng, buf1)
|
||||||
copy(buf2, buf1)
|
copy(buf2, buf1)
|
||||||
bufs = append(bufs, buf1, buf2)
|
bufs = append(bufs, buf1, buf2)
|
||||||
putPtr(gasmArgs, off, unsafe.Pointer(&buf1[0]))
|
putPtr(gasmArgs, off, unsafe.Pointer(&buf1[0]))
|
||||||
@@ -327,12 +389,24 @@ func genDualArgs(rng *rand.Rand, sig funcSig, argSize int) (gasmArgs, goArgs []b
|
|||||||
})
|
})
|
||||||
|
|
||||||
case p.typ == "int" || p.typ == "uint" || p.typ == "int64" || p.typ == "uint64":
|
case p.typ == "int" || p.typ == "uint" || p.typ == "int64" || p.typ == "uint64":
|
||||||
v := uint64(rng.Intn(256))
|
v := uint64(rng.IntN(256))
|
||||||
putU64(gasmArgs, off, v)
|
putU64(gasmArgs, off, v)
|
||||||
putU64(goArgs, off, v)
|
putU64(goArgs, off, v)
|
||||||
off += 8
|
off += 8
|
||||||
entry.Args = append(entry.Args, CorpusArg{Kind: "int", Value: strconv.FormatUint(v, 10)})
|
entry.Args = append(entry.Args, CorpusArg{Kind: "int", Value: strconv.FormatUint(v, 10)})
|
||||||
|
|
||||||
|
case p.typ == "complex64", p.typ == "complex128":
|
||||||
|
// complex64 is two float32s (8 bytes), complex128 two
|
||||||
|
// float64s (16): plain data words to the marshaller, one
|
||||||
|
// corpus scalar per word so replay rebuilds them exactly.
|
||||||
|
for range paramSize(p.typ) / 8 {
|
||||||
|
v := rng.Uint64()
|
||||||
|
putU64(gasmArgs, off, v)
|
||||||
|
putU64(goArgs, off, v)
|
||||||
|
off += 8
|
||||||
|
entry.Args = append(entry.Args, CorpusArg{Kind: "scalar", Value: strconv.FormatUint(v, 10)})
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
v := rng.Uint64()
|
v := rng.Uint64()
|
||||||
putU64(gasmArgs, off, v)
|
putU64(gasmArgs, off, v)
|
||||||
@@ -346,8 +420,12 @@ func genDualArgs(rng *rand.Rand, sig funcSig, argSize int) (gasmArgs, goArgs []b
|
|||||||
|
|
||||||
// ReplayEntry rebuilds the argument block of a corpus entry and invokes the
|
// ReplayEntry rebuilds the argument block of a corpus entry and invokes the
|
||||||
// named function once, returning the argument block after the call. Slice
|
// named function once, returning the argument block after the call. Slice
|
||||||
// buffers get the same safety padding the fuzzer uses, so over-reads that
|
// and string buffers get the same safety padding the fuzzer uses, so
|
||||||
// were harmless during the original run stay harmless on replay.
|
// over-reads that were harmless during the original run stay harmless on
|
||||||
|
// replay.
|
||||||
|
//
|
||||||
|
// Not safe for concurrent use: only one JIT call may be in flight at a
|
||||||
|
// time, the trampolines keep the saved registers in package globals.
|
||||||
func (k *Kernel) ReplayEntry(name string, e CorpusEntry) ([]byte, error) {
|
func (k *Kernel) ReplayEntry(name string, e CorpusEntry) ([]byte, error) {
|
||||||
fl, err := k.Func(name)
|
fl, err := k.Func(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -374,6 +452,21 @@ func (k *Kernel) ReplayEntry(name string, e CorpusEntry) ([]byte, error) {
|
|||||||
putU64(args, off+16, uint64(a.Len))
|
putU64(args, off+16, uint64(a.Len))
|
||||||
off += 24
|
off += 24
|
||||||
|
|
||||||
|
case "string":
|
||||||
|
data, err := hex.DecodeString(a.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("corpus: string data: %w", err)
|
||||||
|
}
|
||||||
|
buf := make([]byte, len(data)+8192)
|
||||||
|
copy(buf, data)
|
||||||
|
bufs = append(bufs, buf)
|
||||||
|
if off+16 > len(args) {
|
||||||
|
return nil, fmt.Errorf("corpus: entry does not fit the argument block of %s", name)
|
||||||
|
}
|
||||||
|
putPtr(args, off, unsafe.Pointer(&buf[0]))
|
||||||
|
putU64(args, off+8, uint64(a.Len))
|
||||||
|
off += 16
|
||||||
|
|
||||||
case "ptr":
|
case "ptr":
|
||||||
data, err := hex.DecodeString(a.Data)
|
data, err := hex.DecodeString(a.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -431,6 +524,12 @@ func paramsSize(sig funcSig) int {
|
|||||||
size += 8 // pointer
|
size += 8 // pointer
|
||||||
case p.typ == "bool":
|
case p.typ == "bool":
|
||||||
size += 1
|
size += 1
|
||||||
|
case p.typ == "string":
|
||||||
|
size += 16 // data pointer + length
|
||||||
|
case p.typ == "complex64":
|
||||||
|
size += 8 // two float32s
|
||||||
|
case p.typ == "complex128":
|
||||||
|
size += 16 // two float64s
|
||||||
default:
|
default:
|
||||||
size += 8 // int, uint, etc.
|
size += 8 // int, uint, etc.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,16 @@ func TestParamsSize(t *testing.T) {
|
|||||||
if got := paramsSize(sig); got != 32 {
|
if got := paramsSize(sig); got != 32 {
|
||||||
t.Errorf("paramsSize = %d, want 32 (24 for slice + 8 for int)", got)
|
t.Errorf("paramsSize = %d, want 32 (24 for slice + 8 for int)", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ABI0 strings are ptr+len (16), complex64 packs two float32s (8),
|
||||||
|
// complex128 two float64s (16).
|
||||||
|
sig = funcSig{
|
||||||
|
name: "hdr",
|
||||||
|
params: []param{{name: "s", typ: "string"}, {name: "c64", typ: "complex64"}, {name: "c128", typ: "complex128"}},
|
||||||
|
}
|
||||||
|
if got := paramsSize(sig); got != 40 {
|
||||||
|
t.Errorf("paramsSize = %d, want 40 (16 string + 8 complex64 + 16 complex128)", got)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBlockCount(t *testing.T) {
|
func TestBlockCount(t *testing.T) {
|
||||||
@@ -103,11 +113,34 @@ func TestBlockCount(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestNewRNGDeterministic pins the reproducibility contract of the fuzz
|
||||||
|
// seeds: the same seed must rebuild the same PCG stream, including the
|
||||||
|
// sub-word tail of fillRandom.
|
||||||
|
func TestNewRNGDeterministic(t *testing.T) {
|
||||||
|
draw := func() []byte {
|
||||||
|
rng := newRNG(7)
|
||||||
|
out := make([]byte, 20) // 8+8+4: exercises a full word and a tail
|
||||||
|
fillRandom(rng, out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
a, b := draw(), draw()
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
t.Fatalf("seed 7 produced different bytes at %d: %02x vs %02x", i, a[i], b[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if newRNG(0) == nil {
|
||||||
|
t.Fatal("newRNG(0) must build a generator")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFillBuffer(t *testing.T) {
|
func TestFillBuffer(t *testing.T) {
|
||||||
t.Run("zero", func(t *testing.T) {
|
t.Run("zero", func(t *testing.T) {
|
||||||
// fillBuffer("zero") is a no-op; relies on make already zeroing.
|
// fillBuffer("zero") is a no-op; relies on make already zeroing.
|
||||||
buf := make([]byte, 16)
|
buf := make([]byte, 16)
|
||||||
fillBuffer(buf, "zero")
|
if err := fillBuffer(buf, "zero"); err != nil {
|
||||||
|
t.Fatalf("fillBuffer(zero): %v", err)
|
||||||
|
}
|
||||||
for _, b := range buf {
|
for _, b := range buf {
|
||||||
if b != 0 {
|
if b != 0 {
|
||||||
t.Error("zero pattern: make should produce zeroed buffer")
|
t.Error("zero pattern: make should produce zeroed buffer")
|
||||||
@@ -117,7 +150,9 @@ func TestFillBuffer(t *testing.T) {
|
|||||||
})
|
})
|
||||||
t.Run("ones", func(t *testing.T) {
|
t.Run("ones", func(t *testing.T) {
|
||||||
buf := make([]byte, 16)
|
buf := make([]byte, 16)
|
||||||
fillBuffer(buf, "ones")
|
if err := fillBuffer(buf, "ones"); err != nil {
|
||||||
|
t.Fatalf("fillBuffer(ones): %v", err)
|
||||||
|
}
|
||||||
for _, b := range buf {
|
for _, b := range buf {
|
||||||
if b != 0xFF {
|
if b != 0xFF {
|
||||||
t.Error("ones pattern should fill with 0xFF")
|
t.Error("ones pattern should fill with 0xFF")
|
||||||
@@ -127,7 +162,9 @@ func TestFillBuffer(t *testing.T) {
|
|||||||
})
|
})
|
||||||
t.Run("seq", func(t *testing.T) {
|
t.Run("seq", func(t *testing.T) {
|
||||||
buf := make([]byte, 256)
|
buf := make([]byte, 256)
|
||||||
fillBuffer(buf, "seq")
|
if err := fillBuffer(buf, "seq"); err != nil {
|
||||||
|
t.Fatalf("fillBuffer(seq): %v", err)
|
||||||
|
}
|
||||||
for i, b := range buf {
|
for i, b := range buf {
|
||||||
if b != byte(i) {
|
if b != byte(i) {
|
||||||
t.Errorf("seq[%d] = %d, want %d", i, b, i)
|
t.Errorf("seq[%d] = %d, want %d", i, b, i)
|
||||||
@@ -137,7 +174,9 @@ func TestFillBuffer(t *testing.T) {
|
|||||||
})
|
})
|
||||||
t.Run("hex", func(t *testing.T) {
|
t.Run("hex", func(t *testing.T) {
|
||||||
buf := make([]byte, 6)
|
buf := make([]byte, 6)
|
||||||
fillBuffer(buf, "deadbeef")
|
if err := fillBuffer(buf, "deadbeef"); err != nil {
|
||||||
|
t.Fatalf("fillBuffer(deadbeef): %v", err)
|
||||||
|
}
|
||||||
want := []byte{0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD}
|
want := []byte{0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD}
|
||||||
for i, b := range buf {
|
for i, b := range buf {
|
||||||
if b != want[i] {
|
if b != want[i] {
|
||||||
@@ -146,6 +185,34 @@ func TestFillBuffer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
t.Run("unknown-pattern", func(t *testing.T) {
|
||||||
|
buf := make([]byte, 8)
|
||||||
|
if err := fillBuffer(buf, "wibble"); err == nil {
|
||||||
|
t.Error("fillBuffer(wibble): expected an error for an unknown pattern name")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("bad-hex", func(t *testing.T) {
|
||||||
|
buf := make([]byte, 8)
|
||||||
|
if err := fillBuffer(buf, "zz"); err == nil {
|
||||||
|
t.Error("fillBuffer(zz): expected an error for undecodable hex")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("empty-hex", func(t *testing.T) {
|
||||||
|
buf := make([]byte, 8)
|
||||||
|
if err := fillBuffer(buf, ""); err == nil {
|
||||||
|
t.Error("fillBuffer(\"\"): expected an error for an empty pattern")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBufPoolAllocBadPattern checks that Alloc surfaces fill errors under
|
||||||
|
// the buffer's name, so hand-built specs fail as loudly as parsed ones.
|
||||||
|
func TestBufPoolAllocBadPattern(t *testing.T) {
|
||||||
|
var pool BufPool
|
||||||
|
defer pool.Close()
|
||||||
|
if err := pool.Alloc([]BufSpec{{Name: "dst", Size: 16, Pattern: "nope"}}); err == nil {
|
||||||
|
t.Fatal("Alloc with an unknown pattern must fail")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFuzzFuncChecked(t *testing.T) {
|
func TestFuzzFuncChecked(t *testing.T) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
package verify
|
package verify
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -65,6 +67,72 @@ func TestFuzzWideCopy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestFuzzFuncSigWiderThanFrame pins the guard against a // func comment
|
||||||
|
// that declares more parameter bytes than the TEXT frame carries: before
|
||||||
|
// the guard, slicing the result area at paramsSize(sig) past the end of
|
||||||
|
// the argument block panicked the whole test binary.
|
||||||
|
func TestFuzzFuncSigWiderThanFrame(t *testing.T) {
|
||||||
|
k := loadBasic(t)
|
||||||
|
|
||||||
|
gt, err := GroundTruth("../testdata/verify/basic_amd64.s")
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("go tool asm unavailable: %v", err)
|
||||||
|
}
|
||||||
|
goCode, ok := gt["add"]
|
||||||
|
if !ok {
|
||||||
|
t.Skip("add not in ground truth")
|
||||||
|
}
|
||||||
|
|
||||||
|
// add carries $0-24; four int parameters declare 32 bytes.
|
||||||
|
sig := funcSig{
|
||||||
|
name: "add",
|
||||||
|
params: []param{
|
||||||
|
{name: "a", typ: "int"},
|
||||||
|
{name: "b", typ: "int"},
|
||||||
|
{name: "c", typ: "int"},
|
||||||
|
{name: "d", typ: "int"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res := k.FuzzFunc("add", sig, goCode, 3, 42)
|
||||||
|
if res.OK() {
|
||||||
|
t.Fatal("expected the over-wide signature to fail the campaign")
|
||||||
|
}
|
||||||
|
if !strings.Contains(res.FirstFail, "parameter bytes") || !strings.Contains(res.FirstFail, "argument bytes") {
|
||||||
|
t.Errorf("FirstFail = %q, want a clear signature-versus-frame message", res.FirstFail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFuzzStringParam runs the differential fuzzer over a kernel whose
|
||||||
|
// only parameter is a string: the marshaller lays out a real two-word
|
||||||
|
// header (data pointer + length) and the comparison slices at
|
||||||
|
// paramsSize(sig) = 16, so the length word is input, not result.
|
||||||
|
func TestFuzzStringParam(t *testing.T) {
|
||||||
|
k := loadStrProbeKernel(t)
|
||||||
|
|
||||||
|
file := t.TempDir() + "/strprobe_amd64.s"
|
||||||
|
if err := os.WriteFile(file, []byte(strProbeSrc), 0o644); err != nil {
|
||||||
|
t.Fatalf("write kernel: %v", err)
|
||||||
|
}
|
||||||
|
gt, err := GroundTruth(file)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("go tool asm unavailable: %v", err)
|
||||||
|
}
|
||||||
|
goCode, ok := gt["strProbe"]
|
||||||
|
if !ok {
|
||||||
|
t.Skip("strProbe not in ground truth")
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, ok := parseFuncSig("// func strProbe(s string) int64")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("parseFuncSig failed")
|
||||||
|
}
|
||||||
|
res := k.FuzzFunc("strProbe", sig, goCode, 100, 42)
|
||||||
|
if !res.OK() {
|
||||||
|
t.Errorf("strProbe fuzz: %s", res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseFuncSig(t *testing.T) {
|
func TestParseFuncSig(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
comment string
|
comment string
|
||||||
|
|||||||
+15
-3
@@ -20,8 +20,8 @@ import (
|
|||||||
// This is the universal oracle: any file that `go tool asm` accepts can
|
// This is the universal oracle: any file that `go tool asm` accepts can
|
||||||
// be verified, with no hand-written reference.
|
// be verified, with no hand-written reference.
|
||||||
//
|
//
|
||||||
// For RISC-V sources the assembler is invoked with GOARCH=riscv64;
|
// For the other architectures use the cross-assembly entry points
|
||||||
// the caller must set the architecture via GroundTruthArch.
|
// GroundTruthARM64, GroundTruthRISCV and GroundTruthLOONG64.
|
||||||
func GroundTruth(path string) (map[string][]byte, error) {
|
func GroundTruth(path string) (map[string][]byte, error) {
|
||||||
return groundTruthArch(path, "")
|
return groundTruthArch(path, "")
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,19 @@ func groundTruthArch(path, goarch string) (map[string][]byte, error) {
|
|||||||
|
|
||||||
cmd := exec.Command(asmBin, "-I", includeDir, "-p", pkg, "-o", objPath, path)
|
cmd := exec.Command(asmBin, "-I", includeDir, "-p", pkg, "-o", objPath, path)
|
||||||
if goarch != "" {
|
if goarch != "" {
|
||||||
cmd.Env = append(os.Environ(), "GOARCH="+goarch)
|
// Replace, do not duplicate, any GOARCH the ambient environment
|
||||||
|
// exports. The Go runtime resolves duplicated keys last-wins,
|
||||||
|
// so the appended entry happens to win today; a single
|
||||||
|
// unambiguous entry keeps the child's target architecture from
|
||||||
|
// depending on that resolution order.
|
||||||
|
environ := os.Environ()
|
||||||
|
env := make([]string, 0, len(environ)+1)
|
||||||
|
for _, e := range environ {
|
||||||
|
if !strings.HasPrefix(e, "GOARCH=") {
|
||||||
|
env = append(env, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd.Env = append(env, "GOARCH="+goarch)
|
||||||
}
|
}
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
return nil, fmt.Errorf("verify: go tool asm (%s): %w\n%s", goarch, err, out)
|
return nil, fmt.Errorf("verify: go tool asm (%s): %w\n%s", goarch, err, out)
|
||||||
|
|||||||
@@ -82,6 +82,22 @@ func TestGroundTruthBadFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGroundTruthCrossEnvGOARCH checks that a GOARCH exported in the
|
||||||
|
// ambient environment does not override cross-assembly: the child
|
||||||
|
// assembler must honour the requested GOARCH even when the environment
|
||||||
|
// already carries a GOARCH entry (os.Getenv reads the first entry, so a
|
||||||
|
// duplicate would win).
|
||||||
|
func TestGroundTruthCrossEnvGOARCH(t *testing.T) {
|
||||||
|
t.Setenv("GOARCH", "amd64")
|
||||||
|
gt, err := GroundTruthARM64("../testdata/verify/basic_arm64.s")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GroundTruthARM64 with ambient GOARCH=amd64: %v", err)
|
||||||
|
}
|
||||||
|
if len(gt) == 0 {
|
||||||
|
t.Fatal("no functions extracted from ground truth")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func keys(m map[string][]byte) []string {
|
func keys(m map[string][]byte) []string {
|
||||||
out := make([]string, 0, len(m))
|
out := make([]string, 0, len(m))
|
||||||
for k := range m {
|
for k := range m {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
func loadBasic(t *testing.T) *Kernel {
|
func loadBasic(t *testing.T) *Kernel {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
requireHost(t, "amd64")
|
||||||
k, err := Load("../testdata/verify/basic_amd64.s")
|
k, err := Load("../testdata/verify/basic_amd64.s")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Load: %v", err)
|
t.Fatalf("Load: %v", err)
|
||||||
|
|||||||
@@ -153,6 +153,13 @@ func paramSize(typ string) int {
|
|||||||
return 2
|
return 2
|
||||||
case typ == "int8", typ == "uint8", typ == "byte", typ == "bool":
|
case typ == "int8", typ == "uint8", typ == "byte", typ == "bool":
|
||||||
return 1
|
return 1
|
||||||
|
case typ == "string":
|
||||||
|
// ABI0 strings are a two-word header: data pointer + length.
|
||||||
|
return 16
|
||||||
|
case typ == "complex64":
|
||||||
|
return 8
|
||||||
|
case typ == "complex128":
|
||||||
|
return 16
|
||||||
default:
|
default:
|
||||||
// Default to 8 bytes for unknown types.
|
// Default to 8 bytes for unknown types.
|
||||||
return 8
|
return 8
|
||||||
|
|||||||
@@ -143,7 +143,10 @@ func TestParamSize(t *testing.T) {
|
|||||||
{"uint8", 1},
|
{"uint8", 1},
|
||||||
{"byte", 1},
|
{"byte", 1},
|
||||||
{"bool", 1},
|
{"bool", 1},
|
||||||
{"string", 8}, // unknown type defaults to 8
|
{"string", 16}, // ABI0 header: data pointer + length
|
||||||
|
{"complex64", 8},
|
||||||
|
{"complex128", 16},
|
||||||
|
{"error", 8}, // unknown type defaults to 8
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
if got := paramSize(tt.typ); got != tt.want {
|
if got := paramSize(tt.typ); got != tt.want {
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
TEXT ·enterJIT(SB), NOSPLIT, $0-16
|
TEXT ·enterJIT(SB), NOSPLIT, $0-16
|
||||||
MOVD fn+0(FP), R0 // target function address
|
MOVD fn+0(FP), R0 // target function address
|
||||||
MOVD R30, savedLR(SB) // save link register
|
MOVD R30, savedLR(SB) // save link register
|
||||||
MOVD R3, savedSP(SB) // save Go stack pointer
|
MOVD RSP, R3 // save Go stack pointer: R3 relays the value, the
|
||||||
|
MOVD R3, savedSP(SB) // arm64 assembler cannot store RSP to memory
|
||||||
MOVD stack+8(FP), R3 // load prepared stack pointer
|
MOVD stack+8(FP), R3 // load prepared stack pointer
|
||||||
MOVD 0(R3), R30 // load leaveJIT address into LR
|
MOVD 0(R3), R30 // load leaveJIT address into LR
|
||||||
MOVD R3, RSP // switch to the prepared stack: SP stays on
|
MOVD R3, RSP // switch to the prepared stack: SP stays on
|
||||||
|
|||||||
+17
-4
@@ -5,7 +5,6 @@ package verify
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
@@ -101,6 +100,9 @@ func (k *Kernel) FuncNames() []string {
|
|||||||
// The arg block is the raw bytes of the function's argument/result area
|
// The arg block is the raw bytes of the function's argument/result area
|
||||||
// (as declared by the TEXT $frame-args suffix). Returns the arg block
|
// (as declared by the TEXT $frame-args suffix). Returns the arg block
|
||||||
// after the call (with any results written back by the function).
|
// after the call (with any results written back by the function).
|
||||||
|
//
|
||||||
|
// Not safe for concurrent use: only one JIT call may be in flight at a
|
||||||
|
// time, the trampolines keep the saved registers in package globals.
|
||||||
func (k *Kernel) CallFunc(name string, args []byte) ([]byte, error) {
|
func (k *Kernel) CallFunc(name string, args []byte) ([]byte, error) {
|
||||||
idx, ok := k.funcs[name]
|
idx, ok := k.funcs[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -116,7 +118,12 @@ func (k *Kernel) CallFunc(name string, args []byte) ([]byte, error) {
|
|||||||
|
|
||||||
// CallFuncChecked invokes the named function with ABI sentinels and a
|
// CallFuncChecked invokes the named function with ABI sentinels and a
|
||||||
// red-zone canary, returning the argument block and an ABIReport that
|
// red-zone canary, returning the argument block and an ABIReport that
|
||||||
// records any callee-saved register or red-zone violations.
|
// records any callee-saved register or red-zone violations. The canary
|
||||||
|
// gap is sized from the function's declared frame, so legal frame writes
|
||||||
|
// do not count as violations.
|
||||||
|
//
|
||||||
|
// Not safe for concurrent use: only one JIT call may be in flight at a
|
||||||
|
// time, the trampolines keep the saved registers in package globals.
|
||||||
func (k *Kernel) CallFuncChecked(name string, args []byte) ([]byte, ABIReport, error) {
|
func (k *Kernel) CallFuncChecked(name string, args []byte) ([]byte, ABIReport, error) {
|
||||||
idx, ok := k.funcs[name]
|
idx, ok := k.funcs[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -127,11 +134,14 @@ func (k *Kernel) CallFuncChecked(name string, args []byte) ([]byte, ABIReport, e
|
|||||||
return nil, ABIReport{}, fmt.Errorf("verify: %s: arg block too small: got %d, need %d", name, 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)
|
fnAddr := k.exec.FuncAddr(fl.Offset)
|
||||||
return CallChecked(fnAddr, args)
|
return CallCheckedFrame(fnAddr, args, fl.Frame)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FuzzFuncCheckedByName is like FuzzFuncChecked but extracts the signature
|
// FuzzFuncCheckedByName is like FuzzFuncChecked but extracts the signature
|
||||||
// from the source code internally.
|
// from the source code internally.
|
||||||
|
//
|
||||||
|
// Not safe for concurrent use: only one JIT call may be in flight at a
|
||||||
|
// time, the trampolines keep the saved registers in package globals.
|
||||||
func (k *Kernel) FuzzFuncCheckedByName(name, src string, iterations int, seed int64) FuzzResult {
|
func (k *Kernel) FuzzFuncCheckedByName(name, src string, iterations int, seed int64) FuzzResult {
|
||||||
result := FuzzResult{Func: name, Iterations: iterations}
|
result := FuzzResult{Func: name, Iterations: iterations}
|
||||||
sig, ok := ExtractSignatures(src)[name]
|
sig, ok := ExtractSignatures(src)[name]
|
||||||
@@ -146,9 +156,12 @@ func (k *Kernel) FuzzFuncCheckedByName(name, src string, iterations int, seed in
|
|||||||
// FuzzFuncChecked combines fuzzing with ABI checks: it generates varied
|
// FuzzFuncChecked combines fuzzing with ABI checks: it generates varied
|
||||||
// inputs and verifies that callee-saved registers and the red zone are
|
// inputs and verifies that callee-saved registers and the red zone are
|
||||||
// preserved even on deep execution paths (not just early exits).
|
// preserved even on deep execution paths (not just early exits).
|
||||||
|
//
|
||||||
|
// Not safe for concurrent use: only one JIT call may be in flight at a
|
||||||
|
// time, the trampolines keep the saved registers in package globals.
|
||||||
func (k *Kernel) FuzzFuncChecked(name string, sig funcSig, iterations int, seed int64) FuzzResult {
|
func (k *Kernel) FuzzFuncChecked(name string, sig funcSig, iterations int, seed int64) FuzzResult {
|
||||||
result := FuzzResult{Func: name, Iterations: iterations}
|
result := FuzzResult{Func: name, Iterations: iterations}
|
||||||
rng := rand.New(rand.NewSource(seed))
|
rng := newRNG(seed)
|
||||||
|
|
||||||
fl, err := k.Func(name)
|
fl, err := k.Func(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user