github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/kcov/kcov.go (about)

     1  // Copyright 2025 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  //go:build linux
     5  
     6  // Package kcov provides Go native code for collecting kernel coverage (KCOV)
     7  // information.
     8  package kcov
     9  
    10  import (
    11  	"os"
    12  	"runtime"
    13  	"sync/atomic"
    14  	"unsafe"
    15  
    16  	"golang.org/x/sys/unix"
    17  )
    18  
    19  const (
    20  	kcovPath = "/sys/kernel/debug/kcov"
    21  	// This is the same value used by the linux executor, see executor_linux.h.
    22  	kcovCoverSize = 512 << 10
    23  )
    24  
    25  // Holds resources for a single traced thread.
    26  type KCOVState struct {
    27  	file  *os.File
    28  	cover []byte
    29  }
    30  
    31  type KCOVTraceResult struct {
    32  	Result   error     // Result of the call.
    33  	Coverage []uintptr // Collected program counters.
    34  }
    35  
    36  // Trace invokes `f` and returns a KCOVTraceResult.
    37  func (st *KCOVState) Trace(f func() error) KCOVTraceResult {
    38  	// First 8 bytes holds the number of collected PCs since last poll.
    39  	countPtr := (*uintptr)(unsafe.Pointer(&st.cover[0]))
    40  	// Reset coverage for this run.
    41  	atomic.StoreUintptr(countPtr, 0)
    42  	// Trigger call.
    43  	err := f()
    44  	// Load the number of PCs that were hit during trigger.
    45  	n := atomic.LoadUintptr(countPtr)
    46  
    47  	pcDataPtr := (*uintptr)(unsafe.Pointer(&st.cover[sizeofUintPtr]))
    48  	pcs := unsafe.Slice(pcDataPtr, n)
    49  	pcsCopy := make([]uintptr, n)
    50  	copy(pcsCopy, pcs)
    51  	return KCOVTraceResult{Result: err, Coverage: pcsCopy}
    52  }
    53  
    54  // EnableTracingForCurrentGoroutine prepares the current goroutine for kcov tracing.
    55  // It must be paired with a call to DisableTracing.
    56  func EnableTracingForCurrentGoroutine() (st *KCOVState, err error) {
    57  	st = &KCOVState{}
    58  	defer func() {
    59  		if err != nil {
    60  			// The original error is more important, so we ignore any potential
    61  			// errors that result from cleaning up.
    62  			_ = st.DisableTracing()
    63  		}
    64  	}()
    65  
    66  	// KCOV is per-thread, so lock goroutine to its current OS thread.
    67  	runtime.LockOSThread()
    68  
    69  	file, err := os.OpenFile(kcovPath, os.O_RDWR, 0)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  	st.file = file
    74  
    75  	// Setup trace mode and size.
    76  	if err := unix.IoctlSetInt(int(st.file.Fd()), uint(kcovInitTrace), kcovCoverSize); err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	// Mmap buffer shared between kernel- and user-space. For more information,
    81  	// see the Linux KCOV documentation: https://docs.kernel.org/dev-tools/kcov.html.
    82  	st.cover, err = unix.Mmap(
    83  		int(st.file.Fd()),
    84  		0, // Offset.
    85  		kcovCoverSize*sizeofUintPtr,
    86  		unix.PROT_READ|unix.PROT_WRITE,
    87  		unix.MAP_SHARED,
    88  	)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	// Enable coverage collection on the current thread.
    94  	if err := unix.IoctlSetInt(int(st.file.Fd()), uint(kcovEnable), kcovTracePC); err != nil {
    95  		return nil, err
    96  	}
    97  	return st, nil
    98  }
    99  
   100  // DisableTracing disables KCOV tracing for the current Go routine. On failure,
   101  // it returns the first error that occurred during cleanup.
   102  func (st *KCOVState) DisableTracing() error {
   103  	var firstErr error
   104  	if err := unix.IoctlSetInt(int(st.file.Fd()), uint(kcovDisable), kcovTracePC); err != nil {
   105  		firstErr = err
   106  	}
   107  	if err := unix.Munmap(st.cover); err != nil && firstErr == nil {
   108  		firstErr = err
   109  	}
   110  	if err := st.file.Close(); err != nil && firstErr == nil {
   111  		firstErr = err
   112  	}
   113  	runtime.UnlockOSThread()
   114  	return firstErr
   115  }