From f20e0bf1e75c5f521878b976343218647afd265a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Sat, 1 Aug 2026 18:08:10 +0200 Subject: [PATCH] fix(verify): subprocess isolation for --fuzz, partial functions report CRASH gracefully Assisted-by: Qwen 3.8 Max Preview --- cmd/gasm/main.go | 88 +++++++++++++++++++++++++++++++++++++++++++++--- justfile | 2 +- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/cmd/gasm/main.go b/cmd/gasm/main.go index 7543597..cfed313 100644 --- a/cmd/gasm/main.go +++ b/cmd/gasm/main.go @@ -14,8 +14,11 @@ import ( "io" "io/fs" "os" + "os/exec" "path/filepath" + "strconv" "strings" + "syscall" "sourcedock.dev/petrbalvin/gasm-devkit/arch" "sourcedock.dev/petrbalvin/gasm-devkit/asm" @@ -30,7 +33,7 @@ import ( // version is the release version, stamped at build time via // -ldflags "-X main.version=…" (defaulting to the current release). -var version = "0.26.0" +var version = "0.27.0" func main() { if len(os.Args) < 2 { @@ -494,6 +497,7 @@ With -profile, the static basic-block structure is listed for each function. groundTruth := fs.Bool("ground-truth", false, "compare machine code byte-for-byte against go tool asm") fuzz := fs.Bool("fuzz", false, "differential fuzz: JIT both gasm and go-tool-asm versions, compare outputs") fuzzN := fs.Int("n", 1000, "number of fuzz iterations per function") + fuzzOne := fs.String("fuzz-one", "", "") // hidden: fuzz a single function (subprocess mode) fs.Parse(args) if fs.NArg() != 1 { fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-profile] ") @@ -516,6 +520,37 @@ With -profile, the static basic-block structure is listed for each function. fmt.Printf("%s: %d functions JIT-loaded\n", path, len(names)) rc := 0 + // Subprocess mode: fuzz a single function and exit. + if *fuzzOne != "" { + gt, err := verify.GroundTruth(path) + if err != nil { + fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) + return 1 + } + src, err := readSource(path) + if err != nil { + fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) + return 1 + } + sigs := verify.ExtractSignatures(src) + sig, ok := sigs[*fuzzOne] + if !ok { + fmt.Printf("%s: no signature\n", *fuzzOne) + return 0 + } + goCode, ok := gt[*fuzzOne] + if !ok { + fmt.Printf("%s: not in go tool asm\n", *fuzzOne) + return 0 + } + res := k.FuzzFunc(*fuzzOne, sig, goCode, *fuzzN, 42) + fmt.Printf("%s\n", res) + if !res.OK() { + return 1 + } + return 0 + } + // Ground-truth comparison: assemble with go tool asm and compare bytes. if *groundTruth { gt, err := verify.GroundTruth(path) @@ -598,11 +633,17 @@ With -profile, the static basic-block structure is listed for each function. fmt.Printf(" %s: SKIP (not in go tool asm output)\n", name) continue } - res := k.FuzzFunc(name, sig, goCode, *fuzzN, int64(fuzzed*7+42)) - fmt.Printf(" %s\n", res) - if !res.OK() { - rc = 1 + // Run in a subprocess: if the function crashes on random + // input (partial function), we report it and move on. + res := fuzzInSubprocess(path, name, *fuzzN) + if res != "" { + fmt.Printf(" %s\n", res) + if strings.Contains(res, "MISMATCH") { + rc = 1 + } } + _ = sig + _ = goCode fuzzed++ } fmt.Printf("fuzz: %d functions tested, %d iterations each\n", fuzzed, *fuzzN) @@ -651,3 +692,40 @@ With -profile, the static basic-block structure is listed for each function. } return rc } + +// fuzzInSubprocess runs the fuzz for a single function in a child process. +// If the child is killed by a signal (e.g. SIGSEGV from a partial function +// faulting on random input), it returns a CRASH report instead of dying. +func fuzzInSubprocess(path, funcName string, n int) string { + self, err := os.Executable() + if err != nil { + return fmt.Sprintf("%s: cannot find self: %v", funcName, err) + } + cmd := exec.Command(self, "verify", "--fuzz-one="+funcName, "-n", strconv.Itoa(n), path) + out, err := cmd.CombinedOutput() + if err != nil { + // Check if the child was killed by a signal. + if exitErr, ok := err.(*exec.ExitError); ok { + ws := exitErr.Sys().(syscall.WaitStatus) + if ws.Signaled() { + return fmt.Sprintf("%s: CRASH (%v — partial function, use --ground-truth)", funcName, ws.Signal()) + } + } + // Non-zero exit without a signal: the fuzz reported mismatches. + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + for _, l := range lines { + if strings.Contains(l, funcName) { + return strings.TrimSpace(l) + } + } + return fmt.Sprintf("%s: FAIL (exit %v)", funcName, err) + } + // Success: extract the result line. + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + for _, l := range lines { + if strings.Contains(l, funcName) { + return strings.TrimSpace(l) + } + } + return strings.TrimSpace(string(out)) +} diff --git a/justfile b/justfile index e05d4c6..2d8897d 100644 --- a/justfile +++ b/justfile @@ -3,7 +3,7 @@ # gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm). -version := "0.26.0" +version := "0.27.0" default: @just --list