aboutsummaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorEthan Graham <ethangraham@google.com>2025-09-15 13:13:20 +0000
committerAleksandr Nogikh <nogikh@google.com>2025-09-22 09:11:54 +0000
commit288cfa16e79d64f1dbaafe91d4aee223fe0dd494 (patch)
tree8376d303c9a6b266e0df5f6f643d0ada2673445b /pkg
parent6e1112d5c5188a4ad2911642c49a7f0b335a0cb7 (diff)
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 <ethangraham@google.com>
Diffstat (limited to 'pkg')
-rw-r--r--pkg/kfuzztest-executor/executor.go123
-rw-r--r--pkg/kfuzztest-manager/manager.go201
2 files changed, 324 insertions, 0 deletions
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())
+ }
+ }
+}