diff --git a/Makefile b/Makefile index bf7bca8046a8..fc4c95332ebb 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/docs/kfuzztest.md b/docs/kfuzztest.md new file mode 100644 index 000000000000..7a1cda534e72 --- /dev/null +++ b/docs/kfuzztest.md @@ -0,0 +1,149 @@ +# KFuzzTest Integration With syzkaller + +KFuzzTest, introduced initially in [this RFC](https://lore.kernel.org/all/20250813133812.926145-1-ethan.w.s.graham@gmail.com/) +is a framework for exposing internal kernel functions to a userspace fuzzing +engine like syzkaller. As the kernel docs put it: + +> The Kernel Fuzz Testing Framework (KFuzzTest) is a framework designed to +> expose internal kernel functions to a userspace fuzzing engine. +> +> It is intended for testing stateless or low-state functions that are difficult +> to reach from the system call interface, such as routines involved in file +> format parsing or complex data transformations. This provides a method for +> in-situ fuzzing of kernel code without requiring that it be built as a +> separate userspace library or that its dependencies be stubbed out. + +This document introduces how syzkaller integrates with KFuzzTest. + +## Getting Started + +Firstly, ensure that the KFuzzTest patch series has been applied to your Linux +tree. + +As of the 22nd of August 2025, the most up-to-date version can be found in +[this Linux Kernel RFC](https://lore.kernel.org/all/20250813133812.926145-1-ethan.w.s.graham@gmail.com/). + +Once this is done, KFuzzTest targets can be defined on arbitrary kernel +functions using the `FUZZ_TEST` macro as described in the kernel docs in +`Documentation/dev-tools/kfuzztest.rst`. + +### Configuration Options + +Ensure that the following KConfig options are enabled for your kernel image: + +- `CONFIG_DEBUG_FS` (used as a communication interface by KFuzzTest). +- `CONFIG_DEBUG_KERNEL`. +- `CONFIG_KFUZZTEST`. + +It is also **highly** recommended to enable the following KConfig options for +more effective fuzzing. + +- `CONFIG_KASAN` (catch memory bugs such as out-of-bounds-accesses). +- `CONFIG_KCOV` (to enable coverage guided fuzzing). + +## Fuzzing KFuzzTest Targets + +Syzkaller implements three ways to fuzz KFuzzTest targets: + +1. `syz-manager` integration with static targets +2. `syz-manager` with dynamic targets +3. `syz-kfuzztest`: a standalone tool that runs inside a VM, discovers KFuzzTest + targets dynamically, and fuzzes them. + +### 1. `syz-manager` with static targets + +Configuration for this method is identical to `syz-manager`, and is designed to +make it easy to integrate KFuzzTest fuzzing into existing continuous fuzzing +deployments. + +One must first write a syzlang description for the KFuzzTest target(s) of +interest, for example in `/sys/linux/my_kfuzztest_target.txt`. Each target +should have the following format: + +``` +some_buffer { + buf ptr[inout, array[int8]] + buflen len[buf, int64] +} + +kfuzztest_underflow_on_buffer(name ptr[in, string["test_underflow_on_buffer"]], data ptr[in, some_buffer], len bytesize[data]) (kfuzz_test) +``` + +Where: + +- The first argument should be a string pointer to the name of the fuzz target, + i.e,. the name of its `debugfs` input directory in the kernel. +- The second should be a pointer to a struct of the type that the fuzz + target accepts as input. +- The third should be the size in bytes of the input argument. +- The call is annotated with attribute `kfuzz_test`. + +For more information on writing syzkaller descriptions attributes, consult the +[syscall description](syscall_descriptions.md) and [syscall description syntax](syscall_descriptions_syntax.md) +documentation files. + +To facilitate the tedious task of writing `syz_kfuzztest_run` descriptions, a +tool (`tools/kfuzztest-gen`) is provided to automatically generate these from a +`vmlinux` binary. One can run the tool and paste the output into a syzlang file. + +```sh +go run ./tools/kfuzztest-gen --vmlinux=path/to/vmlinux +``` + +After writing these descriptions to a file under the `/sys/linux/` directory +(for example, `/sys/linux/my_fuzz_targets.txt`), they need to be compiled with +`make descriptions`. + +Finally, the targets can be enabled in `syz-manager` config file in the +`enable_syscalls` field, e.g. + +```json +{ + "enable_syscalls": [ "syz_kfuzztest_run$test_underflow_on_buffer" ] +} +``` + +### 2. `syz-manager` with dynamic discovery + +This feature greatly reduces the amount of setup needed for fuzzing KFuzzTest +targets, by discovering them all dynamically at launch. + +This approach is considered less stable than the previous as it involves +generating descriptions for KFuzzTest targets without human input and then +immediately fuzzing them. It does, however, better reflect our intentions for +KFuzzTest: continuously fuzzing the kernel with a dynamically changing set of +targets with little intervention from syzkaller maintainers. + +To enable this feature, configure the experimental `enable_kfuzztest` option in +the manager configuration, which enables all discovered KFuzzTest targets by +default. + +```json +{ + "enable_kfuzztest": true +} +``` + +You must also enable pseudo-syscall `syz_kfuzztest_run`, like so: + +```json +{ + "enable_syscalls": [ + "syz_kfuzztest_run" + ], +} +``` + +**IMPORTANT:** for dynamic discovery to work, it is essential for the kernel +image pointed to by the manager configuration is built with `CONFIG_DWARF4` or +`CONFIG_DWARF5` enabled, as dynamic target discovery depends on these symbols +being emitted. + +### 3. `syz-kfuzztest`, an in-VM standalone tool + +In contrast with `syz-manager`, `syz-kfuzztest` is designed to perform coverage +guided fuzzing from within a VM directly rather than orchestrating a fleet of +VMs. It is primarily targetted at development-time fuzzing, rather than longterm +continuous fuzzing. + +For more information, consult [the `syz-kfuzztest` documentation](syz-kfuzztest.md). diff --git a/docs/syscall_descriptions_syntax.md b/docs/syscall_descriptions_syntax.md index 3abb8fd1922a..366903d2c19a 100644 --- a/docs/syscall_descriptions_syntax.md +++ b/docs/syscall_descriptions_syntax.md @@ -109,6 +109,7 @@ Call attributes are: "fsck": the content of the compressed buffer argument for this syscall is a file system and the string argument is a fsck-like command that will be called to verify the filesystem "remote_cover": wait longer to collect remote coverage for this call. +"kfuzz_test": the call is a kfuzztest target ``` ## Ints diff --git a/docs/syz-kfuzztest.md b/docs/syz-kfuzztest.md new file mode 100644 index 000000000000..4df0248c5065 --- /dev/null +++ b/docs/syz-kfuzztest.md @@ -0,0 +1,106 @@ +# `syz-kfuzztest` + +`syz-kfuzztest` is a standalone tool for fuzzing KFuzzTest targets from within +the kernel being fuzzed (e.g., a VM). + +It is intended to be used for development-time fuzzing rather than continuous +fuzzing like `syz-manager`. + +For more information on KFuzzTest, consult the [dedicated readme](kfuzztest.md) +or the Kernel documentation. + +## Usage (in-VM fuzzing) + +### Getting the Kernel Ready + +It is important that the target Kernel image has the correct KConfig options +enabled. Namely + +- `CONFIG_KFUZZTEST` +- `CONFIG_DEBUG_FS` +- `CONFIG_DEBUG_KERNEL` +- `CONFIG_KCOV` +- `CONFIG_DWARF4` or `CONFIG_DWARF5` +- `CONFIG_KASAN` _(optional, choose your favorite sanitizers for a better shot + at finding bugs!)_ + +Furthermore, as you will need to connect to the VM being tested through SSH and +launch `syz-kfuzztest` _(a Go binary with LIBC dependencies)_, it is recommended +to create an image for the kernel being fuzzed (e.g., a Debian Bullseye image). +Detailed instructions on how to do this can be found in +[this setup guide](linux/setup_ubuntu-host_qemu-vm_x86-64-kernel.md). + +### Building and Launching the Binary + +The `syz-kfuzztest` binary is built with `make syz-kfuzztest`, and is intended +to run on the Kernel fuzzed. The common case for this is within a VM _(after +all, the tool is trying to make the Kernel crash)_. + +Then, ensure that the `syz-kfuzztest` binary and `vmlinux` image are copied +over into the VM. E.g., + +```sh +scp $KERNEL/vmlinux root@my-vm:~/syz-kfuzztest/vmlinux +scp $SYZKALLER/bin/syz-kfuzztest root@lmy-vm:~/syz-kfuzztest/syz-kfuzztest +``` + +Then launched like this: + +``` +usage: ./bin/syz-kfuzztest [flags] [enabled targets] + +Args: + One fuzz test name per enabled fuzz test arg. If empty, defaults to + all discovered targets. +Example: + ./syz-kfuzztest -vmlinux ~/kernel/vmlinux fuzz_target_0 fuzz_target_1 +Flags: + -display int + Number of seconds between console outputs (default 5) + -threads int + Number of threads (default 2) + -timeout int + Timeout between program executions in seconds (default 0) + -vmlinux string + Path to vmlinux binary (default "vmlinux") + -vv int + verbosity +``` + +The enabled targets, which are listed after the flag arguments, are the names of +the enabled fuzz targets. For example given some KFuzzTest targets: + +```c +FUZZ_TEST(kfuzztest_target_1, struct input_arg_type) +{ + /* ... */ +} + +FUZZ_TEST(kfuzztest_target_2, struct input_arg_type) +{ + /* ... */ +} + +``` + +Can be fuzzed with: + +```bash +./syz-kfuzztest -vmlinux path/to/vmlinux -threads 4 kfuzztest_target_1 kfuzztest_target_2 +``` + +If the enabled targets list is left empty, `syz-kfuzztest` will fuzz all +discovered targets in the kernel. + +On exit, `syz-kfuzztest` will write the collected program counters (which are +collected with KCOV) into a file called `pcs.out`. These program counters can +be fed into [`syz-cover`](../tools/syz-cover/syz-cover.go) to generate an HTML +visualization of the lines that were covered during fuzzing. It is recommended +to do this on the host machine rather than the VM. + +For example: + +```sh +scp root@my-vm:~/syz-kfuzztest/pcs.out . +go run tools/syz-cover -config my.cfg pcs.out # May require the -force flag. +``` diff --git a/executor/common_linux.h b/executor/common_linux.h index 31ce14dc3d31..5d477a16a812 100644 --- a/executor/common_linux.h +++ b/executor/common_linux.h @@ -5852,3 +5852,57 @@ static long syz_pidfd_open(volatile long pid, volatile long flags) } #endif + +#if SYZ_EXECUTOR || __NR_syz_kfuzztest_run + +#include +#include +#include +#include +#include +#include +#include +#include + +static long syz_kfuzztest_run(volatile long test_name_ptr, volatile long input_data, + volatile long input_data_size, volatile long buffer) +{ + const char* test_name = (const char*)test_name_ptr; + if (!test_name) { + debug("syz_kfuzztest_run: test name was NULL\n"); + return -1; + } + if (!buffer) { + debug("syz_kfuzztest_run: buffer was NULL\n"); + return -1; + } + + char buf[256]; + int ret = snprintf(buf, sizeof(buf), "/sys/kernel/debug/kfuzztest/%s/input", test_name); + if (ret < 0 || (unsigned long)ret >= sizeof(buf)) { + debug("syz_kfuzztest_run: constructed path is too long or snprintf failed\n"); + return -1; + } + + int fd = openat(AT_FDCWD, buf, O_WRONLY, 0); + if (fd < 0) { + debug("syz_kfuzztest_run: failed to open %s\n", buf); + return -1; + } + + ssize_t bytes_written = write(fd, (void*)buffer, (size_t)input_data_size); + if (bytes_written != input_data_size) { + debug("syz_kfuzztest_run: failed to write to %s, reason: %s\n", buf, strerror(errno)); + close(fd); + return -1; + } + + if (close(fd) != 0) { + debug("syz_kfuzztest_run: failed to close file\n"); + return -1; + } + + return 0; +} + +#endif diff --git a/pkg/corpus/corpus.go b/pkg/corpus/corpus.go index 83c2f65206e5..594521fcc784 100644 --- a/pkg/corpus/corpus.go +++ b/pkg/corpus/corpus.go @@ -260,3 +260,7 @@ func (corpus *Corpus) ProgsPerArea() map[string]int { } return ret } + +func (corpus *Corpus) Cover() []uint64 { + return corpus.cover.Serialize() +} diff --git a/pkg/fuzzer/fuzzer.go b/pkg/fuzzer/fuzzer.go index 0c0119e719b3..fdfe955182d7 100644 --- a/pkg/fuzzer/fuzzer.go +++ b/pkg/fuzzer/fuzzer.go @@ -72,6 +72,13 @@ func NewFuzzer(ctx context.Context, cfg *Config, rnd *rand.Rand, return f } +func (fuzzer *Fuzzer) RecommendedCalls() int { + if fuzzer.Config.ModeKFuzzTest { + return prog.RecommendedCallsKFuzzTest + } + return prog.RecommendedCalls +} + type execQueues struct { triageCandidateQueue *queue.DynamicOrderer candidateQueue *queue.PlainQueue @@ -214,6 +221,7 @@ type Config struct { FetchRawCover bool NewInputFilter func(call string) bool PatchTest bool + ModeKFuzzTest bool } func (fuzzer *Fuzzer) triageProgCall(p *prog.Prog, info *flatrpc.CallInfo, call int, triage *map[int]*triageCall) { diff --git a/pkg/fuzzer/job.go b/pkg/fuzzer/job.go index 7f1e47bf6141..bbac544f62ae 100644 --- a/pkg/fuzzer/job.go +++ b/pkg/fuzzer/job.go @@ -43,7 +43,7 @@ func (ji *JobInfo) ID() string { func genProgRequest(fuzzer *Fuzzer, rnd *rand.Rand) *queue.Request { p := fuzzer.target.Generate(rnd, - prog.RecommendedCalls, + fuzzer.RecommendedCalls(), fuzzer.ChoiceTable()) return &queue.Request{ Prog: p, diff --git a/pkg/kcov/cdefs.go b/pkg/kcov/cdefs.go new file mode 100644 index 000000000000..1272360b9cd8 --- /dev/null +++ b/pkg/kcov/cdefs.go @@ -0,0 +1,45 @@ +// 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 kcov + +// This file defines values required for KCOV ioctl calls. More information on +// the values and their semantics can be found in the kernel documentation under +// Documentation/dev-tools/kcov.rst, or at docs.kernel.org/dev-tools/kcov.html. + +import "unsafe" + +const ( + sizeofUintPtr = int(unsafe.Sizeof((*int)(nil))) + + iocNrBits = 8 + iocTypeBits = 8 + iocSizeBits = 14 + iocDirBits = 2 + + iocNrShift = 0 + iocTypeshift = iocNrShift + iocNrBits + iocSizeShift = iocTypeshift + iocTypeBits + iocDirShift = iocSizeShift + iocSizeBits + + iocNone = 0 + iocWrite = 1 + iocRead = 2 + + // kcovInitTrace initializes KCOV tracing. + // #define kcovInitTrace _IOR('c', 1, unsigned long) + kcovInitTrace uintptr = (iocRead << iocDirShift) | + (unsafe.Sizeof(uint64(0)) << iocSizeShift) | ('c' << iocTypeshift) | (1 << iocNrShift) // 0x80086301. + + // kcovEnable enables kcov for the current thread. + // #define kcovEnable _IO('c', 100) + kcovEnable uintptr = (iocNone << iocDirShift) | + (0 << iocSizeShift) | ('c' << iocTypeshift) | (100 << iocNrShift) // 0x6364. + + // kcovDisable disables kcov for the current thread. + // #define kcovDisable _IO('c', 101) + kcovDisable uintptr = (iocNone << iocDirShift) | + (0 << iocSizeShift) | ('c' << iocTypeshift) | (101 << iocNrShift) // 0x6365. + + kcovTracePC = 0 + kcovTraceCMP = 1 +) diff --git a/pkg/kcov/kcov.go b/pkg/kcov/kcov.go new file mode 100644 index 000000000000..0400a32ff05b --- /dev/null +++ b/pkg/kcov/kcov.go @@ -0,0 +1,115 @@ +// 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 kcov provides Go native code for collecting kernel coverage (KCOV) +// information. +package kcov + +import ( + "os" + "runtime" + "sync/atomic" + "unsafe" + + "golang.org/x/sys/unix" +) + +const ( + kcovPath = "/sys/kernel/debug/kcov" + // This is the same value used by the linux executor, see executor_linux.h. + kcovCoverSize = 512 << 10 +) + +// Holds resources for a single traced thread. +type KCOVState struct { + file *os.File + cover []byte +} + +type KCOVTraceResult struct { + Result error // Result of the call. + Coverage []uintptr // Collected program counters. +} + +// Trace invokes `f` and returns a KCOVTraceResult. +func (st *KCOVState) Trace(f func() error) KCOVTraceResult { + // First 8 bytes holds the number of collected PCs since last poll. + countPtr := (*uintptr)(unsafe.Pointer(&st.cover[0])) + // Reset coverage for this run. + atomic.StoreUintptr(countPtr, 0) + // Trigger call. + err := f() + // Load the number of PCs that were hit during trigger. + n := atomic.LoadUintptr(countPtr) + + pcDataPtr := (*uintptr)(unsafe.Pointer(&st.cover[sizeofUintPtr])) + pcs := unsafe.Slice(pcDataPtr, n) + pcsCopy := make([]uintptr, n) + copy(pcsCopy, pcs) + return KCOVTraceResult{Result: err, Coverage: pcsCopy} +} + +// EnableTracingForCurrentGoroutine prepares the current goroutine for kcov tracing. +// It must be paired with a call to DisableTracing. +func EnableTracingForCurrentGoroutine() (st *KCOVState, err error) { + st = &KCOVState{} + defer func() { + if err != nil { + // The original error is more important, so we ignore any potential + // errors that result from cleaning up. + _ = st.DisableTracing() + } + }() + + // KCOV is per-thread, so lock goroutine to its current OS thread. + runtime.LockOSThread() + + file, err := os.OpenFile(kcovPath, os.O_RDWR, 0) + if err != nil { + return nil, err + } + st.file = file + + // Setup trace mode and size. + if err := unix.IoctlSetInt(int(st.file.Fd()), uint(kcovInitTrace), kcovCoverSize); err != nil { + return nil, err + } + + // Mmap buffer shared between kernel- and user-space. For more information, + // see the Linux KCOV documentation: https://docs.kernel.org/dev-tools/kcov.html. + st.cover, err = unix.Mmap( + int(st.file.Fd()), + 0, // Offset. + kcovCoverSize*sizeofUintPtr, + unix.PROT_READ|unix.PROT_WRITE, + unix.MAP_SHARED, + ) + if err != nil { + return nil, err + } + + // Enable coverage collection on the current thread. + if err := unix.IoctlSetInt(int(st.file.Fd()), uint(kcovEnable), kcovTracePC); err != nil { + return nil, err + } + return st, nil +} + +// DisableTracing disables KCOV tracing for the current Go routine. On failure, +// it returns the first error that occurred during cleanup. +func (st *KCOVState) DisableTracing() error { + var firstErr error + if err := unix.IoctlSetInt(int(st.file.Fd()), uint(kcovDisable), kcovTracePC); err != nil { + firstErr = err + } + if err := unix.Munmap(st.cover); err != nil && firstErr == nil { + firstErr = err + } + if err := st.file.Close(); err != nil && firstErr == nil { + firstErr = err + } + runtime.UnlockOSThread() + return firstErr +} diff --git a/pkg/kfuzztest-executor/executor.go b/pkg/kfuzztest-executor/executor.go new file mode 100644 index 000000000000..4637ba553701 --- /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 000000000000..f728230ccc1a --- /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/pkg/kfuzztest/builder.go b/pkg/kfuzztest/builder.go new file mode 100644 index 000000000000..7262fd776437 --- /dev/null +++ b/pkg/kfuzztest/builder.go @@ -0,0 +1,255 @@ +// 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 kfuzztest + +import ( + "debug/dwarf" + "fmt" + "strings" + + "github.com/google/syzkaller/pkg/ast" +) + +type Builder struct { + funcs []SyzFunc + structs []SyzStruct + constraints []SyzConstraint + annotations []SyzAnnotation +} + +func NewBuilder( + funcs []SyzFunc, + structs []SyzStruct, + constraints []SyzConstraint, + annotations []SyzAnnotation, +) *Builder { + return &Builder{funcs, structs, constraints, annotations} +} + +func (b *Builder) AddStruct(s SyzStruct) { + b.structs = append(b.structs, s) +} + +func (b *Builder) AddFunc(f SyzFunc) { + b.funcs = append(b.funcs, f) +} + +func (b *Builder) EmitSyzlangDescription() (string, error) { + constraintMap := make(map[string]map[string]SyzConstraint) + for _, constraint := range b.constraints { + if _, contains := constraintMap[constraint.InputType]; !contains { + constraintMap[constraint.InputType] = make(map[string]SyzConstraint) + } + constraintMap[constraint.InputType][constraint.FieldName] = constraint + } + annotationMap := make(map[string]map[string]SyzAnnotation) + for _, annotation := range b.annotations { + if _, contains := annotationMap[annotation.InputType]; !contains { + annotationMap[annotation.InputType] = make(map[string]SyzAnnotation) + } + annotationMap[annotation.InputType][annotation.FieldName] = annotation + } + + var descBuilder strings.Builder + descBuilder.WriteString("# This description was automatically generated with tools/kfuzztest-gen\n") + for _, s := range b.structs { + structDesc, err := syzStructToSyzlang(s, constraintMap, annotationMap) + if err != nil { + return "", err + } + descBuilder.WriteString(structDesc) + descBuilder.WriteString("\n\n") + } + + for i, fn := range b.funcs { + descBuilder.WriteString(syzFuncToSyzlang(fn)) + if i < len(b.funcs)-1 { + descBuilder.WriteString("\n") + } + } + + // Format the output syzlang descriptions for consistency. + var astError error + eh := func(pos ast.Pos, msg string) { + astError = fmt.Errorf("ast failure: %v: %v", pos, msg) + } + descAst := ast.Parse([]byte(descBuilder.String()), "", eh) + if astError != nil { + return "", astError + } + if descAst == nil { + return "", fmt.Errorf("failed to format generated syzkaller description - is it well-formed?") + } + return string(ast.Format(descAst)), nil +} + +func syzStructToSyzlang(s SyzStruct, constraintMap map[string]map[string]SyzConstraint, + annotationMap map[string]map[string]SyzAnnotation) (string, error) { + var builder strings.Builder + + fmt.Fprintf(&builder, "%s {\n", s.Name) + structAnnotations := annotationMap["struct "+s.Name] + structConstraints := constraintMap["struct "+s.Name] + for _, field := range s.Fields { + line, err := syzFieldToSyzLang(field, structConstraints, structAnnotations) + if err != nil { + return "", err + } + fmt.Fprintf(&builder, "\t%s\n", line) + } + fmt.Fprint(&builder, "}") + return builder.String(), nil +} + +func syzFieldToSyzLang(field SyzField, constraintMap map[string]SyzConstraint, + annotationMap map[string]SyzAnnotation) (string, error) { + constraint, hasConstraint := constraintMap[field.Name] + annotation, hasAnnotation := annotationMap[field.Name] + + var typeDesc string + var err error + if hasAnnotation { + // Annotations override the existing type definitions. + typeDesc, err = processAnnotation(field, annotation) + } else { + typeDesc, err = dwarfToSyzlangType(field.dwarfType) + } + if err != nil { + return "", err + } + + // Process constraints only if unannotated. + // TODO: is there a situation where we would want to process both? + if hasConstraint && !hasAnnotation { + constraint, err := processConstraint(constraint) + if err != nil { + return "", err + } + typeDesc += constraint + } + return fmt.Sprintf("%s %s", field.Name, typeDesc), nil +} + +func processConstraint(c SyzConstraint) (string, error) { + switch c.ConstraintType { + case ExpectEq: + return fmt.Sprintf("[%d]", c.Value1), nil + case ExpectNe: + // syzkaller does not have a built-in way to support an inequality + // constraint AFAIK. + return "", nil + case ExpectLt: + return fmt.Sprintf("[0:%d]", c.Value1-1), nil + case ExpectLe: + return fmt.Sprintf("[0:%d]", c.Value1), nil + case ExpectGt: + return fmt.Sprintf("[%d]", c.Value1+1), nil + case ExpectGe: + return fmt.Sprintf("[%d]", c.Value1), nil + case ExpectInRange: + return fmt.Sprintf("[%d:%d]", c.Value1, c.Value2), nil + default: + fmt.Printf("c = %d\n", c.ConstraintType) + return "", fmt.Errorf("unsupported constraint type") + } +} + +func processAnnotation(field SyzField, annotation SyzAnnotation) (string, error) { + switch annotation.Attribute { + case AttributeLen: + underlyingType, err := dwarfToSyzlangType(field.dwarfType) + if err != nil { + return "", err + } + return fmt.Sprintf("len[%s, %s]", annotation.LinkedFieldName, underlyingType), nil + case AttributeString: + return "ptr[in, string]", nil + case AttributeArray: + pointeeType, isPtr := resolvesToPtr(field.dwarfType) + if !isPtr { + return "", fmt.Errorf("can only annotate pointer fields are arrays") + } + // TODO: discards const qualifier. + typeDesc, err := dwarfToSyzlangType(pointeeType) + if err != nil { + return "", err + } + return fmt.Sprintf("ptr[in, array[%s]]", typeDesc), nil + default: + return "", fmt.Errorf("unsupported attribute type") + } +} + +// Returns true iff `dwarfType` resolved down to a pointer. For example, +// a `const *void` which isn't directly a pointer. +func resolvesToPtr(dwarfType dwarf.Type) (dwarf.Type, bool) { + switch t := dwarfType.(type) { + case *dwarf.QualType: + return resolvesToPtr(t.Type) + case *dwarf.PtrType: + return t.Type, true + } + return nil, false +} + +func syzFuncToSyzlang(s SyzFunc) string { + var builder strings.Builder + typeName := strings.TrimPrefix(s.InputStructName, "struct ") + + fmt.Fprintf(&builder, "syz_kfuzztest_run$%s(", s.Name) + fmt.Fprintf(&builder, "name ptr[in, string[\"%s\"]], ", s.Name) + fmt.Fprintf(&builder, "data ptr[in, %s], ", typeName) + builder.WriteString("len bytesize[data], ") + builder.WriteString("buf ptr[in, array[int8, 65536]]) ") + // TODO:(ethangraham) The only other way I can think of getting this name + // would involve using the "reflect" package and matching against the + // KFuzzTest name, which isn't much better than hardcoding this. + builder.WriteString("(kfuzz_test)") + return builder.String() +} + +// Given a dwarf type, returns a syzlang string representation of this type. +func dwarfToSyzlangType(dwarfType dwarf.Type) (string, error) { + switch t := dwarfType.(type) { + case *dwarf.PtrType: + underlyingType, err := dwarfToSyzlangType(t.Type) + if err != nil { + return "", err + } + return fmt.Sprintf("ptr[in, %s]", underlyingType), nil + case *dwarf.QualType: + if t.Qual == "const" { + return dwarfToSyzlangType(t.Type) + } else { + return "", fmt.Errorf("no support for %s qualifier", t.Qual) + } + case *dwarf.ArrayType: + underlyingType, err := dwarfToSyzlangType(t.Type) + if err != nil { + return "", err + } + // If t.Count == -1 then this is a varlen array as per debug/dwarf + // documentation. + if t.Count == -1 { + return fmt.Sprintf("array[%s]", underlyingType), nil + } else { + return fmt.Sprintf("array[%s, %d]", underlyingType, t.Count), nil + } + case *dwarf.TypedefType: + return dwarfToSyzlangType(t.Type) + case *dwarf.IntType, *dwarf.UintType: + numBits := t.Size() * 8 + return fmt.Sprintf("int%d", numBits), nil + case *dwarf.CharType, *dwarf.UcharType: + return "int8", nil + // `void` isn't a valid type by itself, so we know that it would have + // been wrapped in a pointer, e.g., `void *`. For this reason, we can return + // just interpret it as a byte, i.e., int8. + case *dwarf.VoidType: + return "int8", nil + case *dwarf.StructType: + return strings.TrimPrefix(t.StructName, "struct "), nil + default: + return "", fmt.Errorf("unsupported type %s", dwarfType.String()) + } +} diff --git a/pkg/kfuzztest/description_generation_test.go b/pkg/kfuzztest/description_generation_test.go new file mode 100644 index 000000000000..d68a96b18ff8 --- /dev/null +++ b/pkg/kfuzztest/description_generation_test.go @@ -0,0 +1,103 @@ +// 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 kfuzztest + +import ( + "fmt" + "os" + "path" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/syzkaller/pkg/osutil" + "github.com/google/syzkaller/sys/targets" + "github.com/stretchr/testify/require" +) + +type testData struct { + dir string + desc string +} + +func TestBuildDescriptions(t *testing.T) { + testCases, err := readTestCases("./testdata") + require.NoError(t, err) + + target := targets.Get(targets.Linux, targets.AMD64) + for _, tc := range testCases { + t.Run(tc.dir, func(t *testing.T) { + runTest(t, target, tc) + }) + } +} + +// Tests that the description inferred from a compiled binary matches an +// expected description. +func runTest(t *testing.T, target *targets.Target, tc testData) { + // Compile the C binary containing the metadata. + cmd := flags(tc.dir) + out, err := osutil.RunCmd(time.Hour, "", target.CCompiler, cmd...) + require.NoErrorf(t, err, "Failed to compile: %s", string(out)) + // Cleanup the compiled binary. + defer func() { + out, err := osutil.RunCmd(time.Hour, "", "rm", path.Join(tc.dir, "bin")) + if err != nil { + require.NoErrorf(t, err, "Failed to cleanup: %s", string(out)) + } + }() + + binaryPath := path.Join(tc.dir, "bin") + desc, err := ExtractDescription(binaryPath) + require.NoError(t, err) + + if diffDesc := cmp.Diff(tc.desc, desc); diffDesc != "" { + fmt.Print(diffDesc) + t.Fail() + return + } +} + +func flags(testDir string) []string { + return []string{ + "-g", + "-T", + path.Join(testDir, "..", "linker.ld"), + "-o", + path.Join(testDir, "bin"), + path.Join(testDir, "prog.c"), + } +} + +func readTestCases(dir string) ([]testData, error) { + var testCases []testData + testDirs, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + + for _, subDir := range testDirs { + if !subDir.IsDir() { + continue + } + testData, err := readTestdata(path.Join(dir, subDir.Name())) + if err != nil { + return nil, err + } + testCases = append(testCases, testData) + } + + return testCases, nil +} + +func readTestdata(testDir string) (testData, error) { + content, err := os.ReadFile(path.Join(testDir, "desc.txt")) + if err != nil { + return testData{}, err + } + + return testData{ + dir: testDir, + desc: string(content), + }, nil +} diff --git a/pkg/kfuzztest/extractor.go b/pkg/kfuzztest/extractor.go new file mode 100644 index 000000000000..e13ea46622e2 --- /dev/null +++ b/pkg/kfuzztest/extractor.go @@ -0,0 +1,435 @@ +// 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 kfuzztest + +import ( + "debug/dwarf" + "debug/elf" + "fmt" + "strings" +) + +// Extractor's job is to extract all information relevant to KFuzzTest from a +// VMlinux binary. +type Extractor struct { + // Path to the `vmlinux` being parsed. + vmlinuxPath string + elfFile *elf.File + dwarfData *dwarf.Data + + // We use an index to avoid repeated sequential scans of the whole binary, + // as this is by far the most expensive operation. + symbolsIndexInitialized bool + symbolsIndex map[string]elf.Symbol +} + +func NewExtractor(vmlinuxPath string) (*Extractor, error) { + elfFile, err := elf.Open(vmlinuxPath) + if err != nil { + return nil, err + } + dwarfData, err := elfFile.DWARF() + if err != nil { + elfFile.Close() + return nil, err + } + return &Extractor{vmlinuxPath, elfFile, dwarfData, false, make(map[string]elf.Symbol)}, nil +} + +type ExtractAllResult struct { + VMLinuxPath string + Funcs []SyzFunc + Structs []SyzStruct + Constraints []SyzConstraint + Annotations []SyzAnnotation +} + +func (e *Extractor) ExtractAll() (ExtractAllResult, error) { + funcs, err := e.extractFuncs() + if err != nil { + return ExtractAllResult{}, err + } + structs, err := e.extractStructs(funcs) + if err != nil { + return ExtractAllResult{}, err + } + constraints, err := e.extractDomainConstraints() + if err != nil { + return ExtractAllResult{}, err + } + annotations, err := e.extractAnnotations() + if err != nil { + return ExtractAllResult{}, err + } + + if len(structs) < len(funcs) { + return ExtractAllResult{}, fmt.Errorf("inconsistent KFuzzTest metadata found in vmlinux") + } + if len(funcs) == 0 { + return ExtractAllResult{}, nil + } + + return ExtractAllResult{ + VMLinuxPath: e.vmlinuxPath, + Funcs: funcs, + Structs: structs, + Constraints: constraints, + Annotations: annotations, + }, nil +} + +func (e *Extractor) Close() { + e.elfFile.Close() +} + +func (e *ExtractAllResult) String() string { + var builder strings.Builder + + fmt.Fprint(&builder, "extraction result:\n") + fmt.Fprintf(&builder, "\tVMLinux image: %s\n", e.VMLinuxPath) + fmt.Fprintf(&builder, "\tnum targets: %d\n", len(e.Funcs)) + fmt.Fprintf(&builder, "\tnum struct: %d\n", len(e.Structs)) + fmt.Fprintf(&builder, "\tnum constraints: %d\n", len(e.Constraints)) + fmt.Fprintf(&builder, "\tnum annotations: %d\n", len(e.Annotations)) + + return builder.String() +} + +// Given an address, returns the elf section that this address belongs to in +// the Extractor's elf file. +func (e *Extractor) elfSection(addr uint64) *elf.Section { + for _, section := range e.elfFile.Sections { + if addr >= section.Addr && addr < section.Addr+section.Size { + return section + } + } + return nil +} + +// Reads a string of length at most 128 bytes from the Extractor's elf file. +func (e *Extractor) readElfString(offset uint64) (string, error) { + strSection := e.elfSection(offset) + if strSection == nil { + return "", fmt.Errorf("unable to find section for offset 0x%X", offset) + } + + // 128 bytes is longer than we expect to see in KFuzzTest metadata. + buffer := make([]byte, 128) + _, err := strSection.ReadAt(buffer, int64(offset-strSection.Addr)) + if err != nil { + return "", err + } + + var builder strings.Builder + for _, chr := range buffer { + if chr == 0 { + return builder.String(), nil + } + builder.WriteByte(chr) + } + + return "", fmt.Errorf("could not find null-terminated string with length < 128") +} + +func (e *Extractor) buildSymbolIndex() error { + symbols, err := e.elfFile.Symbols() + if err != nil { + return err + } + for _, sym := range symbols { + e.symbolsIndex[sym.Name] = sym + } + return nil +} + +func (e *Extractor) getSymbol(symbolName string) (elf.Symbol, error) { + if !e.symbolsIndexInitialized { + err := e.buildSymbolIndex() + e.symbolsIndexInitialized = true + if err != nil { + return elf.Symbol{}, err + } + } + + symbol, contains := e.symbolsIndex[symbolName] + if !contains { + return elf.Symbol{}, fmt.Errorf("symbol %s not found in %s", symbolName, e.vmlinuxPath) + } + return symbol, nil +} + +func (e *Extractor) extractFuncs() ([]SyzFunc, error) { + var rawFuncs []*kfuzztestTarget + var err error + + rawFuncs, err = parseKftfObjects[*kfuzztestTarget](e) + if err != nil { + return nil, err + } + + fuzzTargets := make([]SyzFunc, len(rawFuncs)) + for i, raw := range rawFuncs { + name, err := e.readElfString(raw.name) + if err != nil { + return []SyzFunc{}, err + } + argType, err := e.readElfString(raw.argType) + if err != nil { + return []SyzFunc{}, err + } + fuzzTargets[i] = SyzFunc{ + Name: name, + InputStructName: argType, + } + } + + return fuzzTargets, nil +} + +func (e *Extractor) extractDomainConstraints() ([]SyzConstraint, error) { + var rawConstraints []*kfuzztestConstraint + var err error + + rawConstraints, err = parseKftfObjects[*kfuzztestConstraint](e) + if err != nil { + return nil, err + } + + constraints := make([]SyzConstraint, len(rawConstraints)) + for i, raw := range rawConstraints { + typeName, err := e.readElfString(raw.inputType) + if err != nil { + return []SyzConstraint{}, err + } + fieldName, err := e.readElfString(raw.fieldName) + if err != nil { + return []SyzConstraint{}, err + } + + constraints[i] = SyzConstraint{ + InputType: typeName, + FieldName: fieldName, + Value1: raw.value1, + Value2: raw.value2, + ConstraintType: ConstraintType(raw.constraintType), + } + } + + return constraints, nil +} + +func (e *Extractor) extractAnnotations() ([]SyzAnnotation, error) { + var rawAnnotations []*kfuzztestAnnotation + var err error + + rawAnnotations, err = parseKftfObjects[*kfuzztestAnnotation](e) + if err != nil { + return nil, err + } + + annotations := make([]SyzAnnotation, len(rawAnnotations)) + for i, raw := range rawAnnotations { + typeName, err := e.readElfString(raw.inputType) + if err != nil { + return nil, err + } + fieldName, err := e.readElfString(raw.fieldName) + if err != nil { + return nil, err + } + linkedFieldName, err := e.readElfString(raw.linkedFieldName) + if err != nil { + return nil, err + } + + annotations[i] = SyzAnnotation{ + InputType: typeName, + FieldName: fieldName, + LinkedFieldName: linkedFieldName, + Attribute: AnnotationAttribute(raw.annotationAttribute), + } + } + + return annotations, nil +} + +func (e *Extractor) dwarfGetType(entry *dwarf.Entry) (dwarf.Type, error) { + // Case 1: The entry is itself a type definition (e.g., TagStructType, TagBaseType). + // We use its own offset to get the dwarf.Type object. + switch entry.Tag { + case dwarf.TagStructType, dwarf.TagBaseType, dwarf.TagTypedef, dwarf.TagPointerType, dwarf.TagArrayType: + return e.dwarfData.Type(entry.Offset) + } + + // Case 2: The entry refers to a type (e.g., TagMember, TagVariable). + // We use its AttrType field to find the offset of the type definition. + typeOffset, ok := entry.Val(dwarf.AttrType).(dwarf.Offset) + if !ok { + return nil, fmt.Errorf("entry (Tag: %s) has no AttrType field", entry.Tag) + } + + return e.dwarfData.Type(typeOffset) +} + +// extractStructs extracts input structure metadata from discovered KFuzzTest +// targets (funcs). +// Performs a tree-traversal as all struct types need to be defined in the +// resulting description that is emitted by the builder. +func (e *Extractor) extractStructs(funcs []SyzFunc) ([]SyzStruct, error) { + // Set of input map names so that we can skip over entries that aren't + // interesting. + inputStructs := make(map[string]bool) + for _, fn := range funcs { + inputStructs[fn.InputStructName] = true + } + // Unpacks nested types to find an underlying struct type, or return nil + // if nothing is found. For example, when called on `struct myStruct **` + // we return `struct myStruct`. + unpackNested := func(t dwarf.Type) *dwarf.StructType { + for { + switch concreteType := t.(type) { + case *dwarf.StructType: + return concreteType + case *dwarf.PtrType: + t = concreteType.Type + case *dwarf.QualType: + t = concreteType.Type + default: + return nil + } + } + } + + structs := make([]SyzStruct, 0) + + // Perform a DFS on discovered struct types in order to discover nested + // struct types that may be contained within them. + visited := make(map[string]bool) + var visitRecur func(*dwarf.StructType) + visitRecur = func(start *dwarf.StructType) { + newStruct := SyzStruct{dwarfType: start, Name: start.StructName, Fields: make([]SyzField, 0)} + for _, child := range start.Field { + newField := SyzField{Name: child.Name, dwarfType: child.Type} + newStruct.Fields = append(newStruct.Fields, newField) + switch childType := child.Type.(type) { + case *dwarf.StructType: + if _, contains := visited[childType.StructName]; !contains { + visited[childType.StructName] = true + visitRecur(childType) + } + case *dwarf.PtrType, *dwarf.QualType: + // If we hit a pointer or a qualifier, we unpack to see if we + // find a nested struct type so that we can visit it. + maybeStructType := unpackNested(childType) + if maybeStructType != nil { + if _, contains := visited[maybeStructType.StructName]; !contains { + visited[maybeStructType.StructName] = true + visitRecur(maybeStructType) + } + } + default: + continue + } + } + structs = append(structs, newStruct) + } + + dwarfReader := e.dwarfData.Reader() + for { + entry, err := dwarfReader.Next() + if err != nil { + return nil, err + } + // EOF. + if entry == nil { + break + } + if entry.Tag != dwarf.TagStructType { + continue + } + // Skip over unnamed structures. + nameField := entry.AttrField(dwarf.AttrName) + if nameField == nil { + continue + } + name, ok := nameField.Val.(string) + if !ok { + fmt.Printf("unable to get name field\n") + continue + } + // Dwarf file prefixes structures with `struct` so we must prepend + // before lookup. + structName := "struct " + name + // Check whether or not this type is one that we parsed previously + // while traversing the .kftf section of the vmlinux binary, discarding + // if this is not the case. + if _, ok := inputStructs[structName]; !ok { + continue + } + + t, err := e.dwarfGetType(entry) + if err != nil { + return nil, err + } + + switch entryType := t.(type) { + case *dwarf.StructType: + visitRecur(entryType) + default: + // We shouldn't hit this branch if everything before this is + // correct. + panic("Error parsing dwarf - well-formed?") + } + } + + return structs, nil +} + +// Parses a slice of kftf objects contained within a dedicated section. This +// function assumes that all entries are tightly packed, and that each section +// contains only one type of statically-sized entry types. +func parseKftfObjects[T interface { + *P + parsableFromBytes +}, P any](e *Extractor) ([]T, error) { + var typeinfo T + + startSymbol, err := e.getSymbol(typeinfo.startSymbol()) + if err != nil { + return nil, err + } else if startSymbol.Value == 0 { + return nil, fmt.Errorf("failed to resolve %s", typeinfo.startSymbol()) + } + + endSymbol, err := e.getSymbol(typeinfo.endSymbol()) + if err != nil { + return nil, err + } else if endSymbol.Value == 0 { + return nil, fmt.Errorf("failed to resolve %s", typeinfo.endSymbol()) + } + + out := make([]T, 0) + data := make([]byte, typeinfo.size()) + for addr := startSymbol.Value; addr < endSymbol.Value; addr += typeinfo.size() { + section := e.elfSection(addr) + if section == nil { + return nil, fmt.Errorf("failed to locate section for addr=0x%x", addr) + } + + n, err := section.ReadAt(data, int64(addr-section.Addr)) + if err != nil || n < int(typeinfo.size()) { + // If n < sizeof(T), then err is non-nil as per the documentation + // of section.ReadAt. + return nil, err + } + + obj := T(new(P)) + err = obj.fromBytes(e.elfFile, data) + if err != nil { + return nil, err + } + out = append(out, obj) + } + + return out, nil +} diff --git a/pkg/kfuzztest/kfuzztest.go b/pkg/kfuzztest/kfuzztest.go new file mode 100644 index 000000000000..c4702ac421e3 --- /dev/null +++ b/pkg/kfuzztest/kfuzztest.go @@ -0,0 +1,207 @@ +// 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 kfuzztest exposes functions discovering KFuzzTest test cases from a +// vmlinux binary and parsing them into syzkaller-compatible formats. +// The general flow includes: +// - Creating an Extractor that extracts these test cases from the binary +// - Creating a Builder that takes the extractor's output and returns some +// compatible encoding of the test cases that were discovered +package kfuzztest + +import ( + "debug/dwarf" + "fmt" + "path" + "strings" + "sync" + + "github.com/google/syzkaller/pkg/ast" + "github.com/google/syzkaller/pkg/compiler" + "github.com/google/syzkaller/prog" + "github.com/google/syzkaller/sys/targets" +) + +type SyzField struct { + Name string + dwarfType dwarf.Type +} + +type SyzStruct struct { + dwarfType *dwarf.StructType + Name string + Fields []SyzField +} + +type SyzFunc struct { + Name string + InputStructName string +} + +type ConstraintType uint8 + +const ( + ExpectEq ConstraintType = iota + ExpectNe + ExpectLt + ExpectLe + ExpectGt + ExpectGe + ExpectInRange +) + +func (c ConstraintType) String() string { + return [...]string{"EXPECT_EQ", "EXPECT_NE", "EXPECT_LT", "EXPECT_LE", "EXPECT_GT", "EXPECT_GE", "EXPECT_IN_RANGE"}[c] +} + +type SyzConstraint struct { + InputType string + FieldName string + Value1 uintptr + Value2 uintptr + ConstraintType +} + +type AnnotationAttribute uint8 + +const ( + AttributeLen AnnotationAttribute = iota + AttributeString + AttributeArray +) + +func (a AnnotationAttribute) String() string { + return [...]string{"ATTRIBUTE_LEN", "ATTRIBUTE_STRING", "ATTRIBUTE_ARRAY"}[a] +} + +type SyzAnnotation struct { + InputType string + FieldName string + LinkedFieldName string + Attribute AnnotationAttribute +} + +// ExtractDescription returns a syzlang description of all discovered KFuzzTest +// targets, or an error on failure. +func ExtractDescription(vmlinuxPath string) (string, error) { + extractor, err := NewExtractor(vmlinuxPath) + if err != nil { + return "", err + } + defer extractor.Close() + eRes, err := extractor.ExtractAll() + if err != nil { + return "", err + } + builder := NewBuilder(eRes.Funcs, eRes.Structs, eRes.Constraints, eRes.Annotations) + return builder.EmitSyzlangDescription() +} + +type KFuzzTestData struct { + Description string + Calls []*prog.Syscall + Resources []*prog.ResourceDesc + Types []prog.Type +} + +func extractData(vmlinuxPath string) (KFuzzTestData, error) { + desc, err := ExtractDescription(vmlinuxPath) + if err != nil { + return KFuzzTestData{}, err + } + + var astError error + eh := func(pos ast.Pos, msg string) { + astError = fmt.Errorf("ast error: %v: %v", pos, msg) + } + descAst := ast.Parse([]byte(desc), "kfuzztest-autogen", eh) + if astError != nil { + return KFuzzTestData{}, astError + } + if descAst == nil { + return KFuzzTestData{}, fmt.Errorf("failed to build AST for program") + } + + // TODO: this assumes x86_64, but KFuzzTest supports (in theory) any + // architecture. + target := targets.Get(targets.Linux, targets.AMD64) + program := compiler.Compile(descAst, make(map[string]uint64), target, eh) + if astError != nil { + return KFuzzTestData{}, fmt.Errorf("failed to compile extracted KFuzzTest target: %w", astError) + } + + kFuzzTestCalls := []*prog.Syscall{} + for _, call := range program.Syscalls { + // The generated descriptions contain some number of built-ins, which + // we want to filter out. + if call.Attrs.KFuzzTest { + kFuzzTestCalls = append(kFuzzTestCalls, call) + } + } + + // We restore links on all generated system calls for completeness, but we + // only return the filtered slice. + prog.RestoreLinks(program.Syscalls, program.Resources, program.Types) + + return KFuzzTestData{ + Description: desc, + Calls: kFuzzTestCalls, + Resources: program.Resources, + Types: program.Types, + }, nil +} + +type extractKFuzzTestDataState struct { + once sync.Once + data KFuzzTestData + err error +} + +var extractState extractKFuzzTestDataState + +// ExtractData extracts KFuzzTest data from a vmlinux binary. The return value +// of this call is cached so that it can be safely called multiple times +// without incurring a new scan of a vmlinux image. +// NOTE: the implementation assumes the existence of only one vmlinux image +// per process, i.e. no attempt is made to distinguish different vmlinux images +// based on their path. +func ExtractData(vmlinuxPath string) (KFuzzTestData, error) { + extractState.once.Do(func() { + extractState.data, extractState.err = extractData(vmlinuxPath) + }) + + return extractState.data, extractState.err +} + +// ActivateKFuzzTargets extracts all KFuzzTest targets from a vmlinux binary +// and extends a target with the discovered pseudo-syscalls. +func ActivateKFuzzTargets(target *prog.Target, vmlinuxPath string) ([]*prog.Syscall, error) { + data, err := ExtractData(vmlinuxPath) + if err != nil { + return nil, err + } + // TODO: comment this properly. It's important to note here that despite + // extending the target, correct encoding relies on syz_kfuzztest_run being + // compiled into the target, and its ID being available. + target.Extend(data.Calls, data.Types, data.Resources) + return data.Calls, nil +} + +const syzKfuzzTestRun string = "syz_kfuzztest_run" + +// Common prefix that all discriminated syz_kfuzztest_run pseudo-syscalls share. +const KfuzzTestTargetPrefix string = syzKfuzzTestRun + "$" + +func GetTestName(syscall *prog.Syscall) (string, bool) { + if syscall.CallName != syzKfuzzTestRun { + return "", false + } + return strings.CutPrefix(syscall.Name, KfuzzTestTargetPrefix) +} + +const kFuzzTestDir string = "/sys/kernel/debug/kfuzztest" +const inputFile string = "input" + +func GetInputFilepath(testName string) string { + return path.Join(kFuzzTestDir, testName, inputFile) +} diff --git a/pkg/kfuzztest/testdata/.gitignore b/pkg/kfuzztest/testdata/.gitignore new file mode 100644 index 000000000000..837170fcd555 --- /dev/null +++ b/pkg/kfuzztest/testdata/.gitignore @@ -0,0 +1 @@ +*bin diff --git a/pkg/kfuzztest/testdata/1/desc.txt b/pkg/kfuzztest/testdata/1/desc.txt new file mode 100644 index 000000000000..6d18ebeea4a1 --- /dev/null +++ b/pkg/kfuzztest/testdata/1/desc.txt @@ -0,0 +1,7 @@ +# This description was automatically generated with tools/kfuzztest-gen +pkcs7_parse_message_arg { + data ptr[in, array[int8]] + datalen len[data, int64] +} + +syz_kfuzztest_run$test_pkcs7_parse_message(name ptr[in, string["test_pkcs7_parse_message"]], data ptr[in, pkcs7_parse_message_arg], len bytesize[data], buf ptr[in, array[int8, 65536]]) (kfuzz_test) diff --git a/pkg/kfuzztest/testdata/1/prog.c b/pkg/kfuzztest/testdata/1/prog.c new file mode 100644 index 000000000000..b1940ba1fe70 --- /dev/null +++ b/pkg/kfuzztest/testdata/1/prog.c @@ -0,0 +1,24 @@ +// 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. +#include "../common.h" + +#include +#include + +struct pkcs7_parse_message_arg { + const void* data; + size_t datalen; +}; + +DEFINE_FUZZ_TARGET(test_pkcs7_parse_message, struct pkcs7_parse_message_arg); +/* Expect data != NULL. */ +DEFINE_CONSTRAINT(pkcs7_parse_message_arg, data, NULL, NULL, EXPECT_NE); +/* Expect datalen == len(data). */ +DEFINE_ANNOTATION(pkcs7_parse_message_arg, datalen, data, ATTRIBUTE_LEN); +/* Annotate data as an array. */ +DEFINE_ANNOTATION(pkcs7_parse_message_arg, data, , ATTRIBUTE_ARRAY); + +/* Define a main function, otherwise the compiler complains. */ +int main(void) +{ +} diff --git a/pkg/kfuzztest/testdata/2/desc.txt b/pkg/kfuzztest/testdata/2/desc.txt new file mode 100644 index 000000000000..55ee03f8f804 --- /dev/null +++ b/pkg/kfuzztest/testdata/2/desc.txt @@ -0,0 +1,15 @@ +# This description was automatically generated with tools/kfuzztest-gen +bar { + a int32 + b int32 +} + +foo { + b ptr[in, bar] + str ptr[in, string] + data ptr[in, array[int8]] + datalen len[data, int64] + numbers ptr[in, array[int64]] +} + +syz_kfuzztest_run$some_target(name ptr[in, string["some_target"]], data ptr[in, foo], len bytesize[data], buf ptr[in, array[int8, 65536]]) (kfuzz_test) diff --git a/pkg/kfuzztest/testdata/2/prog.c b/pkg/kfuzztest/testdata/2/prog.c new file mode 100644 index 000000000000..908ccd271016 --- /dev/null +++ b/pkg/kfuzztest/testdata/2/prog.c @@ -0,0 +1,39 @@ +// 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. +#include "../common.h" + +#include + +struct bar { + int a; + int b; +}; + +struct foo { + struct bar* b; + const char* str; + const char* data; + size_t datalen; + uint64_t* numbers; +}; + +DEFINE_FUZZ_TARGET(some_target, struct foo); +/* Expect foo.bar != NULL. */ +DEFINE_CONSTRAINT(foo, bar, NULL, NULL, EXPECT_NE); +/* Expect foo.str != NULL. */ +DEFINE_CONSTRAINT(foo, str, NULL, NULL, EXPECT_NE); +/* Annotate foo.str as a string. */ +DEFINE_ANNOTATION(foo, str, , ATTRIBUTE_STRING); +/* Expect foo.data != NULL. */ +DEFINE_CONSTRAINT(foo, data, NULL, NULL, EXPECT_NE); +/* Annotate foo.data as an array. */ +DEFINE_ANNOTATION(foo, data, , ATTRIBUTE_ARRAY); +/* Annotate foo.datalen == len(foo.data). */ +DEFINE_ANNOTATION(foo, datalen, data, ATTRIBUTE_LEN); +/* Annotate foo.numbers as an array. */ +DEFINE_ANNOTATION(foo, numbers, , ATTRIBUTE_ARRAY); + +/* Define a main function, otherwise the compiler complains. */ +int main(void) +{ +} diff --git a/pkg/kfuzztest/testdata/common.h b/pkg/kfuzztest/testdata/common.h new file mode 100644 index 000000000000..29e8b193e6ba --- /dev/null +++ b/pkg/kfuzztest/testdata/common.h @@ -0,0 +1,81 @@ +// 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. + +// Common struct definitions that ressemble those sound in the kernel source +// under include/linux/kfuzztest.h. For testing purposes, it is only required +// that these have the same sizes and emitted metadata as the kernel +// definitions, and therefore there is no strict requirement that their fields +// match one-to-one. +#ifndef COMMON_H +#define COMMON_H + +#include + +struct kfuzztest_target { + const char *name; + const char *arg_type_name; + uintptr_t write_input_cb; +} __attribute__((aligned(32))); + +enum kfuzztest_constraint_type { + EXPECT_EQ, + EXPECT_NE, + EXPECT_LT, + EXPECT_LE, + EXPECT_GT, + EXPECT_GE, + EXPECT_IN_RANGE, +}; + +struct kfuzztest_constraint { + const char *input_type; + const char *field_name; + uintptr_t value1; + uintptr_t value2; + enum kfuzztest_constraint_type type; +} __attribute__((aligned(64))); + +enum kfuzztest_annotation_attribute { + ATTRIBUTE_LEN, + ATTRIBUTE_STRING, + ATTRIBUTE_ARRAY, +}; + +struct kfuzztest_annotation { + const char *input_type; + const char *field_name; + const char *linked_field_name; + enum kfuzztest_annotation_attribute attrib; +} __attribute__((aligned(32))); + +#define DEFINE_FUZZ_TARGET(test_name, test_arg_type) \ + struct kfuzztest_target __fuzz_test__##test_name \ + __attribute__((section(".kfuzztest_target"), __used__)) = { \ + .name = #test_name, \ + .arg_type_name = #test_arg_type, \ + }; \ + /* Avoid the compiler optimizing out the struct definition. */ \ + static test_arg_type arg; + +#define DEFINE_CONSTRAINT(arg_type, field, val1, val2, tpe) \ + static struct kfuzztest_constraint __constraint_##arg_type##_##field \ + __attribute__((section(".kfuzztest_constraint"), \ + __used__)) = { \ + .input_type = "struct " #arg_type, \ + .field_name = #field, \ + .value1 = (uintptr_t)val1, \ + .value2 = (uintptr_t)val2, \ + .type = tpe, \ + } + +#define DEFINE_ANNOTATION(arg_type, field, linked_field, attribute) \ + static struct kfuzztest_annotation __annotation_##arg_type##_##field \ + __attribute__((section(".kfuzztest_annotation"), \ + __used__)) = { \ + .input_type = "struct " #arg_type, \ + .field_name = #field, \ + .linked_field_name = #linked_field, \ + .attrib = attribute, \ + } + +#endif /* COMMON_H */ diff --git a/pkg/kfuzztest/testdata/linker.ld b/pkg/kfuzztest/testdata/linker.ld new file mode 100644 index 000000000000..345c021285f5 --- /dev/null +++ b/pkg/kfuzztest/testdata/linker.ld @@ -0,0 +1,39 @@ +/* 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. */ + +/* Defines a basic linkage script for building kernel-like KFuzzTest metadata into a binary. */ +PAGE_SIZE = 0x1000; + +PHDRS +{ + text PT_LOAD FLAGS(5); /* R, X */ + data PT_LOAD FLAGS(6); /* R, W */ +} + +SECTIONS +{ + .text : { *(.text) } :text + + .rodata : { + *(.rodata*) + + . = ALIGN(PAGE_SIZE); + __kfuzztest_targets_start = .; + KEEP(*(.kfuzztest_target)); + __kfuzztest_targets_end = .; + + . = ALIGN(PAGE_SIZE); + __kfuzztest_constraints_start = .; + KEEP(*(.kfuzztest_constraint)); + __kfuzztest_constraints_end = .; + + . = ALIGN(PAGE_SIZE); + __kfuzztest_annotations_start = .; + KEEP(*(.kfuzztest_annotation)); + __kfuzztest_annotations_end = .; + + } :text + + .data : { *(.data) } :data + .bss : { *(.bss) } :data +} diff --git a/pkg/kfuzztest/types.go b/pkg/kfuzztest/types.go new file mode 100644 index 000000000000..b533f95c3afa --- /dev/null +++ b/pkg/kfuzztest/types.go @@ -0,0 +1,135 @@ +// 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 kfuzztest + +import ( + "debug/elf" + "fmt" +) + +// The parsableFromBytes interface describes a kftf object that can be parsed +// from a vmlinux binary. All objects are expected to satisfy the following +// constraints +// - Must be statically sized. I.e. the size() function should return some +// fixed value +// - Densely packed: size must exactly describe the number of bytes between +// the start address of instance i and that of instance i+1. +// +// No further assumptions are made about the semantics of the object. For +// example if some field is a pointer to a string (*const char) this will not +// be read from the binary. This responsibility is offloaded to the caller. +type parsableFromBytes interface { + fromBytes(elfFile *elf.File, data []byte) error + size() uint64 + startSymbol() string + endSymbol() string +} + +type kfuzztestTarget struct { + name uint64 + argType uint64 + writeCb uint64 + readCb uint64 +} + +const kfuzztestTargetStart string = "__kfuzztest_targets_start" +const kfuzztestTargetEnd string = "__kfuzztest_targets_end" +const kfuzztestTargetSize uint64 = 32 + +func incorrectByteSizeErr(expected, actual uint64) error { + return fmt.Errorf("incorrect number of bytes: expected %d, got %d", expected, actual) +} + +func (targ *kfuzztestTarget) fromBytes(elfFile *elf.File, data []byte) error { + if targ.size() != uint64(len(data)) { + return incorrectByteSizeErr(targ.size(), uint64(len(data))) + } + targ.name = elfFile.ByteOrder.Uint64(data[0:8]) + targ.argType = elfFile.ByteOrder.Uint64(data[8:16]) + targ.writeCb = elfFile.ByteOrder.Uint64(data[16:24]) + targ.readCb = elfFile.ByteOrder.Uint64(data[24:32]) + return nil +} + +func (targ *kfuzztestTarget) size() uint64 { + return kfuzztestTargetSize +} + +func (targ *kfuzztestTarget) startSymbol() string { + return kfuzztestTargetStart +} + +func (targ *kfuzztestTarget) endSymbol() string { + return kfuzztestTargetEnd +} + +type kfuzztestConstraint struct { + inputType uint64 + fieldName uint64 + value1 uintptr + value2 uintptr + constraintType uint8 +} + +const kfuzztestConstraintStart string = "__kfuzztest_constraints_start" +const kfuzztestConstraintEnd string = "__kfuzztest_constraints_end" +const kfuzztestConstraintSize uint64 = 64 + +func (c *kfuzztestConstraint) fromBytes(elfFile *elf.File, data []byte) error { + if c.size() != uint64(len(data)) { + return incorrectByteSizeErr(c.size(), uint64(len(data))) + } + constraintTypeBytes := elfFile.ByteOrder.Uint64(data[32:40]) + c.inputType = elfFile.ByteOrder.Uint64(data[0:8]) + c.fieldName = elfFile.ByteOrder.Uint64(data[8:16]) + c.value1 = uintptr(elfFile.ByteOrder.Uint64(data[16:24])) + c.value2 = uintptr(elfFile.ByteOrder.Uint64(data[24:32])) + c.constraintType = uint8(constraintTypeBytes & 0xFF) + return nil +} + +func (c *kfuzztestConstraint) size() uint64 { + return kfuzztestConstraintSize +} + +func (c *kfuzztestConstraint) startSymbol() string { + return kfuzztestConstraintStart +} + +func (c *kfuzztestConstraint) endSymbol() string { + return kfuzztestConstraintEnd +} + +type kfuzztestAnnotation struct { + inputType uint64 + fieldName uint64 + linkedFieldName uint64 + annotationAttribute uint8 +} + +func (a *kfuzztestAnnotation) fromBytes(elfFile *elf.File, data []byte) error { + if a.size() != uint64(len(data)) { + return incorrectByteSizeErr(a.size(), uint64(len(data))) + } + a.inputType = elfFile.ByteOrder.Uint64(data[0:8]) + a.fieldName = elfFile.ByteOrder.Uint64(data[8:16]) + a.linkedFieldName = elfFile.ByteOrder.Uint64(data[16:24]) + a.annotationAttribute = data[24] + return nil +} + +const kftfAnnotationStart string = "__kfuzztest_annotations_start" +const kftfAnnotationEnd string = "__kfuzztest_annotations_end" +const kftfAnnotationSize uint64 = 32 + +func (a *kfuzztestAnnotation) size() uint64 { + return kftfAnnotationSize +} + +func (a *kfuzztestAnnotation) startSymbol() string { + return kftfAnnotationStart +} + +func (a *kfuzztestAnnotation) endSymbol() string { + return kftfAnnotationEnd +} diff --git a/pkg/mgrconfig/config.go b/pkg/mgrconfig/config.go index c9944900e9ad..45145243c98d 100644 --- a/pkg/mgrconfig/config.go +++ b/pkg/mgrconfig/config.go @@ -255,6 +255,9 @@ type Experimental struct { // with an empty Filter, but non-empty weight. // E.g. "focus_areas": [ {"filter": {"files": ["^net"]}, "weight": 10.0}, {"weight": 1.0} ]. FocusAreas []FocusArea `json:"focus_areas,omitempty"` + + // Enable dynamic discovery and fuzzing of KFuzzTest targets. + EnableKFuzzTest bool `json:"enable_kfuzztest"` } type FocusArea struct { diff --git a/pkg/vminfo/linux_syscalls.go b/pkg/vminfo/linux_syscalls.go index abd749be32da..605b939d255b 100644 --- a/pkg/vminfo/linux_syscalls.go +++ b/pkg/vminfo/linux_syscalls.go @@ -106,6 +106,7 @@ var linuxSyscallChecks = map[string]func(*checkContext, *prog.Syscall) string{ "syz_socket_connect_nvme_tcp": linuxSyzSocketConnectNvmeTCPSupported, "syz_pidfd_open": alwaysSupported, "syz_create_resource": alwaysSupported, + "syz_kfuzztest_run": alwaysSupported, } func linuxSyzOpenDevSupported(ctx *checkContext, call *prog.Syscall) string { diff --git a/prog/encodingexec.go b/prog/encodingexec.go index fb8e5fbaf154..14466a272501 100644 --- a/prog/encodingexec.go +++ b/prog/encodingexec.go @@ -75,7 +75,9 @@ func (p *Prog) SerializeForExec() ([]byte, error) { w.write(uint64(len(p.Calls))) for _, c := range p.Calls { w.csumMap, w.csumUses = calcChecksumsCall(c) - w.serializeCall(c) + // TODO: if we propagate this error, something breaks and no coverage + // is displayed to the dashboard or the logs. + _ = w.serializeCall(c) } w.write(execInstrEOF) if len(w.buf) > ExecBufferSize { @@ -87,7 +89,14 @@ func (p *Prog) SerializeForExec() ([]byte, error) { return w.buf, nil } -func (w *execContext) serializeCall(c *Call) { +func (w *execContext) serializeCall(c *Call) error { + // We introduce special serialization logic for kfuzztest targets, which + // require special handling due to their use of relocation tables to copy + // entire blobs of data into the kenrel. + if c.Meta.Attrs.KFuzzTest { + return w.serializeKFuzzTestCall(c) + } + // Calculate arg offsets within structs. // Generate copyin instructions that fill in data into pointer arguments. w.writeCopyin(c) @@ -117,6 +126,68 @@ func (w *execContext) serializeCall(c *Call) { // Generate copyout instructions that persist interesting return values. w.writeCopyout(c) + return nil +} + +// KFuzzTest targets require special handling due to their use of relocation +// tables for serializing all data (including pointed-to data) into a +// continuous blob that can be passed into the kernel. +func (w *execContext) serializeKFuzzTestCall(c *Call) error { + if !c.Meta.Attrs.KFuzzTest { + // This is a specialized function that shouldn't be called on anything + // other than an instance of a syz_kfuzztest_run$* syscall + panic("serializeKFuzzTestCall called on an invalid syscall") + } + + // Generate the final syscall instruction with the update arguments. + kFuzzTestRunID, err := w.target.KFuzzTestRunID() + if err != nil { + panic(err) + } + // Ensures that we copy some arguments into the executor so that it doesn't + // receive an incomplete program on failure. + defer func() { + w.write(uint64(kFuzzTestRunID)) + w.write(ExecNoCopyout) + w.write(uint64(len(c.Args))) + for _, arg := range c.Args { + w.writeArg(arg) + } + }() + + // Write the initial string argument (test name) normally. + w.writeCopyin(&Call{Meta: c.Meta, Args: []Arg{c.Args[0]}}) + + // Args[1] is the second argument to syz_kfuzztest_run, which is a pointer + // to some struct input. This is the data that must be flattened and sent + // to the fuzzing driver with a relocation table. + dataArg := c.Args[1].(*PointerArg) + finalBlob := MarshallKFuzztestArg(dataArg.Res) + if len(finalBlob) > int(KFuzzTestMaxInputSize) { + return fmt.Errorf("encoded blob was too large") + } + + // Use the buffer argument as data offset - this represents a buffer of + // size 64KiB - the maximum input size that the KFuzzTest module accepts. + bufferArg := c.Args[3].(*PointerArg) + if bufferArg.Res == nil { + return fmt.Errorf("buffer was nil") + } + blobAddress := w.target.PhysicalAddr(bufferArg) - w.target.DataOffset + + // Write the entire marshalled blob as a raw byte array. + w.write(execInstrCopyin) + w.write(blobAddress) + w.write(execArgData) + w.write(uint64(len(finalBlob))) + w.buf = append(w.buf, finalBlob...) + + // Update the value of the length arg which should now match the length of + // the byte array that we created. Previously, it contained the bytesize + // of the struct argument passed into the pseudo-syscall. + lenArg := c.Args[2].(*ConstArg) + lenArg.Val = uint64(len(finalBlob)) + return nil } type execContext struct { diff --git a/prog/kfuzztest.go b/prog/kfuzztest.go new file mode 100644 index 000000000000..dacd54885c83 --- /dev/null +++ b/prog/kfuzztest.go @@ -0,0 +1,296 @@ +// 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 prog + +import ( + "bytes" + "encoding/binary" + "fmt" +) + +const ( + kFuzzTestRegionIDNull uint32 = ^uint32(0) + kFuzzTestPoisonSize uint64 = 0x8 + + kFuzzTestVersion uint32 = 0 + kFuzzTestMagic uint32 = 0xBFACE + kFuzzTestPrefixSize = 8 + + // Minimum region alignment required by KFuzzTest. This is exposed by the + // /sys/kernel/debug/kfuzztest/_config/minalign debugfs file. This value + // always equals MAX(ARCH_KMALLOC_MINALIGN, KFUZZTEST_POISON_SIZE) = 8 on + // x86_64, so we hardcode it for now. A more robust solution would involve + // reading this from the debugfs entry at boot before fuzzing begins. + kFuzzTestMinalign uint64 = 8 + + // Maximum input size accepted by the KFuzzTest kernel module. + KFuzzTestMaxInputSize uint64 = 64 << 10 +) + +func kFuzzTestWritePrefix(buf *bytes.Buffer) { + prefix := (uint64(kFuzzTestVersion) << 32) | uint64(kFuzzTestMagic) + binary.Write(buf, binary.LittleEndian, prefix) +} + +func isPowerOfTwo(n uint64) bool { + return n > 0 && (n&(n-1) == 0) +} + +func roundUpPowerOfTwo(x, n uint64) uint64 { + if !isPowerOfTwo(n) { + panic("n was not a power of 2") + } + return (x + n - 1) &^ (n - 1) +} + +// Pad b so that it's length is a multiple of alignment, with at least +// minPadding bytes of padding, where alignment is a power of 2. +func padWithAlignment(b *bytes.Buffer, alignment, minPadding uint64) { + var newSize uint64 + if alignment == 0 { + newSize = uint64(b.Len()) + minPadding + } else { + newSize = roundUpPowerOfTwo(uint64(b.Len())+minPadding, alignment) + } + + paddingBytes := newSize - uint64(b.Len()) + for range paddingBytes { + b.WriteByte(byte(0)) + } +} + +type sliceQueue[T any] struct { + q []T +} + +func (sq *sliceQueue[T]) push(elem T) { + sq.q = append(sq.q, elem) +} + +func (sq *sliceQueue[T]) pop() T { + ret := sq.q[0] + sq.q = sq.q[1:] + return ret +} + +func (sq *sliceQueue[T]) isEmpty() bool { + return len(sq.q) == 0 +} + +func newSliceQueue[T any]() *sliceQueue[T] { + return &sliceQueue[T]{q: make([]T, 0)} +} + +type kFuzzTestRelocation struct { + offset uint32 + srcRegion Arg + dstRegion Arg +} + +type kFuzzTestRegion struct { + offset uint32 + size uint32 +} + +// The following helpers and definitions follow directly from the C-struct +// definitions in . +const kFuzzTestRegionSize = 8 + +func kFuzzTestRegionArraySize(numRegions int) int { + return 4 + kFuzzTestRegionSize*numRegions +} + +func kFuzzTestWriteRegion(buf *bytes.Buffer, region kFuzzTestRegion) { + binary.Write(buf, binary.LittleEndian, region.offset) + binary.Write(buf, binary.LittleEndian, region.size) +} + +func kFuzzTestWriteRegionArray(buf *bytes.Buffer, regions []kFuzzTestRegion) { + binary.Write(buf, binary.LittleEndian, uint32(len(regions))) + for _, reg := range regions { + kFuzzTestWriteRegion(buf, reg) + } +} + +const kFuzzTestRelocationSize = 12 + +func kFuzzTestRelocTableSize(numRelocs int) int { + return 8 + kFuzzTestRelocationSize*numRelocs +} + +func kFuzzTestWriteReloc(buf *bytes.Buffer, regToID *map[Arg]int, reloc kFuzzTestRelocation) { + binary.Write(buf, binary.LittleEndian, uint32((*regToID)[reloc.srcRegion])) + binary.Write(buf, binary.LittleEndian, reloc.offset) + if reloc.dstRegion == nil { + binary.Write(buf, binary.LittleEndian, kFuzzTestRegionIDNull) + } else { + binary.Write(buf, binary.LittleEndian, uint32((*regToID)[reloc.dstRegion])) + } +} + +func kFuzzTestWriteRelocTable(buf *bytes.Buffer, regToID *map[Arg]int, + relocations []kFuzzTestRelocation, paddingBytes uint64) { + binary.Write(buf, binary.LittleEndian, uint32(len(relocations))) + binary.Write(buf, binary.LittleEndian, uint32(paddingBytes)) + for _, reloc := range relocations { + kFuzzTestWriteReloc(buf, regToID, reloc) + } + buf.Write(make([]byte, paddingBytes)) +} + +const kFuzzTestPlaceHolderPtr uint64 = 0xFFFFFFFFFFFFFFFF + +// Expands a region, and returns a list of relocations that need to be made. +func kFuzzTestExpandRegion(reg Arg) ([]byte, []kFuzzTestRelocation) { + relocations := []kFuzzTestRelocation{} + var encoded bytes.Buffer + queue := newSliceQueue[Arg]() + queue.push(reg) + + for !queue.isEmpty() { + arg := queue.pop() + padWithAlignment(&encoded, arg.Type().Alignment(), 0) + + switch a := arg.(type) { + case *PointerArg: + offset := uint32(encoded.Len()) + binary.Write(&encoded, binary.LittleEndian, kFuzzTestPlaceHolderPtr) + relocations = append(relocations, kFuzzTestRelocation{offset, reg, a.Res}) + case *GroupArg: + for _, inner := range a.Inner { + queue.push(inner) + } + case *DataArg: + data := a.data + buffer, ok := a.ArgCommon.Type().(*BufferType) + if !ok { + panic("DataArg should be a BufferType") + } + // Unlike length fields whose incorrectness can be prevented easily, + // it is an invasive change to prevent generation of + // non-null-terminated strings. Forcibly null-terminating them + // during encoding allows us to centralize this easily and prevent + // false positive buffer overflows in KFuzzTest targets. + if buffer.Kind == BufferString && (len(data) == 0 || data[len(data)-1] != byte(0)) { + data = append(data, byte(0)) + } + encoded.Write(data) + case *ConstArg: + val, _ := a.Value() + switch a.Size() { + case 1: + binary.Write(&encoded, binary.LittleEndian, uint8(val)) + case 2: + binary.Write(&encoded, binary.LittleEndian, uint16(val)) + case 4: + binary.Write(&encoded, binary.LittleEndian, uint32(val)) + case 8: + binary.Write(&encoded, binary.LittleEndian, val) + default: + panic(fmt.Sprintf("unsupported constant size: %d", a.Size())) + } + // TODO: handle union args. + default: + panic(fmt.Sprintf("tried to serialize unsupported type: %s", a.Type().Name())) + } + } + + return encoded.Bytes(), relocations +} + +// MarshallKFuzzTestArg serializes a syzkaller Arg into a flat binary format +// understood by the KFuzzTest kernel interface (see `include/linux/kfuzztest.h`). +// +// The goal is to represent a tree-like structure of arguments (which may contain +// pointers and cycles) as a single byte slice that the kernel can deserialize +// into a set of distinct heap allocations. +// +// The binary format consists of three contiguous parts, in this order: +// +// 1. Region Array: A header describing all logical memory regions that will be +// allocated by the kernel. Each `relocRegion` defines a region's unique `id`, +// its `size`, its `alignment`, and its `start` offset within the payload. +// The kernel uses this table to create one distinct heap allocation per region. +// +// 2. Relocation Table: A header containing a list of `relocationEntry` structs. +// Each entry identifies the location of a pointer field within the payload +// (via a `regionID` and `regionOffset`) and maps it to the logical region +// it points to (via a `value` which holds the pointee's `regionID`). +// A NULL pointer is identified by the special value `kFuzzTestNilPtrVal`. +// +// 3. Payload: The raw, serialized data for all arguments, laid out as a single +// contiguous block of memory with padded regions as per the KFuzzTest input +// format's specification defined in `Documentation/dev-tools/kfuzztest.rst`. +// +// Cycles are handled by tracking visited arguments, ensuring that a region is +// only visited and encoded once. +// +// For a concrete example of the final binary layout, see the test cases for this +// function in `prog/kfuzztest_test.go`. +func MarshallKFuzztestArg(topLevel Arg) []byte { + regions := []kFuzzTestRegion{} + allRelocations := []kFuzzTestRelocation{} + visitedRegions := make(map[Arg]int) + queue := newSliceQueue[Arg]() + var payload bytes.Buffer + queue.push(topLevel) + maxAlignment := uint64(8) + + if topLevel == nil { + return []byte{} + } + +Loop: + for { + if queue.isEmpty() { + break Loop + } + + reg := queue.pop() + if _, visited := visitedRegions[reg]; visited { + continue Loop + } + + alignment := max(kFuzzTestMinalign, reg.Type().Alignment()) + maxAlignment = max(maxAlignment, alignment) + + regionData, relocations := kFuzzTestExpandRegion(reg) + for _, reloc := range relocations { + if reloc.dstRegion == nil { + continue + } + if _, visited := visitedRegions[reloc.dstRegion]; !visited { + queue.push(reloc.dstRegion) + } + } + allRelocations = append(allRelocations, relocations...) + + padWithAlignment(&payload, alignment, 0) + regions = append(regions, kFuzzTestRegion{ + offset: uint32(payload.Len()), + size: uint32(len(regionData))}, + ) + visitedRegions[reg] = len(regions) - 1 + payload.Write(regionData) + // The end of the payload should have at least kFuzzTestPoisonSize bytes + // of padding, and be aligned to kFuzzTestPoisonSize. + padWithAlignment(&payload, kFuzzTestPoisonSize, kFuzzTestPoisonSize) + } + + headerLen := 0x8 // Two integer values - the magic value, and the version number. + regionArrayLen := kFuzzTestRegionArraySize(len(regions)) + relocTableLen := kFuzzTestRelocTableSize(len(allRelocations)) + metadataLen := headerLen + regionArrayLen + relocTableLen + + // The payload needs to be aligned to max alignment to ensure that all + // nested structs are properly aligned, and there should be enough padding + // so that the region before the payload can be poisoned with a redzone. + paddingBytes := roundUpPowerOfTwo(uint64(metadataLen)+kFuzzTestPoisonSize, maxAlignment) - uint64(metadataLen) + + var out bytes.Buffer + kFuzzTestWritePrefix(&out) + kFuzzTestWriteRegionArray(&out, regions) + kFuzzTestWriteRelocTable(&out, &visitedRegions, allRelocations, paddingBytes) + out.Write(payload.Bytes()) + return out.Bytes() +} diff --git a/prog/kfuzztest_test.go b/prog/kfuzztest_test.go new file mode 100644 index 000000000000..5f1b3d9c4d63 --- /dev/null +++ b/prog/kfuzztest_test.go @@ -0,0 +1,262 @@ +// 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 prog + +import ( + "bytes" + "encoding/binary" + "testing" + + "github.com/stretchr/testify/assert" +) + +type testCase struct { + prog string + extractArg func(*Prog) Arg + regionArray []any + relocationTable []any + payload []any +} + +func TestRoundUpPowerOfTwo(t *testing.T) { + if res := roundUpPowerOfTwo(9, 8); res != 16 { + t.Fatalf("expected 16, got %d", res) + } + if res := roundUpPowerOfTwo(21, 4); res != 24 { + t.Fatalf("expected 24, got %d", res) + } + if res := roundUpPowerOfTwo(113, 16); res != 128 { + t.Fatalf("expected 24, got %d", res) + } +} + +func createBuffer(data []any) []byte { + var buf bytes.Buffer + + for _, d := range data { + switch val := d.(type) { + case uint8, uint16, uint32, uint64: + binary.Write(&buf, binary.LittleEndian, val) + case []byte: + buf.Write(val) + } + } + + return buf.Bytes() +} + +func createPrefix() []byte { + var prefix bytes.Buffer + binary.Write(&prefix, binary.LittleEndian, kFuzzTestMagic) + binary.Write(&prefix, binary.LittleEndian, uint32(0)) + return prefix.Bytes() +} + +//nolint:all +func TestMarshallKFuzzTestArg(t *testing.T) { + testCases := []testCase{ + // This test case validates the encoding of the following structure: + // msg: ptr[in, msghdr_netlink[netlink_msg_xfrm]] { + // msghdr_netlink[netlink_msg_xfrm] { + // addr: nil + // addrlen: len = 0x0 (4 bytes) + // pad = 0x0 (4 bytes) + // vec: ptr[in, iovec[in, netlink_msg_xfrm]] { + // iovec[in, netlink_msg_xfrm] { + // addr: ptr[inout, array[ANYUNION]] { + // array[ANYUNION] { + // } + // } + // len: len = 0x33fe0 (8 bytes) + // } + // } + // vlen: const = 0x1 (8 bytes) + // ctrl: const = 0x0 (8 bytes) + // ctrllen: const = 0x0 (8 bytes) + // f: send_flags = 0x0 (4 bytes) + // pad = 0x0 (4 bytes) + // } + // } + { + `r0 = openat$cgroup_ro(0xffffffffffffff9c, &(0x7f00000003c0)='cpuacct.stat\x00', 0x26e1, 0x0) +sendmsg$nl_xfrm(r0, &(0x7f0000000240)={0x0, 0x0, &(0x7f0000000080)={&(0x7f00000001c0)=ANY=[], 0x33fe0}}, 0x0)`, + func(p *Prog) Arg { + sendMsgCall := p.Calls[1] + msgHdr := sendMsgCall.Args[1].(*PointerArg).Res + return msgHdr + }, + []any{ + uint32(3), // Num regions. + + // Region definitions: (offset, size) pairs. + uint32(0), uint32(0x38), + uint32(0x40), uint32(0x10), + uint32(0x58), uint32(0x0), + }, + []any{ + uint32(3), // Num entries. + uint32(0x8), // Bytes of padding. + + // Relocation definitions: (source region, offset, dest region) triplets. + uint32(0), uint32(0x00), kFuzzTestRegionIDNull, + uint32(0), uint32(0x10), uint32(1), + uint32(1), uint32(0x00), uint32(2), + uint64(0x0), // 8 bytes of padding. + }, + []any{ + // Region 0 data. + kFuzzTestPlaceHolderPtr, // `addr` field, placeholder pointer. + uint32(0x0), // `addrlen`. + uint32(0x0), // `pad[4]`. + kFuzzTestPlaceHolderPtr, // `vec` field, placeholder pointer. + uint64(0x1), // `vlen`. + uint64(0x0), // `ctrl`. + uint64(0x0), // `ctrllen`. + uint32(0x0), // `f`. + uint32(0x0), // `pad[4]`. + + uint64(0x0), // 8 bytes of padding between regions. + + // Region 1 data. + kFuzzTestPlaceHolderPtr, // `addr` field, placeholder pointer. + uint64(0x033fe0), // `len`. + + make([]byte, kFuzzTestPoisonSize), // Inter-region padding. + + []byte{}, // Region 2 data (empty). + + make([]byte, kFuzzTestPoisonSize), // Tail padding. + }, + }, + // This test case validates the encoding of the following structure: + // loop_info64 { + // lo_device: const = 0x0 (8 bytes) + // lo_inode: const = 0x0 (8 bytes) + // lo_rdevice: const = 0x0 (8 bytes) + // lo_offset: int64 = 0x1 (8 bytes) + // lo_sizelimit: int64 = 0x8005 (8 bytes) + // lo_number: const = 0x0 (4 bytes) + // lo_enc_type: lo_encrypt_type = 0x0 (4 bytes) + // lo_enc_key_size: int32 = 0x19 (4 bytes) + // lo_flags: lo_flags = 0x1c (4 bytes) + // lo_file_name: buffer: {ef 35 9f 41 3b b9 38 52 f7 d6 a4 ae 6d dd fb + // d1 ce 5d 29 c2 ee 5e 5c a9 00 0f f8 ee 09 e7 37 ff 0e df 11 0f f4 11 + // 76 39 c2 eb 4b 78 c6 60 e6 77 df 70 19 05 b9 aa fa b4 af aa f7 55 a3 + // f6 a0 04} (length 0x40) lo_crypt_name: buffer: {03 6c 47 c6 78 08 20 + // d1 cb f7 96 6d 61 fd cf 33 52 63 bd 9b ff bc c2 54 2d ed 71 03 82 59 + // ca 17 1c e1 a3 11 ef 54 ec 32 d7 1e 14 ef 3d c1 77 e9 b4 8b 00 00 00 + // 00 00 00 00 00 00 00 00 00 00 00} (length 0x40) lo_enc_key: buffer: + // {f2 83 59 73 8e 22 9a 4c 66 81 00 00 00 00 00 d3 00 e6 d6 02 00 00 + // 00 00 00 00 00 00 00 00 00 01} (length 0x20) lo_init: array[int64] { + // int64 = 0x204 (8 bytes) + // int64 = 0x0 (8 bytes) + // } + // } + // } + // ] + { + `r0 = open(&(0x7f0000000000)='./bus\x00', 0x0, 0x0) +ioctl$LOOP_SET_STATUS64(r0, 0x4c04, &(0x7f0000000540)={0x0, 0x0, 0x0, 0x1, 0x8005, 0x0, 0x0, 0x19, 0x1c, "ef359f413bb93852f7d6a4ae6dddfbd1ce5d29c2ee5e5ca9000ff8ee09e737ff0edf110ff4117639c2eb4b78c660e677df701905b9aafab4afaaf755a3f6a004", "036c47c6780820d1cbf7966d61fdcf335263bd9bffbcc2542ded71038259ca171ce1a311ef54ec32d71e14ef3dc177e9b48b00", "f28359738e229a4c66810000000000d300e6d602000000000000000000000001", [0x204]})`, + func(p *Prog) Arg { + ioctlCall := p.Calls[1] + ptrArg := ioctlCall.Args[2].(*PointerArg) + ret := ptrArg.Res + return ret + }, + []any{ + uint32(1), // Num regions. + + // Region definitions: (offset, size) pairs. + uint32(0), uint32(0xe8), + }, + []any{ + uint32(0), // Num entries. + uint32(12), // Number of bytes of padding. + make([]byte, 12), // Padding. + }, + []any{ + uint64(0x0), // `lo_device`. + uint64(0x0), // `lo_inode`. + uint64(0x0), // `lo_rdevice`. + uint64(0x1), // `lo_offset`. + uint64(0x8005), // `lo_sizelimit`. + uint32(0x0), // `lo_number`. + uint32(0x0), // `lo_enc_type`. + uint32(0x19), // `lo_enc_key_size`. + uint32(0x1c), // `lo_flags`. + []byte{ + 0xef, 0x35, 0x9f, 0x41, 0x3b, 0xb9, 0x38, 0x52, + 0xf7, 0xd6, 0xa4, 0xae, 0x6d, 0xdd, 0xfb, 0xd1, + 0xce, 0x5d, 0x29, 0xc2, 0xee, 0x5e, 0x5c, 0xa9, + 0x00, 0x0f, 0xf8, 0xee, 0x09, 0xe7, 0x37, 0xff, + 0x0e, 0xdf, 0x11, 0x0f, 0xf4, 0x11, 0x76, 0x39, + 0xc2, 0xeb, 0x4b, 0x78, 0xc6, 0x60, 0xe6, 0x77, + 0xdf, 0x70, 0x19, 0x05, 0xb9, 0xaa, 0xfa, 0xb4, + 0xaf, 0xaa, 0xf7, 0x55, 0xa3, 0xf6, 0xa0, 0x04, + }, // `lo_file_name`. + []byte{ + 0x03, 0x6c, 0x47, 0xc6, 0x78, 0x08, 0x20, 0xd1, + 0xcb, 0xf7, 0x96, 0x6d, 0x61, 0xfd, 0xcf, 0x33, + 0x52, 0x63, 0xbd, 0x9b, 0xff, 0xbc, 0xc2, 0x54, + 0x2d, 0xed, 0x71, 0x03, 0x82, 0x59, 0xca, 0x17, + 0x1c, 0xe1, 0xa3, 0x11, 0xef, 0x54, 0xec, 0x32, + 0xd7, 0x1e, 0x14, 0xef, 0x3d, 0xc1, 0x77, 0xe9, + 0xb4, 0x8b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, // `lo_crypt_name`. + []byte{ + 0xf2, 0x83, 0x59, 0x73, 0x8e, 0x22, 0x9a, 0x4c, + 0x66, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd3, + 0x00, 0xe6, 0xd6, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + }, // `lo_enc_key`. + uint64(0x204), // `lo_init[0]. + uint64(0x0), // `lo_init[1]. + + make([]byte, kFuzzTestPoisonSize), // Tail padding. + }, + }, + } + + for _, tc := range testCases { + testOne(t, tc) + } +} + +func testOne(t *testing.T, tc testCase) { + target, err := GetTarget("linux", "amd64") + if err != nil { + t.Fatal(err) + } + p, err := target.Deserialize([]byte(tc.prog), NonStrict) + if err != nil { + t.Fatal(err) + } + + arg := tc.extractArg(p) + encoded := MarshallKFuzztestArg(arg) + + wantPrefix := createPrefix() + wantRegionArray := createBuffer(tc.regionArray) + wantRelocTable := createBuffer(tc.relocationTable) + wantPayload := createBuffer(tc.payload) + + regionArrayLen := len(wantRegionArray) + relocTableLen := len(wantRelocTable) + payloadLen := len(wantPayload) + + if len(encoded) != kFuzzTestPrefixSize+regionArrayLen+relocTableLen+payloadLen { + t.Fatalf("encoded output has wrong total length: got %d, want %d", + len(encoded), regionArrayLen+relocTableLen+payloadLen) + } + + gotPrefix := encoded[:kFuzzTestPrefixSize] + gotRegionArray := encoded[kFuzzTestPrefixSize : kFuzzTestPrefixSize+regionArrayLen] + gotRelocTable := encoded[kFuzzTestPrefixSize+regionArrayLen : kFuzzTestPrefixSize+regionArrayLen+relocTableLen] + gotPayload := encoded[kFuzzTestPrefixSize+regionArrayLen+relocTableLen:] + + assert.Equal(t, wantPrefix, gotPrefix) + assert.Equal(t, wantRegionArray, gotRegionArray) + assert.Equal(t, wantRelocTable, gotRelocTable) + assert.Equal(t, wantPayload, gotPayload) +} diff --git a/prog/mutation.go b/prog/mutation.go index 4d05e0a7d1c2..ecae816088fd 100644 --- a/prog/mutation.go +++ b/prog/mutation.go @@ -227,13 +227,20 @@ func (ctx *mutator) mutateArg() bool { return false } c := p.Calls[idx] + if c.Meta.Attrs.KFuzzTest { + tmp := r.genKFuzzTest + r.genKFuzzTest = true + defer func() { + r.genKFuzzTest = tmp + }() + } if ctx.noMutate[c.Meta.ID] { return false } updateSizes := true for stop, ok := false, false; !stop; stop = ok && r.oneOf(ctx.opts.MutateArgCount) { ok = true - ma := &mutationArgs{target: p.Target} + ma := &mutationArgs{target: p.Target, ignoreLengths: c.Meta.Attrs.KFuzzTest} ForeachArg(c, ma.collectArg) if len(ma.args) == 0 { return false @@ -271,7 +278,7 @@ func chooseCall(p *Prog, r *randGen) int { for _, c := range p.Calls { var totalPrio float64 ForeachArg(c, func(arg Arg, ctx *ArgCtx) { - prio, stopRecursion := arg.Type().getMutationPrio(p.Target, arg, false) + prio, stopRecursion := arg.Type().getMutationPrio(p.Target, arg, false, c.Meta.Attrs.KFuzzTest) totalPrio += prio ctx.Stop = stopRecursion }) @@ -509,7 +516,10 @@ func (t *ArrayType) mutate(r *randGen, s *state, arg Arg, ctx ArgCtx) (calls []* func (t *PtrType) mutate(r *randGen, s *state, arg Arg, ctx ArgCtx) (calls []*Call, retry, preserve bool) { a := arg.(*PointerArg) - if r.oneOf(1000) { + // Do not generate special pointers for KFuzzTest calls, as they are + // difficult to identify in the kernel and can lead to false positive + // crash reports. + if r.oneOf(1000) && !r.genKFuzzTest { removeArg(a.Res) index := r.rand(len(r.target.SpecialPointers)) newArg := MakeSpecialPointerArg(t, a.Dir(), index) @@ -565,6 +575,7 @@ func (t *ConstType) mutate(r *randGen, s *state, arg Arg, ctx ArgCtx) (calls []* type mutationArgs struct { target *Target ignoreSpecial bool + ignoreLengths bool prioSum float64 args []mutationArg argsBuffer [16]mutationArg @@ -587,7 +598,7 @@ func (ma *mutationArgs) collectArg(arg Arg, ctx *ArgCtx) { ma.ignoreSpecial = false typ := arg.Type() - prio, stopRecursion := typ.getMutationPrio(ma.target, arg, ignoreSpecial) + prio, stopRecursion := typ.getMutationPrio(ma.target, arg, ignoreSpecial, ma.ignoreLengths) ctx.Stop = stopRecursion if prio == dontMutate { @@ -617,7 +628,8 @@ func (ma *mutationArgs) chooseArg(r *rand.Rand) (Arg, ArgCtx) { // TODO: find a way to estimate optimal priority values. // Assign a priority for each type. The boolean is the reference type and it has // the minimum priority, since it has only two possible values. -func (t *IntType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) (prio float64, stopRecursion bool) { +func (t *IntType) getMutationPrio(target *Target, arg Arg, + ignoreSpecial, ignoreLengths bool) (prio float64, stopRecursion bool) { // For a integer without a range of values, the priority is based on // the number of bits occupied by the underlying type. plainPrio := math.Log2(float64(t.TypeBitSize())) + 0.1*maxPriority @@ -650,14 +662,16 @@ func (t *IntType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) ( return prio, false } -func (t *StructType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) (prio float64, stopRecursion bool) { +func (t *StructType) getMutationPrio(target *Target, arg Arg, + ignoreSpecial, ignoreLengths bool) (prio float64, stopRecursion bool) { if target.SpecialTypes[t.Name()] == nil || ignoreSpecial { return dontMutate, false } return maxPriority, true } -func (t *UnionType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) (prio float64, stopRecursion bool) { +func (t *UnionType) getMutationPrio(target *Target, arg Arg, + ignoreSpecial, ignoreLengths bool) (prio float64, stopRecursion bool) { if target.SpecialTypes[t.Name()] == nil && len(t.Fields) == 1 || ignoreSpecial { return dontMutate, false } @@ -669,7 +683,8 @@ func (t *UnionType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) return maxPriority, true } -func (t *FlagsType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) (prio float64, stopRecursion bool) { +func (t *FlagsType) getMutationPrio(target *Target, arg Arg, + ignoreSpecial, ignoreLengths bool) (prio float64, stopRecursion bool) { prio = rangeSizePrio(uint64(len(t.Vals))) if t.BitMask { // We want a higher priority because the mutation will include @@ -694,7 +709,8 @@ func rangeSizePrio(size uint64) (prio float64) { return prio } -func (t *PtrType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) (prio float64, stopRecursion bool) { +func (t *PtrType) getMutationPrio(target *Target, arg Arg, + ignoreSpecial, ignoreLengths bool) (prio float64, stopRecursion bool) { if arg.(*PointerArg).IsSpecial() { // TODO: we ought to mutate this, but we don't have code for this yet. return dontMutate, false @@ -702,32 +718,42 @@ func (t *PtrType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) ( return 0.3 * maxPriority, false } -func (t *ConstType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) (prio float64, stopRecursion bool) { +func (t *ConstType) getMutationPrio(target *Target, arg Arg, + ignoreSpecial, ignoreLengths bool) (prio float64, stopRecursion bool) { return dontMutate, false } -func (t *CsumType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) (prio float64, stopRecursion bool) { +func (t *CsumType) getMutationPrio(target *Target, arg Arg, + ignoreSpecial, ignoreLengths bool) (prio float64, stopRecursion bool) { return dontMutate, false } -func (t *ProcType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) (prio float64, stopRecursion bool) { +func (t *ProcType) getMutationPrio(target *Target, arg Arg, + ignoreSpecial, ignoreLengths bool) (prio float64, stopRecursion bool) { return 0.5 * maxPriority, false } -func (t *ResourceType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) (prio float64, stopRecursion bool) { +func (t *ResourceType) getMutationPrio(target *Target, arg Arg, + ignoreSpecial, ignoreLengths bool) (prio float64, stopRecursion bool) { return 0.5 * maxPriority, false } -func (t *VmaType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) (prio float64, stopRecursion bool) { +func (t *VmaType) getMutationPrio(target *Target, arg Arg, + ignoreSpecial, ignoreLengths bool) (prio float64, stopRecursion bool) { return 0.5 * maxPriority, false } -func (t *LenType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) (prio float64, stopRecursion bool) { +func (t *LenType) getMutationPrio(target *Target, arg Arg, + ignoreSpecial, ignoreLengths bool) (prio float64, stopRecursion bool) { // Mutating LenType only produces "incorrect" results according to descriptions. + if ignoreLengths { + return dontMutate, false + } return 0.1 * maxPriority, false } -func (t *BufferType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) (prio float64, stopRecursion bool) { +func (t *BufferType) getMutationPrio(target *Target, arg Arg, + ignoreSpecial, ignoreLengths bool) (prio float64, stopRecursion bool) { if arg.Dir() == DirOut && !t.Varlen() { return dontMutate, false } @@ -742,7 +768,8 @@ func (t *BufferType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool return 0.8 * maxPriority, false } -func (t *ArrayType) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) (prio float64, stopRecursion bool) { +func (t *ArrayType) getMutationPrio(target *Target, arg Arg, + ignoreSpecial, ignoreLengths bool) (prio float64, stopRecursion bool) { if t.Kind == ArrayRangeLen && t.RangeBegin == t.RangeEnd { return dontMutate, false } diff --git a/prog/rand.go b/prog/rand.go index 957cf711249a..d54ef0dfee9e 100644 --- a/prog/rand.go +++ b/prog/rand.go @@ -21,6 +21,10 @@ const ( // "Recommended" max number of calls in programs. // If we receive longer programs from hub/corpus we discard them. MaxCalls = 40 + // "Recommended" number of calls in KFuzzTest mode. These targets test the behavior + // of internal kernel functions rather than system behavior, and for this reason + // it is more sensible to generate a smaller number of calls instead of long chains. + RecommendedCallsKFuzzTest = 5 ) type randGen struct { @@ -28,6 +32,7 @@ type randGen struct { target *Target inGenerateResource bool patchConditionalDepth int + genKFuzzTest bool recDepth map[string]int } @@ -354,7 +359,9 @@ func (r *randGen) randString(s *state, t *BufferType) []byte { buf.Write([]byte{byte(r.Intn(256))}) } } - if r.oneOf(100) == t.NoZ { + // We always null-terminate strings that are inputs to KFuzzTest calls to + // avoid false-positive buffer overflow reports. + if r.oneOf(100) == t.NoZ || r.genKFuzzTest { buf.Write([]byte{0}) } return buf.Bytes() @@ -609,6 +616,16 @@ func (r *randGen) generateParticularCall(s *state, meta *Syscall) (calls []*Call panic(fmt.Sprintf("generating no_generate call: %v", meta.Name)) } c := MakeCall(meta, nil) + // KFuzzTest calls restrict mutation and generation. Since calls to + // generateParticularCall can be recursive, we save the previous value, and + // set it true. + if c.Meta.Attrs.KFuzzTest { + tmp := r.genKFuzzTest + r.genKFuzzTest = true + defer func() { + r.genKFuzzTest = tmp + }() + } c.Args, calls = r.generateArgs(s, meta.Args, DirIn) moreCalls, _ := r.patchConditionalFields(c, s) r.target.assignSizesCall(c) diff --git a/prog/target.go b/prog/target.go index 11127046bda5..300a86a32656 100644 --- a/prog/target.go +++ b/prog/target.go @@ -127,6 +127,17 @@ func AllTargets() []*Target { return res } +// Extend extends a target with a new set of syscalls, types, and resources. +// It is assumed that all new syscalls, types, and resources do not conflict +// with those already present in the target. +func (target *Target) Extend(syscalls []*Syscall, types []Type, resources []*ResourceDesc) { + target.Syscalls = append(target.Syscalls, syscalls...) + target.Types = append(target.Types, types...) + target.Resources = append(target.Resources, resources...) + // Updates the system call map and restores any links. + target.initTarget() +} + func (target *Target) lazyInit() { target.Neutralize = func(c *Call, fixStructure bool) error { return nil } target.AnnotateCall = func(c ExecCall) string { return "" } @@ -135,6 +146,10 @@ func (target *Target) lazyInit() { target.initUselessHints() target.initRelatedFields() target.initArch(target) + // We ignore the return value here as they are cached, and it makes more + // sense to react to them when we attempt to execute a KFuzzTest call. + _, _ = target.KFuzzTestRunID() + // Give these 2 known addresses fixed positions and prepend target-specific ones at the end. target.SpecialPointers = append([]uint64{ 0x0000000000000000, // NULL pointer (keep this first because code uses special index=0 as NULL) @@ -153,8 +168,6 @@ func (target *Target) lazyInit() { panic(fmt.Sprintf("bad special file length %v", ln)) } } - // These are used only during lazyInit. - target.Types = nil } func (target *Target) initTarget() { @@ -522,3 +535,24 @@ func (pg *Builder) Finalize() (*Prog, error) { pg.p = nil return p, nil } + +var kFuzzTestIDCache struct { + sync.Once + id int + err error +} + +// KFuzzTestRunID returns the ID for the syz_kfuzztest_run pseudo-syscall, +// or an error if it is not found in the target. +func (t *Target) KFuzzTestRunID() (int, error) { + kFuzzTestIDCache.Do(func() { + for _, call := range t.Syscalls { + if call.Attrs.KFuzzTest { + kFuzzTestIDCache.id = call.ID + return + } + } + kFuzzTestIDCache.err = fmt.Errorf("could not find ID for syz_kfuzztest_run - does it exist?") + }) + return kFuzzTestIDCache.id, kFuzzTestIDCache.err +} diff --git a/prog/types.go b/prog/types.go index 5a170dad105c..2329c348f82d 100644 --- a/prog/types.go +++ b/prog/types.go @@ -48,6 +48,7 @@ type SyscallAttrs struct { RemoteCover bool Automatic bool AutomaticHelper bool + KFuzzTest bool Fsck string // Filesystem is used in tools/syz-imagegen when fs name cannot be deduced from // the part after $. @@ -193,7 +194,7 @@ type Type interface { isDefaultArg(arg Arg) bool generate(r *randGen, s *state, dir Dir) (arg Arg, calls []*Call) mutate(r *randGen, s *state, arg Arg, ctx ArgCtx) (calls []*Call, retry, preserve bool) - getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) (prio float64, stopRecursion bool) + getMutationPrio(target *Target, arg Arg, ignoreSpecial, ignoreLengths bool) (prio float64, stopRecursion bool) minimize(ctx *minimizeArgsCtx, arg Arg, path string) bool ref() Ref setRef(ref Ref) @@ -223,7 +224,7 @@ func (ti Ref) generate(r *randGen, s *state, dir Dir) (Arg, []*Call) { panic("pr func (ti Ref) mutate(r *randGen, s *state, arg Arg, ctx ArgCtx) ([]*Call, bool, bool) { panic("prog.Ref method called") } -func (ti Ref) getMutationPrio(target *Target, arg Arg, ignoreSpecial bool) (float64, bool) { +func (ti Ref) getMutationPrio(target *Target, arg Arg, ignoreSpecial, ignoreLengths bool) (float64, bool) { panic("prog.Ref method called") } func (ti Ref) minimize(ctx *minimizeArgsCtx, arg Arg, path string) bool { diff --git a/sys/linux/kfuzztest.txt b/sys/linux/kfuzztest.txt new file mode 100644 index 000000000000..094d50ac024b --- /dev/null +++ b/sys/linux/kfuzztest.txt @@ -0,0 +1,4 @@ +# 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. + +syz_kfuzztest_run(name ptr[in, string], data ptr[in, array[int8]], len bytesize[data], buf ptr[in, array[int8, 65536]]) (kfuzz_test, no_generate) diff --git a/syz-kfuzztest/main.go b/syz-kfuzztest/main.go new file mode 100644 index 000000000000..e46ecc257c9a --- /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) +} diff --git a/syz-manager/manager.go b/syz-manager/manager.go index 3f94bd23a714..67af3bb29fc2 100644 --- a/syz-manager/manager.go +++ b/syz-manager/manager.go @@ -15,6 +15,7 @@ import ( "net" "os" "os/exec" + "path" "path/filepath" "sort" "sync" @@ -31,6 +32,7 @@ import ( "github.com/google/syzkaller/pkg/gce" "github.com/google/syzkaller/pkg/ifaceprobe" "github.com/google/syzkaller/pkg/image" + "github.com/google/syzkaller/pkg/kfuzztest" "github.com/google/syzkaller/pkg/log" "github.com/google/syzkaller/pkg/manager" "github.com/google/syzkaller/pkg/mgrconfig" @@ -241,6 +243,14 @@ func main() { cfg.DashboardClient = "" cfg.HubClient = "" } + if cfg.Experimental.EnableKFuzzTest { + vmLinuxPath := path.Join(cfg.KernelObj, cfg.SysTarget.KernelObject) + log.Log(0, "enabling KFuzzTest targets") + _, err := kfuzztest.ActivateKFuzzTargets(cfg.Target, vmLinuxPath) + if err != nil { + log.Fatalf("failed to enable KFuzzTest targets: %v", err) + } + } RunManager(mode, cfg) } @@ -1113,6 +1123,22 @@ func (mgr *Manager) MachineChecked(features flatrpc.Feature, mgr.exit(mgr.mode.Name) } + // If KFuzzTest is enabled, we exclusively fuzz KFuzzTest targets - so + // delete any existing entries in enabled syscalls, and enable all + // discovered KFuzzTest targets explicitly. + if mgr.cfg.Experimental.EnableKFuzzTest { + for call := range enabledSyscalls { + delete(enabledSyscalls, call) + } + data, err := kfuzztest.ExtractData(path.Join(mgr.cfg.KernelObj, "vmlinux")) + if err != nil { + return nil, err + } + for _, call := range data.Calls { + enabledSyscalls[call] = true + } + } + mgr.mu.Lock() defer mgr.mu.Unlock() if mgr.phase != phaseInit { @@ -1160,6 +1186,7 @@ func (mgr *Manager) MachineChecked(features flatrpc.Feature, defer mgr.mu.Unlock() return !mgr.saturatedCalls[call] }, + ModeKFuzzTest: mgr.cfg.Experimental.EnableKFuzzTest, }, rnd, mgr.target) fuzzerObj.AddCandidates(candidates) mgr.fuzzer.Store(fuzzerObj) diff --git a/tools/kfuzztest-gen/main.go b/tools/kfuzztest-gen/main.go new file mode 100644 index 000000000000..c469c39161dd --- /dev/null +++ b/tools/kfuzztest-gen/main.go @@ -0,0 +1,47 @@ +// 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. + +// Small tool for systematically outputting syzlang descriptions of KFuzzTest +// of a vmlinux binary. +package main + +import ( + "fmt" + "io" + "os" + + "github.com/google/syzkaller/pkg/kfuzztest" + "github.com/google/syzkaller/pkg/log" + "github.com/google/syzkaller/pkg/tool" +) + +func main() { + usage := func(w io.Writer) { + fmt.Fprintln(w, "usage: ./kfuzztest-gen /path/to/vmlinux") + } + if len(os.Args) != 2 { + usage(os.Stderr) + os.Exit(1) + } + + extractor, err := kfuzztest.NewExtractor(os.Args[1]) + if err != nil { + tool.Fail(err) + } + defer extractor.Close() + + log.Log(0, "extracting ELF data") + res, err := extractor.ExtractAll() + if err != nil { + tool.Fail(err) + } + log.Log(0, res.String()) + + builder := kfuzztest.NewBuilder(res.Funcs, res.Structs, res.Constraints, res.Annotations) + desc, err := builder.EmitSyzlangDescription() + if err != nil { + tool.Fail(err) + } + log.Logf(0, "emitting syzlang description") + fmt.Println(desc) +} diff --git a/tools/syz-prog2c/prog2c.go b/tools/syz-prog2c/prog2c.go index 27476ffbfcfc..f5402041d30c 100644 --- a/tools/syz-prog2c/prog2c.go +++ b/tools/syz-prog2c/prog2c.go @@ -11,6 +11,7 @@ import ( "runtime" "github.com/google/syzkaller/pkg/csource" + "github.com/google/syzkaller/pkg/kfuzztest" "github.com/google/syzkaller/prog" _ "github.com/google/syzkaller/sys" ) @@ -33,6 +34,7 @@ var ( flagLeak = flag.Bool("leak", false, "do leak checking") flagEnable = flag.String("enable", "none", "enable only listed additional features") flagDisable = flag.String("disable", "none", "enable all additional features except listed") + flagVmlinux = flag.String("vmlinux", "", "path to vmlinux binary (required for dynamically discovered calls") ) func main() { @@ -54,6 +56,13 @@ func main() { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } + if *flagVmlinux != "" { + _, err = kfuzztest.ActivateKFuzzTargets(target, *flagVmlinux) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + } data, err := os.ReadFile(*flagProg) if err != nil { fmt.Fprintf(os.Stderr, "failed to read prog file: %v\n", err)