diff options
| -rw-r--r-- | pkg/kcov/cdefs.go | 45 | ||||
| -rw-r--r-- | pkg/kcov/kcov.go | 115 |
2 files changed, 160 insertions, 0 deletions
diff --git a/pkg/kcov/cdefs.go b/pkg/kcov/cdefs.go new file mode 100644 index 000000000..1272360b9 --- /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 000000000..0400a32ff --- /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 +} |
