From 288cfa16e79d64f1dbaafe91d4aee223fe0dd494 Mon Sep 17 00:00:00 2001 From: Ethan Graham Date: Mon, 15 Sep 2025 13:13:20 +0000 Subject: syz-kfuzztest: add syz-kfuzztest executable syz-kfuzztest is a new standalone designed for fuzzing KFuzzTest on a live kernel VM (e.g., inside QEMU). It has no dependencies on the executor program, instead directly writing into a KFuzzTest target's debugfs entry. Signed-off-by: Ethan Graham --- Makefile | 5 +- pkg/kfuzztest-executor/executor.go | 123 +++++++++++++++++++++++ pkg/kfuzztest-manager/manager.go | 201 +++++++++++++++++++++++++++++++++++++ syz-kfuzztest/main.go | 61 +++++++++++ 4 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 pkg/kfuzztest-executor/executor.go create mode 100644 pkg/kfuzztest-manager/manager.go create mode 100644 syz-kfuzztest/main.go diff --git a/Makefile b/Makefile index bf7bca804..fc4c95332 100644 --- a/Makefile +++ b/Makefile @@ -104,7 +104,7 @@ ifeq ("$(TARGETOS)", "trusty") endif .PHONY: all clean host target \ - manager executor ci hub \ + manager executor kfuzztest ci hub \ execprog mutate prog2c trace2syz repro upgrade db \ usbgen symbolize cover kconf syz-build crush \ bin/syz-extract bin/syz-fmt \ @@ -217,6 +217,9 @@ syz-build: bisect: descriptions GOOS=$(HOSTOS) GOARCH=$(HOSTARCH) $(HOSTGO) build $(GOHOSTFLAGS) -o ./bin/syz-bisect github.com/google/syzkaller/tools/syz-bisect +kfuzztest: descriptions + GOOS=$(HOSTOS) GOARCH=$(HOSTARCH) $(HOSTGO) build $(GOHOSTFLAGS) -o ./bin/syz-kfuzztest github.com/google/syzkaller/syz-kfuzztest + verifier: descriptions # TODO: switch syz-verifier to use syz-executor. # GOOS=$(HOSTOS) GOARCH=$(HOSTARCH) $(HOSTGO) build $(GOHOSTFLAGS) -o ./bin/syz-verifier github.com/google/syzkaller/syz-verifier diff --git a/pkg/kfuzztest-executor/executor.go b/pkg/kfuzztest-executor/executor.go new file mode 100644 index 000000000..4637ba553 --- /dev/null +++ b/pkg/kfuzztest-executor/executor.go @@ -0,0 +1,123 @@ +// Copyright 2025 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +//go:build linux + +// Package kfuzztestexecutor implements local execution (i.e., without the +// C++ executor program) for KFuzzTest targets. +package kfuzztestexecutor + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/google/syzkaller/pkg/flatrpc" + "github.com/google/syzkaller/pkg/fuzzer/queue" + "github.com/google/syzkaller/pkg/kcov" + "github.com/google/syzkaller/pkg/kfuzztest" + "github.com/google/syzkaller/pkg/log" + "github.com/google/syzkaller/pkg/osutil" + "github.com/google/syzkaller/prog" +) + +// KFuzzTestExecutor is an executor that upon receiving a request, will invoke +// a KFuzzTest target. +type KFuzzTestExecutor struct { + ctx context.Context + jobChan chan *queue.Request + // Cooldown between execution requests. + cooldown time.Duration + wg sync.WaitGroup +} + +// Implements the queue.Executor interface. +func (kfe *KFuzzTestExecutor) Submit(req *queue.Request) { + kfe.jobChan <- req +} + +func (kfe *KFuzzTestExecutor) Shutdown() { + close(kfe.jobChan) + kfe.wg.Wait() +} + +func NewKFuzzTestExecutor(ctx context.Context, numWorkers int, cooldown uint32) *KFuzzTestExecutor { + jobChan := make(chan *queue.Request) + + kfe := &KFuzzTestExecutor{ + ctx: ctx, + jobChan: jobChan, + cooldown: time.Duration(cooldown) * time.Second, + } + + kfe.wg.Add(numWorkers) + for i := range numWorkers { + go kfe.workerLoop(i) + } + return kfe +} + +func (kfe *KFuzzTestExecutor) workerLoop(tid int) { + defer kfe.wg.Done() + kcovSt, err := kcov.EnableTracingForCurrentGoroutine() + if err != nil { + log.Logf(1, "failed to enable kcov for thread_%d", tid) + return + } + defer kcovSt.DisableTracing() + + for req := range kfe.jobChan { + if req.Prog == nil { + log.Logf(1, "thread_%d: exec request had nil program", tid) + } + + info := new(flatrpc.ProgInfo) + for _, call := range req.Prog.Calls { + callInfo := new(flatrpc.CallInfo) + + // Trace each individual call, collecting the covered PCs. + coverage, err := execKFuzzTestCallLocal(kcovSt, call) + if err != nil { + // Set this call info as a failure. -1 is a placeholder. + callInfo.Error = -1 + callInfo.Flags |= flatrpc.CallFlagBlocked + } else { + for _, pc := range coverage { + callInfo.Signal = append(callInfo.Signal, uint64(pc)) + callInfo.Cover = append(callInfo.Cover, uint64(pc)) + } + callInfo.Flags |= flatrpc.CallFlagExecuted + } + + info.Calls = append(info.Calls, callInfo) + } + + req.Done(&queue.Result{Info: info, Executor: queue.ExecutorID{VM: 0, Proc: tid}}) + + if kfe.cooldown != 0 { + time.Sleep(kfe.cooldown) + } + } + log.Logf(0, "thread_%d exiting", tid) +} + +func execKFuzzTestCallLocal(st *kcov.KCOVState, call *prog.Call) ([]uintptr, error) { + if !call.Meta.Attrs.KFuzzTest { + return []uintptr{}, fmt.Errorf("call is not a KFuzzTest call") + } + testName, isKFuzzTest := kfuzztest.GetTestName(call.Meta) + if !isKFuzzTest { + return []uintptr{}, fmt.Errorf("tried to execute a syscall that wasn't syz_kfuzztest_run") + } + + dataArg, ok := call.Args[1].(*prog.PointerArg) + if !ok { + return []uintptr{}, fmt.Errorf("second arg for syz_kfuzztest_run should be a pointer") + } + finalBlob := prog.MarshallKFuzztestArg(dataArg.Res) + inputPath := kfuzztest.GetInputFilepath(testName) + + res := st.Trace(func() error { return osutil.WriteFile(inputPath, finalBlob) }) + return res.Coverage, res.Result +} diff --git a/pkg/kfuzztest-manager/manager.go b/pkg/kfuzztest-manager/manager.go new file mode 100644 index 000000000..f728230cc --- /dev/null +++ b/pkg/kfuzztest-manager/manager.go @@ -0,0 +1,201 @@ +// Copyright 2025 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. +package kfuzztestmanager + +import ( + "context" + "fmt" + "math/rand" + "os" + "slices" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/google/syzkaller/pkg/corpus" + "github.com/google/syzkaller/pkg/fuzzer" + "github.com/google/syzkaller/pkg/fuzzer/queue" + "github.com/google/syzkaller/pkg/kfuzztest" + executor "github.com/google/syzkaller/pkg/kfuzztest-executor" + "github.com/google/syzkaller/pkg/log" + "github.com/google/syzkaller/pkg/mgrconfig" + "github.com/google/syzkaller/pkg/stat" + "github.com/google/syzkaller/prog" + "github.com/google/syzkaller/sys/targets" +) + +type kFuzzTestManager struct { + fuzzer atomic.Pointer[fuzzer.Fuzzer] + source queue.Source + target *prog.Target + config Config +} + +type Config struct { + VmlinuxPath string + Cooldown uint32 + DisplayInterval uint32 + NumThreads int + EnabledTargets []string +} + +func NewKFuzzTestManager(ctx context.Context, cfg Config) (*kFuzzTestManager, error) { + var mgr kFuzzTestManager + + target, err := prog.GetTarget(targets.Linux, targets.AMD64) + if err != nil { + return nil, err + } + + log.Logf(0, "extracting KFuzzTest targets from \"%s\" (this will take a few seconds)", cfg.VmlinuxPath) + calls, err := kfuzztest.ActivateKFuzzTargets(target, cfg.VmlinuxPath) + if err != nil { + return nil, err + } + + enabledCalls := make(map[*prog.Syscall]bool) + for _, call := range calls { + enabledCalls[call] = true + } + + // Disable all calls that weren't explicitly enabled. + if len(cfg.EnabledTargets) > 0 { + enabledMap := make(map[string]bool) + for _, enabled := range cfg.EnabledTargets { + enabledMap[enabled] = true + } + for syscall := range enabledCalls { + testName, isSyzKFuzzTest := kfuzztest.GetTestName(syscall) + _, isEnabled := enabledMap[testName] + if isSyzKFuzzTest && syscall.Attrs.KFuzzTest && isEnabled { + enabledMap[testName] = true + } else { + delete(enabledCalls, syscall) + } + } + } + + dispDiscoveredTargets := func() string { + var builder strings.Builder + totalEnabled := 0 + + builder.WriteString("enabled KFuzzTest targets: [\n") + for targ, enabled := range enabledCalls { + if enabled { + fmt.Fprintf(&builder, "\t%s,\n", targ.Name) + totalEnabled++ + } + } + fmt.Fprintf(&builder, "]\ntotal = %d\n", totalEnabled) + return builder.String() + } + log.Logf(0, "%s", dispDiscoveredTargets()) + + corpus := corpus.NewCorpus(ctx) + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + fuzzerObj := fuzzer.NewFuzzer(ctx, &fuzzer.Config{ + Corpus: corpus, + Snapshot: false, + Coverage: true, + FaultInjection: false, + Comparisons: false, + Collide: false, + EnabledCalls: enabledCalls, + NoMutateCalls: make(map[int]bool), + FetchRawCover: false, + Logf: func(level int, msg string, args ...any) { + if level != 0 { + return + } + log.Logf(level, msg, args...) + }, + NewInputFilter: func(call string) bool { + // Don't filter anything. + return true + }, + }, rnd, target) + + // TODO: Sufficient for startup, but not ideal that we are passing a + // manager config here. Would require changes to pkg/fuzzer if we wanted to + // avoid the dependency. + execOpts := fuzzer.DefaultExecOpts(&mgrconfig.Config{Sandbox: "none"}, 0, false) + + mgr.target = target + mgr.fuzzer.Store(fuzzerObj) + mgr.source = queue.DefaultOpts(fuzzerObj, execOpts) + mgr.config = cfg + + return &mgr, nil +} + +func (mgr *kFuzzTestManager) Run(ctx context.Context) { + var wg sync.WaitGroup + + // Launches the executor threads. + executor := executor.NewKFuzzTestExecutor(ctx, mgr.config.NumThreads, mgr.config.Cooldown) + + // Display logs periodically. + display := func() { + defer wg.Done() + mgr.displayLoop(ctx) + } + + wg.Add(1) + go display() + +FuzzLoop: + for { + select { + case <-ctx.Done(): + break FuzzLoop + default: + } + + req := mgr.source.Next() + if req == nil { + continue + } + + executor.Submit(req) + } + + log.Log(0, "fuzzing finished, shutting down executor") + executor.Shutdown() + wg.Wait() + + const filepath string = "pcs.out" + log.Logf(0, "writing PCs out to \"%s\"", filepath) + if err := mgr.writePCs(filepath); err != nil { + log.Logf(0, "failed to write PCs: %v", err) + } + + log.Log(0, "KFuzzTest manager exited") +} + +func (mgr *kFuzzTestManager) writePCs(filepath string) error { + pcs := mgr.fuzzer.Load().Config.Corpus.Cover() + slices.Sort(pcs) + var builder strings.Builder + for _, pc := range pcs { + fmt.Fprintf(&builder, "0x%x\n", pc) + } + return os.WriteFile(filepath, []byte(builder.String()), 0644) +} + +func (mgr *kFuzzTestManager) displayLoop(ctx context.Context) { + ticker := time.NewTicker(time.Duration(mgr.config.DisplayInterval) * time.Second) + defer ticker.Stop() + for { + var buf strings.Builder + select { + case <-ctx.Done(): + return + case <-ticker.C: + for _, stat := range stat.Collect(stat.Console) { + fmt.Fprintf(&buf, "%v=%v ", stat.Name, stat.Value) + } + log.Log(0, buf.String()) + } + } +} diff --git a/syz-kfuzztest/main.go b/syz-kfuzztest/main.go new file mode 100644 index 000000000..e46ecc257 --- /dev/null +++ b/syz-kfuzztest/main.go @@ -0,0 +1,61 @@ +// Copyright 2025 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. +package main + +import ( + "context" + "flag" + "fmt" + "os" + + manager "github.com/google/syzkaller/pkg/kfuzztest-manager" + "github.com/google/syzkaller/pkg/osutil" +) + +var ( + flagVmlinux = flag.String("vmlinux", "vmlinux", "path to vmlinux binary") + flagCooldown = flag.Int("cooldown", 0, "cooldown between KFuzzTest target invocations in seconds") + flagThreads = flag.Int("threads", 2, "number of threads") + flagDisplayInterval = flag.Int("display", 5, "number of seconds between console outputs") +) + +func main() { + usage := func() { + w := flag.CommandLine.Output() + fmt.Fprintf(w, "usage: %s [flags] [enabled targets]\n\n", os.Args[0]) + fmt.Fprintln(w, `Args: + One fuzz test name per enabled fuzz test arg. If empty, defaults to + all discovered targets.`) + fmt.Fprintln(w, `Example: + ./syz-kfuzztest -vmlinux ~/kernel/vmlinux fuzz_target_0 fuzz_target_1`) + fmt.Fprintln(w, "Flags:") + flag.PrintDefaults() + } + flag.Usage = usage + flag.Parse() + enabledTargets := flag.Args() + + cfg := manager.Config{ + VmlinuxPath: *flagVmlinux, + Cooldown: uint32(*flagCooldown), + DisplayInterval: uint32(*flagDisplayInterval), + NumThreads: *flagThreads, + EnabledTargets: enabledTargets, + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + shutdownChan := make(chan struct{}) + osutil.HandleInterrupts(shutdownChan) + go func() { + <-shutdownChan + cancel() + }() + + mgr, err := manager.NewKFuzzTestManager(ctx, cfg) + if err != nil { + panic(err) + } + mgr.Run(ctx) +} -- cgit mrf-deployment