From eb3c79c3c85c660aafd6b1543f0402cffcd04d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Mon, 24 Aug 2026 21:01:32 +0200 Subject: [PATCH] fix(verify): isolate smoke and abi sweeps in a child process Assisted-by: GLM 5.3 --- cmd/gasm/main.go | 162 +++++++++++++++++++++++++++++++----------- cmd/gasm/main_test.go | 55 ++++++++++++++ 2 files changed, 174 insertions(+), 43 deletions(-) diff --git a/cmd/gasm/main.go b/cmd/gasm/main.go index 073e4be..f482aac 100644 --- a/cmd/gasm/main.go +++ b/cmd/gasm/main.go @@ -1051,7 +1051,8 @@ decoders) that crash on random input but should succeed on valid data. 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) + fuzzOne := fs.String("fuzz-one", "", "") // hidden: fuzz a single function (subprocess mode) + sweepOne := fs.String("sweep-one", "", "") // hidden: smoke/abi a single function (subprocess mode) call := fs.String("call", "", "call a single function with -buf instead of the sweeps") bufSpec := fs.String("buf", "", "buffer spec for -call: name:size:pattern[,name:size:pattern] (zero, ones, seq, or hex)") repeat := fs.Int("repeat", 1, "number of times to repeat a -call invocation") @@ -1126,6 +1127,25 @@ decoders) that crash on random input but should succeed on valid data. return 0 } + // Subprocess mode: run the smoke/abi checks for a single function and + // exit with the accumulated status. The parent interprets a clean exit + // as success, a non-zero exit as failure and death-by-signal as a crash. + if *sweepOne != "" { + fl, err := k.Func(*sweepOne) + if err != nil || !fl.NoSplit { + fmt.Fprintf(os.Stderr, "gasm verify: %s: %v\n", *sweepOne, err) + return 1 + } + msgs, failed := runSweepChecks(k, path, *sweepOne, fl, *smoke, *abi, *abiN) + for _, m := range msgs { + fmt.Println(m) + } + if failed { + return 1 + } + return 0 + } + // Ground-truth comparison: assemble with go tool asm and compare bytes. if *groundTruth { gt, err := verify.GroundTruth(path) @@ -1241,7 +1261,10 @@ decoders) that crash on random input but should succeed on valid data. } } - // Run smoke and ABI checks in parallel. + // Run smoke and ABI checks in parallel, each function in its own child + // process: the JIT'd code runs with zeroed or fuzzed arguments, and a + // function that dereferences them faults — the crash is reported as a + // CRASH line instead of killing this process (mirrors fuzzInSubprocess). if *smoke || *abi { type checkResult struct { name string @@ -1259,53 +1282,16 @@ decoders) that crash on random input but should succeed on valid data. continue } wg.Add(1) - go func(name string, fl asm.FuncLayout) { + go func(name string) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() - var msgs []string - failed := false - - if *smoke { - args := make([]byte, fl.Args) - _, err := k.CallFunc(name, args) - if err != nil { - msgs = append(msgs, fmt.Sprintf(" smoke: FAIL — %v", err)) - failed = true - } else { - msgs = append(msgs, " smoke: OK") - } - } - - if *abi { - if src, err := readSource(path); err == nil { - result := k.FuzzFuncCheckedByName(name, src, *abiN, int64(*abiN)) - if result.Mismatches > 0 { - msgs = append(msgs, fmt.Sprintf(" abi: %s", result)) - failed = true - } else { - msgs = append(msgs, fmt.Sprintf(" abi: clean (%d varied inputs)", result.Matches)) - } - } else { - args := make([]byte, fl.Args) - _, report, err := k.CallFuncChecked(name, args) - if err != nil { - msgs = append(msgs, fmt.Sprintf(" abi: FAIL — %v", err)) - failed = true - } else if !report.OK() { - msgs = append(msgs, fmt.Sprintf(" abi: %s", report)) - failed = true - } else { - msgs = append(msgs, " abi: clean") - } - } - } - + msg, fail := sweepInSubprocess(path, name, *smoke, *abi, *abiN) mu.Lock() - results = append(results, checkResult{name: name, msg: strings.Join(msgs, "\n"), fail: failed}) + results = append(results, checkResult{name: name, msg: msg, fail: fail}) mu.Unlock() - }(name, fl) + }(name) } wg.Wait() @@ -1363,6 +1349,96 @@ func fuzzInSubprocess(path, funcName string, n int) string { return strings.TrimSpace(string(out)) } +// sweepInSubprocess runs the smoke/abi checks for a single function in a +// child process. If the child is killed by a signal (e.g. SIGSEGV from a +// function that dereferences its zeroed or fuzzed arguments), it returns a +// CRASH report instead of dying — the same isolation fuzzInSubprocess +// provides for the fuzz sweep. +func sweepInSubprocess(path, funcName string, smoke, abi bool, abiN int) (string, bool) { + self, err := os.Executable() + if err != nil { + return fmt.Sprintf(" smoke/abi: FAIL — cannot find self: %v", err), true + } + args := []string{"verify", "--sweep-one=" + funcName} + if smoke { + args = append(args, "-smoke") + } + if abi { + args = append(args, "-abi", "-abi-n", strconv.Itoa(abiN)) + } + args = append(args, path) + cmd := exec.Command(self, args...) + out, err := cmd.CombinedOutput() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + ws, ok := exitErr.Sys().(syscall.WaitStatus) + if ok && ws.Signaled() { + return fmt.Sprintf(" smoke/abi: CRASH (%v — the function faults on zeroed or fuzzed\n arguments; verify it with -call and valid buffers)", ws.Signal()), true + } + } + // Non-zero exit without a signal: the checks themselves failed and + // the child already printed the diagnostic lines. + return sweepCheckLines(out), true + } + return sweepCheckLines(out), false +} + +// sweepCheckLines extracts the check-result lines from child output, +// dropping the child's own file/function banners (the parent prints those). +func sweepCheckLines(out []byte) string { + var lines []string + for _, l := range strings.Split(string(out), "\n") { + t := strings.TrimSpace(l) + if strings.HasPrefix(t, "smoke:") || strings.HasPrefix(t, "abi:") { + lines = append(lines, " "+t) + } + } + return strings.Join(lines, "\n") +} + +// runSweepChecks performs the in-process smoke and ABI checks for one +// function — the child half of sweepInSubprocess. +func runSweepChecks(k *verify.Kernel, path, name string, fl asm.FuncLayout, smoke, abi bool, abiN int) ([]string, bool) { + var msgs []string + failed := false + + if smoke { + args := make([]byte, fl.Args) + _, err := k.CallFunc(name, args) + if err != nil { + msgs = append(msgs, fmt.Sprintf(" smoke: FAIL — %v", err)) + failed = true + } else { + msgs = append(msgs, " smoke: OK") + } + } + + if abi { + if src, err := readSource(path); err == nil { + result := k.FuzzFuncCheckedByName(name, src, abiN, int64(abiN)) + if result.Mismatches > 0 { + msgs = append(msgs, fmt.Sprintf(" abi: %s", result)) + failed = true + } else { + msgs = append(msgs, fmt.Sprintf(" abi: clean (%d varied inputs)", result.Matches)) + } + } else { + args := make([]byte, fl.Args) + _, report, err := k.CallFuncChecked(name, args) + if err != nil { + msgs = append(msgs, fmt.Sprintf(" abi: FAIL — %v", err)) + failed = true + } else if !report.OK() { + msgs = append(msgs, fmt.Sprintf(" abi: %s", report)) + failed = true + } else { + msgs = append(msgs, " abi: clean") + } + } + } + return msgs, failed +} + // cmdVerifyCall implements `gasm verify --call [--buf spec] [--repeat n]`. // It invokes a single function with user-supplied buffers and prints the arg // block before and after the call, so the user can inspect return values and diff --git a/cmd/gasm/main_test.go b/cmd/gasm/main_test.go index c14c6af..8615bbe 100644 --- a/cmd/gasm/main_test.go +++ b/cmd/gasm/main_test.go @@ -7,8 +7,11 @@ import ( "bytes" "io" "os" + "os/exec" "path/filepath" + "runtime" "strings" + "syscall" "testing" ) @@ -237,3 +240,55 @@ func TestCmdArgErrors(t *testing.T) { t.Errorf("cmdParse() code = %d, want 2", code) } } + +// TestVerifySmokeCrashIsolation checks that a function faulting on its +// zeroed smoke arguments is reported as CRASH by a child process instead of +// killing `gasm verify` itself. +func TestVerifySmokeCrashIsolation(t *testing.T) { + if testing.Short() { + t.Skip("builds the gasm binary") + } + if runtime.GOARCH != "amd64" { + t.Skip("amd64 JIT only") + } + bin := filepath.Join(t.TempDir(), "gasm") + if out, err := exec.Command("go", "build", "-o", bin, ".").CombinedOutput(); err != nil { + t.Fatalf("build gasm: %v\n%s", err, out) + } + src := filepath.Join(t.TempDir(), "crash_amd64.s") + kernel := "#include \"textflag.h\"\n" + + "\n" + + "// func Fault(x []byte) int\n" + + "TEXT ·Fault(SB), NOSPLIT, $0-32\n" + + "\tMOVQ\tx+0(FP), AX\n" + + "\tMOVQ\t(AX), AX // faults on the zeroed nil pointer\n" + + "\tMOVQ\tAX, ret+24(FP)\n" + + "\tRET\n" + if err := os.WriteFile(src, []byte(kernel), 0o644); err != nil { + t.Fatal(err) + } + cmd := exec.Command(bin, "verify", "-smoke", src) + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatalf("expected a failure report, got success:\n%s", out) + } + if exitErr, ok := err.(*exec.ExitError); ok { + if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok && ws.Signaled() { + t.Fatalf("verify died from %v — the crash was not isolated:\n%s", ws.Signal(), out) + } + } + if !strings.Contains(string(out), "CRASH") { + t.Errorf("output does not report CRASH:\n%s", out) + } +} + +func TestSweepCheckLines(t *testing.T) { + out := []byte("crash_amd64.s: 1 functions JIT-loaded\n" + + " Fault: 21 bytes, args=32, frame=0 NOSPLIT\n" + + " smoke: OK\n" + + " abi: clean (10 varied inputs)\n") + want := " smoke: OK\n abi: clean (10 varied inputs)" + if got := sweepCheckLines(out); got != want { + t.Errorf("sweepCheckLines = %q, want %q", got, want) + } +}