diff --git a/asm/assembler_fuzz_test.go b/asm/assembler_fuzz_test.go index 33ca596..8fe74e8 100644 --- a/asm/assembler_fuzz_test.go +++ b/asm/assembler_fuzz_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "sourcedock.dev/petrbalvin/gasm-sdk/ast" "sourcedock.dev/petrbalvin/gasm-sdk/parser" ) @@ -17,11 +18,11 @@ import ( // directory, so a seed's #include resolves the way the CLI's -I list does. var fuzzIncludeDirs = []string{filepath.Join("testdata", "include")} -// corpusSeeds 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. The non-amd64 kernels exercise the rejection -// path (the fixed amd64 target reports them as diagnostics); the amd64 ones -// reach the encoder. +// corpusSeeds seeds every fuzz target with the repository's kernels, so a +// plain `go test` run replays each seed as a regression case and CI exercises +// them without any fuzzing budget. Kernels of a foreign architecture +// exercise the rejection path (the fixed target reports them as diagnostics); +// kernels of the target's own architecture reach its encoder. func corpusSeeds(f *testing.F) { for _, pattern := range []string{ "../testdata/*.s", @@ -36,14 +37,15 @@ func corpusSeeds(f *testing.F) { } } -// FuzzAssembleAMD64 hammers the full parse-and-assemble pipeline for the -// fixed amd64 target with arbitrary source: expansion (macros and includes) -// included, matching the CLI's own pipeline. The contract: +// fuzzAssemble is the whole fuzz body, shared by every target and one line +// apart between them: parse with macro and include expansion, assemble +// through the target's file-level entry, and hold the invariants. The +// contract: // // - no panic, however malformed the source (a crash fails the target); // - a rejected file yields a diagnostic and never a partial emission: -// AssembleFile returns a nil image beside its error, and the diagnostic -// is not empty; +// the assembler returns a nil image beside its error, and the +// diagnostic is not empty; // - the output is deterministic: the same source, parsed and assembled // again from scratch, produces the same bytes; // - no unbounded memory: the fence around every test run kills a run that @@ -54,6 +56,39 @@ func corpusSeeds(f *testing.F) { // line-oriented and tolerant, so it hands back a usable file either way, and // the assembler's own contract is to answer any file it is given with bytes // or with a diagnostic, never with a panic. +func fuzzAssemble(t *testing.T, name string, src string, assemble func(*ast.File) (*Image, error)) { + file, _ := parser.ParseWithOptions(name, src, + parser.Options{Expand: true, IncludeDirs: fuzzIncludeDirs}) + if file == nil { + t.Fatal("ParseWithOptions returned a nil file") + } + img, err := assemble(file) + if err != nil { + if img != nil { + t.Fatal("the assembler returned an image beside its error: a rejected file must not emit") + } + if strings.TrimSpace(err.Error()) == "" { + t.Fatal("rejection carries an empty diagnostic") + } + return + } + // Determinism: a second assembly of the same file must produce the same + // bytes. One parse serves both runs, so any mutation the assembler makes + // to the syntax tree it was handed shows up as differing bytes, and the + // workers' footprint under the shared memory fence stays that of a single + // parse. + img2, err2 := assemble(file) + if err2 != nil { + t.Fatalf("the second assembly failed where the first succeeded: %v", err2) + } + if !bytes.Equal(img.Bytes(), img2.Bytes()) { + t.Fatal("the same source assembled to different bytes") + } +} + +// FuzzAssembleAMD64 hammers the full parse-and-assemble pipeline for the +// fixed amd64 target with arbitrary source: expansion (macros and includes) +// included, matching the CLI's own pipeline. func FuzzAssembleAMD64(f *testing.F) { corpusSeeds(f) f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tRET\n") @@ -90,32 +125,57 @@ func FuzzAssembleAMD64(f *testing.F) { f.Add("#define A A\nA\n") f.Fuzz(func(t *testing.T, src string) { - file, _ := parser.ParseWithOptions("fuzz_amd64.s", src, - parser.Options{Expand: true, IncludeDirs: fuzzIncludeDirs}) - if file == nil { - t.Fatal("ParseWithOptions returned a nil file") - } - img, err := AssembleFile(file) - if err != nil { - if img != nil { - t.Fatal("AssembleFile returned an image beside its error: a rejected file must not emit") - } - if strings.TrimSpace(err.Error()) == "" { - t.Fatal("rejection carries an empty diagnostic") - } - return - } - // Determinism: a second assembly of the same file must produce the - // same bytes. One parse serves both runs, so any mutation the - // assembler makes to the syntax tree it was handed shows up as - // differing bytes, and the workers' footprint under the shared - // memory fence stays that of a single parse. - img2, err2 := AssembleFile(file) - if err2 != nil { - t.Fatalf("the second assembly failed where the first succeeded: %v", err2) - } - if !bytes.Equal(img.Bytes(), img2.Bytes()) { - t.Fatal("the same source assembled to different bytes") - } + fuzzAssemble(t, "fuzz_amd64.s", src, func(f *ast.File) (*Image, error) { + return AssembleFile(f) + }) + }) +} + +// FuzzAssembleARM64 hammers the same pipeline for the fixed arm64 target, +// whose encoder carries its own immediate classification, memory-offset +// gates and literal pool. The seeds pin the recent encoder families: the +// system registers and barriers, the LSE atomics and exclusive pairs, the +// NEON structure loads and stores, the ADDCON2 offset split, the pooled +// vector constants, and the macro and include expansion over arm64 +// spellings. The rejection shapes pin the diagnostic paths the accepted +// seeds never reach. +func FuzzAssembleARM64(f *testing.F) { + corpusSeeds(f) + f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tRET\n") + f.Add("TEXT ·f(SB), $16-8\n\tMOVW x+0(FP), R0\n\tMOVW R0, ret+8(FP)\n\tRET\n") + f.Add("TEXT ·f(SB), $256-0\n\tCALL ·helper(SB)\n\tRET\nTEXT ·helper(SB), NOSPLIT, $0\n\tRET\n") + f.Add("#define L(n) MOVD $n, R0\nTEXT ·f(SB), NOSPLIT, $0\n\tL(7)\n\tRET\n") + f.Add("#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n") + f.Add("#include \"fuzzdefs.h\"\nTEXT ·f(SB), $16-8\n\tMOVD KONST, R0\n\tMOVD ARG(x), R1\n\tRET\n") + f.Add("DATA d<>+0(SB)/8, $0xf4f8fcff\nDATA d<>+4(SB)/4, $1\nGLOBL d<>(SB), RODATA, $8\n" + + "TEXT ·f(SB), NOSPLIT, $0\n\tMOVD d<>(SB), R0\n\tRET\n") + f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tMRS DCZID_EL0, R3\n\tMRS CNTVCT_EL0, R0\n\tMSR $3, SPSel\n" + + "\tMSR $9, DAIFSet\n\tDMB $15\n\tDSB $4\n\tISB $1\n\tDC ZVA, R4\n\tSVC $0\n\tBRK $35943\n\tRET\n") + f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tLDADDB R2, (R1), R3\n\tLDADDD R2, (R1), ZR\n\tCASW R2, (R1), R3\n" + + "\tSWPD R2, (R1), ZR\n\tLDXR (R1), R2\n\tLDAXRW (R1), R5\n\tSTXR R2, (R1), R6\n\tSTLXRB R3, (R1), R6\n" + + "\tLDXP (R1), (R2, R3)\n\tSTXP (R2, R3), (R1), R6\n\tRET\n") + f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tVLD1 (R2), [V21.B16]\n\tVLD1.P 32(R1), [V2.B16, V3.B16]\n" + + "\tVLD1R (R0), [V0.B16]\n\tVST1 [V2.S4, V3.S4], (R14)\n\tVST1.P [V2.B16], (R1)\n\tRET\n") + f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tADD $0xaaaaaa, R2, R3\n\tSUB $0x186a0, R2, R3\n\tADDW $0x60060, R2\n\tCMP $40960, R0\n\tRET\n") + f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tVMOVD $0x123456789ABCDEF0, V0\n\tVMOVQ $0x12345678, $0x9ABCDEF0, V1\n\tRET\n") + f.Add("TEXT ·f(SB), NOSPLIT, $0-8\n\tMOVW $-1, R0\n\tB after1\n\tMOVD $0x0001000200030004, R1\n" + + "after1:\n\tMOVD R1, ret+0(FP)\n\tRET\n") + f.Add("TEXT ·f(SB), NOSPLIT, $0\nL1:\n\tCBZ R1, L1\n\tTBZ $3, R2, L1\n\tBEQ L1\n\tJMP L1\n\tCALL (R5)\n\tRET\n") + f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tPCALIGN $16\n\tMOVD R0, R1\n\tPCALIGN $32\n\tRET\n") + f.Add("TEXT ·f(SB), $0\n\tPCDATA $0, $1\n\tFUNCDATA $0, ·meta(SB)\n\tRET\n") + f.Add("TEXT ·f(SB), $32-0\n\tMOVD R0, x-8(SP)\n\tRET\n") + // Shapes that must be rejected: each pins a diagnostic path the seeds + // above never reach. + f.Add("TEXT ·f(SB), $0\n\tBOGUSINSTR R0, R1\n\tRET\n") + f.Add("GLOBL d(SB), $-8\n") + f.Add("GLOBL d(SB), $0x4000001\n") + f.Add("DATA d+0(SB)/9, $1\nGLOBL d(SB), $8\n") + f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tADD R1, X99\n\tRET\n") + f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tMOVD $1, R1\n\tMOVD 0x1000000(R1), R2\n\tRET\n") + f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tVLD1 (R2), [V21.B17]\n\tRET\n") + f.Add("#define A A\nA\n") + + f.Fuzz(func(t *testing.T, src string) { + fuzzAssemble(t, "fuzz_arm64.s", src, AssembleFileARM64) }) }