1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
|
package tracefs
import (
"crypto/rand"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"github.com/cilium/ebpf/internal"
"github.com/cilium/ebpf/internal/linux"
"github.com/cilium/ebpf/internal/unix"
)
var (
ErrInvalidInput = errors.New("invalid input")
ErrInvalidMaxActive = errors.New("can only set maxactive on kretprobes")
)
//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=ProbeType -linecomment
type ProbeType uint8
const (
Kprobe ProbeType = iota // kprobe
Uprobe // uprobe
)
func (pt ProbeType) eventsFile() (*os.File, error) {
path, err := sanitizeTracefsPath(fmt.Sprintf("%s_events", pt.String()))
if err != nil {
return nil, err
}
return os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0666)
}
type ProbeArgs struct {
Type ProbeType
Symbol, Group, Path string
Offset, RefCtrOffset, Cookie uint64
Pid, RetprobeMaxActive int
Ret bool
}
// RandomGroup generates a pseudorandom string for use as a tracefs group name.
// Returns an error when the output string would exceed 63 characters (kernel
// limitation), when rand.Read() fails or when prefix contains characters not
// allowed by IsValidTraceID.
func RandomGroup(prefix string) (string, error) {
if !validIdentifier(prefix) {
return "", fmt.Errorf("prefix '%s' must be alphanumeric or underscore: %w", prefix, ErrInvalidInput)
}
b := make([]byte, 8)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("reading random bytes: %w", err)
}
group := fmt.Sprintf("%s_%x", prefix, b)
if len(group) > 63 {
return "", fmt.Errorf("group name '%s' cannot be longer than 63 characters: %w", group, ErrInvalidInput)
}
return group, nil
}
// validIdentifier implements the equivalent of a regex match
// against "^[a-zA-Z_][0-9a-zA-Z_]*$".
//
// Trace event groups, names and kernel symbols must adhere to this set
// of characters. Non-empty, first character must not be a number, all
// characters must be alphanumeric or underscore.
func validIdentifier(s string) bool {
if len(s) < 1 {
return false
}
for i, c := range []byte(s) {
switch {
case c >= 'a' && c <= 'z':
case c >= 'A' && c <= 'Z':
case c == '_':
case i > 0 && c >= '0' && c <= '9':
default:
return false
}
}
return true
}
func sanitizeTracefsPath(path ...string) (string, error) {
base, err := getTracefsPath()
if err != nil {
return "", err
}
l := filepath.Join(path...)
p := filepath.Join(base, l)
if !strings.HasPrefix(p, base) {
return "", fmt.Errorf("path '%s' attempts to escape base path '%s': %w", l, base, ErrInvalidInput)
}
return p, nil
}
// getTracefsPath will return a correct path to the tracefs mount point.
// Since kernel 4.1 tracefs should be mounted by default at /sys/kernel/tracing,
// but may be also be available at /sys/kernel/debug/tracing if debugfs is mounted.
// The available tracefs paths will depends on distribution choices.
var getTracefsPath = sync.OnceValues(func() (string, error) {
if !internal.OnLinux {
return "", fmt.Errorf("tracefs: %w", internal.ErrNotSupportedOnOS)
}
for _, p := range []struct {
path string
fsType int64
}{
{"/sys/kernel/tracing", unix.TRACEFS_MAGIC},
{"/sys/kernel/debug/tracing", unix.TRACEFS_MAGIC},
// RHEL/CentOS
{"/sys/kernel/debug/tracing", unix.DEBUGFS_MAGIC},
} {
if fsType, err := linux.FSType(p.path); err == nil && fsType == p.fsType {
return p.path, nil
}
}
return "", errors.New("neither debugfs nor tracefs are mounted")
})
// sanitizeIdentifier replaces every invalid character for the tracefs api with an underscore.
//
// It is equivalent to calling regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString("_").
func sanitizeIdentifier(s string) string {
var skip bool
return strings.Map(func(c rune) rune {
switch {
case c >= 'a' && c <= 'z',
c >= 'A' && c <= 'Z',
c >= '0' && c <= '9':
skip = false
return c
case skip:
return -1
default:
skip = true
return '_'
}
}, s)
}
// EventID reads a trace event's ID from tracefs given its group and name.
// The kernel requires group and name to be alphanumeric or underscore.
func EventID(group, name string) (uint64, error) {
if !validIdentifier(group) {
return 0, fmt.Errorf("invalid tracefs group: %q", group)
}
if !validIdentifier(name) {
return 0, fmt.Errorf("invalid tracefs name: %q", name)
}
path, err := sanitizeTracefsPath("events", group, name, "id")
if err != nil {
return 0, err
}
tid, err := internal.ReadUint64FromFile("%d\n", path)
if errors.Is(err, os.ErrNotExist) {
return 0, err
}
if err != nil {
return 0, fmt.Errorf("reading trace event ID of %s/%s: %w", group, name, err)
}
return tid, nil
}
func probePrefix(ret bool, maxActive int) string {
if ret {
if maxActive > 0 {
return fmt.Sprintf("r%d", maxActive)
}
return "r"
}
return "p"
}
// Event represents an entry in a tracefs probe events file.
type Event struct {
typ ProbeType
group, name string
// event id allocated by the kernel. 0 if the event has already been removed.
id uint64
}
// NewEvent creates a new ephemeral trace event.
//
// Returns os.ErrNotExist if symbol is not a valid
// kernel symbol, or if it is not traceable with kprobes. Returns os.ErrExist
// if a probe with the same group and symbol already exists. Returns an error if
// args.RetprobeMaxActive is used on non kprobe types. Returns ErrNotSupported if
// the kernel is too old to support kretprobe maxactive.
func NewEvent(args ProbeArgs) (*Event, error) {
// Before attempting to create a trace event through tracefs,
// check if an event with the same group and name already exists.
// Kernels 4.x and earlier don't return os.ErrExist on writing a duplicate
// entry, so we need to rely on reads for detecting uniqueness.
eventName := sanitizeIdentifier(args.Symbol)
_, err := EventID(args.Group, eventName)
if err == nil {
return nil, fmt.Errorf("trace event %s/%s: %w", args.Group, eventName, os.ErrExist)
}
if errors.Is(err, unix.EINVAL) {
return nil, fmt.Errorf("trace event %s/%s: %w (unknown symbol?)", args.Group, eventName, err)
}
if !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("checking trace event %s/%s: %w", args.Group, eventName, err)
}
// Open the kprobe_events file in tracefs.
f, err := args.Type.eventsFile()
if err != nil {
return nil, err
}
defer f.Close()
var pe, token string
switch args.Type {
case Kprobe:
// The kprobe_events syntax is as follows (see Documentation/trace/kprobetrace.txt):
// p[:[GRP/]EVENT] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS] : Set a probe
// r[MAXACTIVE][:[GRP/]EVENT] [MOD:]SYM[+0] [FETCHARGS] : Set a return probe
// -:[GRP/]EVENT : Clear a probe
//
// Some examples:
// r:ebpf_1234/r_my_kretprobe nf_conntrack_destroy
// p:ebpf_5678/p_my_kprobe __x64_sys_execve
//
// Leaving the kretprobe's MAXACTIVE set to 0 (or absent) will make the
// kernel default to NR_CPUS. This is desired in most eBPF cases since
// subsampling or rate limiting logic can be more accurately implemented in
// the eBPF program itself.
// See Documentation/kprobes.txt for more details.
if args.RetprobeMaxActive != 0 && !args.Ret {
return nil, ErrInvalidMaxActive
}
token = KprobeToken(args)
pe = fmt.Sprintf("%s:%s/%s %s", probePrefix(args.Ret, args.RetprobeMaxActive), args.Group, eventName, token)
case Uprobe:
// The uprobe_events syntax is as follows:
// p[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a probe
// r[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a return probe
// -:[GRP/]EVENT : Clear a probe
//
// Some examples:
// r:ebpf_1234/readline /bin/bash:0x12345
// p:ebpf_5678/main_mySymbol /bin/mybin:0x12345(0x123)
//
// See Documentation/trace/uprobetracer.txt for more details.
if args.RetprobeMaxActive != 0 {
return nil, ErrInvalidMaxActive
}
token = UprobeToken(args)
pe = fmt.Sprintf("%s:%s/%s %s", probePrefix(args.Ret, 0), args.Group, eventName, token)
}
_, err = f.WriteString(pe)
// Since commit 97c753e62e6c, ENOENT is correctly returned instead of EINVAL
// when trying to create a retprobe for a missing symbol.
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("token %s: not found: %w", token, err)
}
// Since commit ab105a4fb894, EILSEQ is returned when a kprobe sym+offset is resolved
// to an invalid insn boundary. The exact conditions that trigger this error are
// arch specific however.
if errors.Is(err, syscall.EILSEQ) {
return nil, fmt.Errorf("token %s: bad insn boundary: %w", token, os.ErrNotExist)
}
// ERANGE is returned when the `SYM[+offs]` token is too big and cannot
// be resolved.
if errors.Is(err, syscall.ERANGE) {
return nil, fmt.Errorf("token %s: offset too big: %w", token, os.ErrNotExist)
}
if err != nil {
return nil, fmt.Errorf("token %s: writing '%s': %w", token, pe, err)
}
// Get the newly-created trace event's id.
tid, err := EventID(args.Group, eventName)
if args.RetprobeMaxActive != 0 && errors.Is(err, os.ErrNotExist) {
// Kernels < 4.12 don't support maxactive and therefore auto generate
// group and event names from the symbol and offset. The symbol is used
// without any sanitization.
// See https://elixir.bootlin.com/linux/v4.10/source/kernel/trace/trace_kprobe.c#L712
event := fmt.Sprintf("kprobes/r_%s_%d", args.Symbol, args.Offset)
if err := removeEvent(args.Type, event); err != nil {
return nil, fmt.Errorf("failed to remove spurious maxactive event: %s", err)
}
return nil, fmt.Errorf("create trace event with non-default maxactive: %w", internal.ErrNotSupported)
}
if err != nil {
return nil, fmt.Errorf("get trace event id: %w", err)
}
evt := &Event{args.Type, args.Group, eventName, tid}
runtime.SetFinalizer(evt, (*Event).Close)
return evt, nil
}
// Close removes the event from tracefs.
//
// Returns os.ErrClosed if the event has already been closed before.
func (evt *Event) Close() error {
if evt.id == 0 {
return os.ErrClosed
}
evt.id = 0
runtime.SetFinalizer(evt, nil)
pe := fmt.Sprintf("%s/%s", evt.group, evt.name)
return removeEvent(evt.typ, pe)
}
func removeEvent(typ ProbeType, pe string) error {
f, err := typ.eventsFile()
if err != nil {
return err
}
defer f.Close()
// See [k,u]probe_events syntax above. The probe type does not need to be specified
// for removals.
if _, err = f.WriteString("-:" + pe); err != nil {
return fmt.Errorf("remove event %q from %s: %w", pe, f.Name(), err)
}
return nil
}
// ID returns the tracefs ID associated with the event.
func (evt *Event) ID() uint64 {
return evt.id
}
// Group returns the tracefs group used by the event.
func (evt *Event) Group() string {
return evt.group
}
// KprobeToken creates the SYM[+offs] token for the tracefs api.
func KprobeToken(args ProbeArgs) string {
po := args.Symbol
if args.Offset != 0 {
po += fmt.Sprintf("+%#x", args.Offset)
}
return po
}
|