2026-08-05 20:46:39 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
package verify
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/binary"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"fmt"
|
2026-08-29 16:15:14 +02:00
|
|
|
"strconv"
|
2026-08-05 20:46:39 +02:00
|
|
|
"strings"
|
|
|
|
|
"unsafe"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// BufSpec is one buffer allocation request parsed from the user's --buf spec.
|
|
|
|
|
type BufSpec struct {
|
|
|
|
|
Name string
|
|
|
|
|
Size int // declared slice length and capacity
|
|
|
|
|
Pattern string // "zero", "ones", "seq", or a hex blob
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ParseBufSpec parses a "name:size:pattern[,name:size:pattern]" spec string
|
|
|
|
|
// into individual buffer specs. Empty input yields an empty slice.
|
|
|
|
|
func ParseBufSpec(spec string) ([]BufSpec, error) {
|
|
|
|
|
if spec == "" {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
var out []BufSpec
|
2026-08-29 15:40:31 +02:00
|
|
|
for part := range strings.SplitSeq(spec, ",") {
|
2026-08-05 20:46:39 +02:00
|
|
|
fields := strings.SplitN(part, ":", 3)
|
|
|
|
|
if len(fields) != 3 {
|
|
|
|
|
return nil, fmt.Errorf("verify: invalid buffer spec %q (expected name:size:pattern)", part)
|
|
|
|
|
}
|
|
|
|
|
var size int
|
|
|
|
|
if _, err := fmt.Sscanf(fields[1], "%d", &size); err != nil || size <= 0 {
|
|
|
|
|
return nil, fmt.Errorf("verify: invalid buffer size %q in %q", fields[1], part)
|
|
|
|
|
}
|
|
|
|
|
out = append(out, BufSpec{Name: fields[0], Size: size, Pattern: fields[2]})
|
|
|
|
|
}
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// allocatedBuf is one live buffer in a pool.
|
|
|
|
|
type allocatedBuf struct {
|
|
|
|
|
spec BufSpec
|
|
|
|
|
data []byte // Size + safetyMargin bytes; the first Size are the live region
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// safetyMargin is the extra bytes allocated past the declared size so SIMD
|
|
|
|
|
// over-reads and functions that read slightly past len never touch unmapped
|
|
|
|
|
// memory. Matches the margin used by the fuzz generator.
|
|
|
|
|
const safetyMargin = 8192
|
|
|
|
|
|
|
|
|
|
// BufPool is a set of allocated buffers held alive for the duration of one or
|
|
|
|
|
// more calls. Buffers live on the Go heap (the JIT call is in-process); the
|
|
|
|
|
// pool keeps the backing slices referenced so the GC does not collect them
|
|
|
|
|
// before the call returns.
|
|
|
|
|
type BufPool struct {
|
|
|
|
|
bufs []allocatedBuf
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Alloc allocates and fills the buffers described by specs. The returned
|
|
|
|
|
// pool must be kept alive until every call using it has returned.
|
|
|
|
|
func (p *BufPool) Alloc(specs []BufSpec) error {
|
|
|
|
|
for _, s := range specs {
|
|
|
|
|
data := make([]byte, s.Size+safetyMargin)
|
|
|
|
|
fillBuffer(data, s.Pattern)
|
|
|
|
|
p.bufs = append(p.bufs, allocatedBuf{spec: s, data: data})
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close releases the pool. No-op for Go-heap buffers, but keeps the API
|
|
|
|
|
// symmetric with debug's mmap-backed pool.
|
|
|
|
|
func (p *BufPool) Close() {
|
|
|
|
|
p.bufs = nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// findByName returns the buffer with the given spec name, if any.
|
|
|
|
|
func (p *BufPool) findByName(name string) *allocatedBuf {
|
|
|
|
|
for i := range p.bufs {
|
|
|
|
|
if p.bufs[i].spec.Name == name {
|
|
|
|
|
return &p.bufs[i]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BuildArgs constructs an ABI0 argument block of argSize bytes for the given
|
|
|
|
|
// layout, placing each buffer's pointer/length/capacity at the matching
|
|
|
|
|
// parameter offset. Parameters whose names match a buffer spec get the
|
|
|
|
|
// buffer address; non-pointer parameters and unmatched pointers are zeroed.
|
|
|
|
|
//
|
|
|
|
|
// Matching is by exact name, then by prefix (a buffer named "src" matches a
|
|
|
|
|
// parameter named "src" or "srcBuf"), mirroring the debug allocator.
|
|
|
|
|
func (p *BufPool) BuildArgs(layout []ArgOffset, argSize int) []byte {
|
|
|
|
|
args := make([]byte, argSize)
|
|
|
|
|
for _, a := range layout {
|
|
|
|
|
if !a.IsPtr {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
buf := p.matchBuf(a.Name)
|
|
|
|
|
if buf == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if a.Offset+8 <= len(args) {
|
|
|
|
|
binary.LittleEndian.PutUint64(args[a.Offset:a.Offset+8], uint64(uintptr(unsafe.Pointer(&buf.data[0]))))
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(a.Typ, "[]") && a.Offset+24 <= len(args) {
|
|
|
|
|
binary.LittleEndian.PutUint64(args[a.Offset+8:a.Offset+16], uint64(buf.spec.Size))
|
|
|
|
|
binary.LittleEndian.PutUint64(args[a.Offset+16:a.Offset+24], uint64(buf.spec.Size))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return args
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// matchBuf finds a buffer matching the parameter name (exact, then prefix).
|
|
|
|
|
func (p *BufPool) matchBuf(name string) *allocatedBuf {
|
|
|
|
|
if b := p.findByName(name); b != nil {
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
for i := range p.bufs {
|
|
|
|
|
if strings.HasPrefix(name, p.bufs[i].spec.Name) {
|
|
|
|
|
return &p.bufs[i]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
func fillBuffer(buf []byte, pattern string) {
|
|
|
|
|
switch pattern {
|
|
|
|
|
case "zero":
|
|
|
|
|
// Already zeroed by make.
|
|
|
|
|
case "ones":
|
|
|
|
|
for i := range buf {
|
|
|
|
|
buf[i] = 0xFF
|
|
|
|
|
}
|
|
|
|
|
case "seq":
|
|
|
|
|
for i := range buf {
|
|
|
|
|
buf[i] = byte(i)
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
if data, err := hex.DecodeString(pattern); err == nil && len(data) > 0 {
|
|
|
|
|
for i := range buf {
|
|
|
|
|
buf[i] = data[i%len(data)]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-29 14:03:45 +02:00
|
|
|
|
|
|
|
|
// ApplyScalarArgs writes user-supplied scalar argument values into an ABI0
|
|
|
|
|
// argument block after BuildArgs. Each name=value pair addresses the layout
|
|
|
|
|
// entry of that parameter name; only word-sized scalar parameters (int,
|
|
|
|
|
// int64, uint64, and their named spellings) accept values, so a typo'd name
|
|
|
|
|
// or a slice parameter fails loudly instead of silently corrupting the call.
|
|
|
|
|
func ApplyScalarArgs(args []byte, layout []ArgOffset, scalars map[string]uint64) error {
|
|
|
|
|
if len(scalars) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
byName := make(map[string]ArgOffset, len(layout))
|
|
|
|
|
for _, l := range layout {
|
|
|
|
|
byName[l.Name] = l
|
|
|
|
|
}
|
|
|
|
|
for name, val := range scalars {
|
|
|
|
|
l, ok := byName[name]
|
|
|
|
|
if !ok {
|
|
|
|
|
return fmt.Errorf("scalar arg %q: no such parameter", name)
|
|
|
|
|
}
|
|
|
|
|
if l.IsPtr || l.Size != 8 {
|
|
|
|
|
return fmt.Errorf("scalar arg %q: parameter has type %s; only word-sized scalars accept values", name, l.Typ)
|
|
|
|
|
}
|
|
|
|
|
if l.Offset+8 > len(args) {
|
|
|
|
|
return fmt.Errorf("scalar arg %q: offset %d outside the %d-byte arg block", name, l.Offset, len(args))
|
|
|
|
|
}
|
|
|
|
|
binary.LittleEndian.PutUint64(args[l.Offset:], val)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ParseScalarArgs parses a "name=value[,name=value...]" spec into a map.
|
|
|
|
|
// Values are decimal or 0x-prefixed hex.
|
|
|
|
|
func ParseScalarArgs(spec string) (map[string]uint64, error) {
|
|
|
|
|
out := map[string]uint64{}
|
|
|
|
|
if strings.TrimSpace(spec) == "" {
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
2026-08-29 15:40:31 +02:00
|
|
|
for part := range strings.SplitSeq(spec, ",") {
|
2026-08-29 14:03:45 +02:00
|
|
|
part = strings.TrimSpace(part)
|
|
|
|
|
if part == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
name, val, err := splitScalar(part)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
out[name] = val
|
|
|
|
|
}
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func splitScalar(part string) (string, uint64, error) {
|
2026-08-29 16:15:14 +02:00
|
|
|
name, valStr, ok := strings.Cut(part, "=")
|
|
|
|
|
if !ok || strings.TrimSpace(name) == "" {
|
2026-08-29 14:03:45 +02:00
|
|
|
return "", 0, fmt.Errorf("scalar arg spec %q: want name=value", part)
|
|
|
|
|
}
|
2026-08-29 16:15:14 +02:00
|
|
|
val, err := strconv.ParseUint(strings.TrimSpace(valStr), 0, 64)
|
|
|
|
|
if err != nil {
|
2026-08-29 14:03:45 +02:00
|
|
|
return "", 0, fmt.Errorf("scalar arg spec %q: bad value", part)
|
|
|
|
|
}
|
2026-08-29 16:15:14 +02:00
|
|
|
return strings.TrimSpace(name), val, nil
|
2026-08-29 14:03:45 +02:00
|
|
|
}
|