// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package verify import ( "encoding/binary" "encoding/hex" "fmt" "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 for part := range strings.SplitSeq(spec, ",") { 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)] } } } } // 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 } for part := range strings.SplitSeq(spec, ",") { 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) { eq := strings.Index(part, "=") if eq <= 0 { return "", 0, fmt.Errorf("scalar arg spec %q: want name=value", part) } name := strings.TrimSpace(part[:eq]) valStr := strings.TrimSpace(part[eq+1:]) var val uint64 var err error if strings.HasPrefix(valStr, "0x") || strings.HasPrefix(valStr, "0X") { _, err = fmt.Sscanf(valStr, "0x%x", &val) } else { _, err = fmt.Sscanf(valStr, "%d", &val) } if err != nil || name == "" { return "", 0, fmt.Errorf("scalar arg spec %q: bad value", part) } return name, val, nil }