fix(verify): arm64 stack save, adaptive canary and host gating
Assisted-by: GLM 5.3
This commit is contained in:
+110
-11
@@ -6,7 +6,7 @@ package verify
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"math/rand/v2"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -14,6 +14,28 @@ import (
|
||||
"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
|
||||
// function.
|
||||
type FuzzResult struct {
|
||||
@@ -43,9 +65,9 @@ func (r FuzzResult) String() string {
|
||||
|
||||
// CorpusArg is one replayable argument of a corpus entry.
|
||||
type CorpusArg struct {
|
||||
Kind string `json:"kind"` // "slice", "ptr", "int", "scalar"
|
||||
Len int `json:"len,omitempty"` // slice: declared length in elements
|
||||
Data string `json:"data,omitempty"` // slice/ptr: hex-encoded buffer content
|
||||
Kind string `json:"kind"` // "slice", "string", "ptr", "int", "scalar"
|
||||
Len int `json:"len,omitempty"` // slice: declared length in elements; string: length in bytes
|
||||
Data string `json:"data,omitempty"` // slice/string/ptr: hex-encoded buffer content
|
||||
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
|
||||
// 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 {
|
||||
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
|
||||
// crash or a mismatch), receiving a replayable corpus entry. A nil hook
|
||||
// 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 {
|
||||
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.
|
||||
goExec, err := Map(goCode)
|
||||
@@ -203,6 +231,18 @@ func (k *Kernel) FuzzFuncHook(name string, sig funcSig, goCode []byte, iteration
|
||||
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 {
|
||||
// Generate inputs and build TWO independent arg blocks (one per
|
||||
// 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 {
|
||||
case strings.HasPrefix(p.typ, "[]"):
|
||||
elemSize := elemSizeFor(p.typ)
|
||||
n := 1 + rng.Intn(127)
|
||||
n := 1 + rng.IntN(127)
|
||||
var declaredLen int
|
||||
if sliceIdx == 0 {
|
||||
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.
|
||||
buf1 := make([]byte, bufBytes)
|
||||
buf2 := make([]byte, bufBytes)
|
||||
rng.Read(buf1[:n*elemSize])
|
||||
fillRandom(rng, buf1[:n*elemSize])
|
||||
copy(buf2, buf1)
|
||||
bufs = append(bufs, buf1, buf2)
|
||||
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]),
|
||||
})
|
||||
|
||||
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, "*["):
|
||||
nElem := arrayLen(p.typ)
|
||||
elem := elemSizeFor("[]" + p.typ[strings.Index(p.typ, "]")+1:])
|
||||
size := max(nElem*elem, 8)
|
||||
buf1 := make([]byte, size)
|
||||
buf2 := make([]byte, size)
|
||||
rng.Read(buf1)
|
||||
fillRandom(rng, buf1)
|
||||
copy(buf2, buf1)
|
||||
bufs = append(bufs, buf1, buf2)
|
||||
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":
|
||||
v := uint64(rng.Intn(256))
|
||||
v := uint64(rng.IntN(256))
|
||||
putU64(gasmArgs, off, v)
|
||||
putU64(goArgs, off, v)
|
||||
off += 8
|
||||
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:
|
||||
v := rng.Uint64()
|
||||
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
|
||||
// named function once, returning the argument block after the call. Slice
|
||||
// buffers get the same safety padding the fuzzer uses, so over-reads that
|
||||
// were harmless during the original run stay harmless on replay.
|
||||
// and string buffers get the same safety padding the fuzzer uses, so
|
||||
// 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) {
|
||||
fl, err := k.Func(name)
|
||||
if err != nil {
|
||||
@@ -374,6 +452,21 @@ func (k *Kernel) ReplayEntry(name string, e CorpusEntry) ([]byte, error) {
|
||||
putU64(args, off+16, uint64(a.Len))
|
||||
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":
|
||||
data, err := hex.DecodeString(a.Data)
|
||||
if err != nil {
|
||||
@@ -431,6 +524,12 @@ func paramsSize(sig funcSig) int {
|
||||
size += 8 // pointer
|
||||
case p.typ == "bool":
|
||||
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:
|
||||
size += 8 // int, uint, etc.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user