fix(verify): isolate smoke and abi sweeps in a child process
Assisted-by: GLM 5.3
This commit is contained in:
+118
-42
@@ -1052,6 +1052,7 @@ decoders) that crash on random input but should succeed on valid data.
|
|||||||
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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user