Compare commits

..
1 Commits
Author SHA1 Message Date
petrbalvin f20e0bf1e7 fix(verify): subprocess isolation for --fuzz, partial functions report CRASH gracefully
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
2 changed files with 84 additions and 6 deletions
+83 -5
View File
@@ -14,8 +14,11 @@ import (
"io" "io"
"io/fs" "io/fs"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"syscall"
"sourcedock.dev/petrbalvin/gasm-devkit/arch" "sourcedock.dev/petrbalvin/gasm-devkit/arch"
"sourcedock.dev/petrbalvin/gasm-devkit/asm" "sourcedock.dev/petrbalvin/gasm-devkit/asm"
@@ -30,7 +33,7 @@ import (
// version is the release version, stamped at build time via // version is the release version, stamped at build time via
// -ldflags "-X main.version=…" (defaulting to the current release). // -ldflags "-X main.version=…" (defaulting to the current release).
var version = "0.26.0" var version = "0.27.0"
func main() { func main() {
if len(os.Args) < 2 { 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") 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") 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") 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) fs.Parse(args)
if fs.NArg() != 1 { if fs.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-profile] <file.s>") fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-profile] <file.s>")
@@ -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)) fmt.Printf("%s: %d functions JIT-loaded\n", path, len(names))
rc := 0 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. // Ground-truth comparison: assemble with go tool asm and compare bytes.
if *groundTruth { if *groundTruth {
gt, err := verify.GroundTruth(path) 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) fmt.Printf(" %s: SKIP (not in go tool asm output)\n", name)
continue continue
} }
res := k.FuzzFunc(name, sig, goCode, *fuzzN, int64(fuzzed*7+42)) // Run in a subprocess: if the function crashes on random
fmt.Printf(" %s\n", res) // input (partial function), we report it and move on.
if !res.OK() { res := fuzzInSubprocess(path, name, *fuzzN)
rc = 1 if res != "" {
fmt.Printf(" %s\n", res)
if strings.Contains(res, "MISMATCH") {
rc = 1
}
} }
_ = sig
_ = goCode
fuzzed++ fuzzed++
} }
fmt.Printf("fuzz: %d functions tested, %d iterations each\n", fuzzed, *fuzzN) 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 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))
}
+1 -1
View File
@@ -3,7 +3,7 @@
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm). # gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
version := "0.26.0" version := "0.27.0"
default: default:
@just --list @just --list