Files
gasm-sdk/parser/fuzz_test.go
T
2026-10-02 00:40:20 +02:00

119 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package parser
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
)
// corpusFiles seeds a fuzz target with the repository's kernels, so a plain
// `go test` run replays every seed as a regression case and CI exercises them
// without any fuzzing budget.
func corpusFiles(f *testing.F) {
for _, pattern := range []string{
"../testdata/*.s",
"../testdata/verify/*.s",
} {
files, _ := filepath.Glob(pattern)
for _, path := range files {
if b, err := os.ReadFile(path); err == nil {
f.Add(string(b))
}
}
}
}
// FuzzParse hammers the parser with arbitrary input. The contract: no panic,
// always a usable file whether or not diagnostics were reported, and every
// diagnostic carries a real position and a non-empty message.
func FuzzParse(f *testing.F) {
corpusFiles(f)
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tRET\n")
f.Add("garbage ### ??? ::: \xff\xfe\n")
f.Add("#define A(x) x+1\nTEXT ·f(SB), $0\n\tA(2)\n\tRET\n")
f.Add("DATA t<>+0(SB)/8, $1\nGLOBL t<>(SB), RODATA, $8\n")
f.Add("TEXT ·f(SB), $0\n\tJMP (AX)\n\tCALL (BX)\n\tRET\n")
f.Add("\xef\xbb\xbfTEXT ·f(SB), $0\n") // BOM
f.Add("CALL internal∕runtime∕atomic·Xchg(SB)\n") // U+2215 path, U+00B7 name
f.Add("TEXT ·f(SB), $0\n\tJMP -3(PC)\n\tRET\n") // negative PC jump
f.Add("DATA d<>+0(SB)/8, $0xFFFFFFFFFFFFFFFF\n") // unsigned 64-bit immediate
f.Add("TEXT ·f(SB), $0-0xFFFFFFFFFFFFFFFF\n\tRET\n") // argument area overflow
f.Add("TEXT ·f(SB), $0\n\tMOVD $(1<<0|1<<9), R0\n\tRET\n") // constant expression
f.Add("TEXT ·f(SB), $0\n\tADD $-8, R10; SUB $~63, R11\n\tRET\n")
// Constant-expression nesting: an immediate and a displacement read
// through the recursive folder, so deep parenthesis groups probe its
// depth handling on every plain run. The immediate seed is deep enough
// that, before the folder carried a depth bound, it grew the goroutine
// stack past its limit with a fatal error.
f.Add("TEXT ·f(SB), $0\n\tMOVD $" + strings.Repeat("(", 4_000_000) + "1" + strings.Repeat(")", 4_000_000) + ", R0\n\tRET\n")
f.Add("TEXT ·f(SB), $0\n\tMOVQ " + strings.Repeat("(", 2000) + "1" + strings.Repeat(")", 2000) + "(AX), BX\n\tRET\n")
f.Add("TEXT ·f(SB), $0\n\tMOVD $" + strings.Repeat("~", 5000) + "1, R0\n\tRET\n")
f.Fuzz(func(t *testing.T, src string) {
file, errs := Parse("fuzz.s", src)
if file == nil {
t.Fatal("Parse returned a nil file")
}
for _, err := range errs {
var perr Error
if !errors.As(err, &perr) {
t.Fatalf("diagnostic %v is not a parser Error", err)
}
if !perr.Pos.IsValid() {
t.Fatalf("diagnostic %q carries no position", perr.Msg)
}
if strings.TrimSpace(perr.Msg) == "" {
t.Fatalf("diagnostic at %v carries no message", perr.Pos)
}
}
})
}
// FuzzParseExpand hammers the preprocessing path, macro expansion and include
// splicing included, with arbitrary input. The contract is FuzzParse's: no
// panic, a usable file, and diagnostics that name a place and a reason.
func FuzzParseExpand(f *testing.F) {
corpusFiles(f)
f.Add("#define A 1\nTEXT ·f(SB), $0\n\tMOVD $A, R0\n\tRET\n")
f.Add("#define A(x) x+1\nA(2)\n")
f.Add("#define A(x) ((x))\nA(A(A(1)))\n")
f.Add("#define A\nTEXT ·f(SB), $0\n\tRET\n") // empty body
f.Add("#define A A\nA\n") // self-reference
f.Add("#define A B\n#define B A\nA\n") // mutual recursion
f.Add("#ifdef X\n#else\n#endif\n")
f.Add("#ifndef X\none\n#else\ntwo\n#endif\n")
f.Add("#undef Y\n")
f.Add("#include \"textflag.h\"\n")
f.Add("#include \"no/such/header.h\"\n")
f.Add("#line 7 \"f.s\"\n")
f.Add("#define M(x) \\\n\tADD $x, R0 \\\n\tSUB $x, R1\nM(1)\n") // continuations
// A body that repeats its argument multiplies every nesting level; the
// expansion must stop at the work budget, not produce the exponential
// result it names.
f.Add("#define A(x) x x x x x x x x x x\nA(A(A(A(A(A(A(A(A(A(1))))))))))\n")
f.Fuzz(func(t *testing.T, src string) {
file, errs := ParseWithOptions("fuzz.s", src, Options{Expand: true})
if file == nil {
t.Fatal("ParseWithOptions returned a nil file")
}
for _, err := range errs {
var perr Error
if !errors.As(err, &perr) {
t.Fatalf("diagnostic %v is not a parser Error", err)
}
if !perr.Pos.IsValid() {
t.Fatalf("diagnostic %q carries no position", perr.Msg)
}
if strings.TrimSpace(perr.Msg) == "" {
t.Fatalf("diagnostic at %v carries no message", perr.Pos)
}
}
})
}