fix(verify): isolate smoke and abi sweeps in a child process
Assisted-by: GLM 5.3
This commit is contained in:
+119
-43
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user