fix(verify): isolate smoke and abi sweeps in a child process

Assisted-by: GLM 5.3
This commit is contained in:
2026-08-24 21:01:32 +02:00
parent eade875b53
commit eb3c79c3c8
2 changed files with 174 additions and 43 deletions
+119 -43
View File
@@ -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") 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) 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") 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)") 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") 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 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. // 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)
@@ -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 { if *smoke || *abi {
type checkResult struct { type checkResult struct {
name string name string
@@ -1259,53 +1282,16 @@ decoders) that crash on random input but should succeed on valid data.
continue continue
} }
wg.Add(1) wg.Add(1)
go func(name string, fl asm.FuncLayout) { go func(name string) {
defer wg.Done() defer wg.Done()
sem <- struct{}{} sem <- struct{}{}
defer func() { <-sem }() defer func() { <-sem }()
var msgs []string msg, fail := sweepInSubprocess(path, name, *smoke, *abi, *abiN)
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")
}
}
}
mu.Lock() 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() mu.Unlock()
}(name, fl) }(name)
} }
wg.Wait() wg.Wait()
@@ -1363,6 +1349,96 @@ func fuzzInSubprocess(path, funcName string, n int) string {
return strings.TrimSpace(string(out)) 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 <func> [--buf spec] [--repeat n]`. // cmdVerifyCall implements `gasm verify --call <func> [--buf spec] [--repeat n]`.
// It invokes a single function with user-supplied buffers and prints the arg // 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 // block before and after the call, so the user can inspect return values and
+55
View File
@@ -7,8 +7,11 @@ import (
"bytes" "bytes"
"io" "io"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"syscall"
"testing" "testing"
) )
@@ -237,3 +240,55 @@ func TestCmdArgErrors(t *testing.T) {
t.Errorf("cmdParse() code = %d, want 2", code) 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)
}
}