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")
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 <func> [--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