diff --git a/LICENSES/github.com/mdlayher/kobject/LICENSE.md b/LICENSES/github.com/mdlayher/kobject/LICENSE.md new file mode 100644 index 000000000..ffcdf89c9 --- /dev/null +++ b/LICENSES/github.com/mdlayher/kobject/LICENSE.md @@ -0,0 +1,10 @@ +MIT License +=========== + +Copyright (C) 2017 Matt Layher + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/go.mod b/go.mod index 4c69c9f2d..3f5ac3202 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/google/uuid v1.6.0 github.com/jsimonetti/rtnetlink/v2 v2.0.3 github.com/klauspost/compress v1.18.0 + github.com/mdlayher/kobject v0.0.0-20200520190114-19ca17470d7d github.com/minio/sha256-simd v1.0.1 github.com/peterbourgon/ff/v3 v3.4.0 github.com/sirupsen/logrus v1.9.3 diff --git a/go.sum b/go.sum index f3fd1bd2d..2f90cc198 100644 --- a/go.sum +++ b/go.sum @@ -92,6 +92,10 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -101,6 +105,8 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= +github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/jsimonetti/rtnetlink/v2 v2.0.3 h1:Jcp7GTnTPepoUAJ9+LhTa7ZiebvNS56T1GtlEUaPNFE= github.com/jsimonetti/rtnetlink/v2 v2.0.3/go.mod h1:atIkksp/9fqtf6rpAw45JnttnP2gtuH9X88WPfWfS9A= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -115,6 +121,11 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mdlayher/kobject v0.0.0-20200520190114-19ca17470d7d h1:JmrZTpS0GAyMV4ZQVVH/AS0Y6r2PbnYNSRUuRX+HOLA= +github.com/mdlayher/kobject v0.0.0-20200520190114-19ca17470d7d/go.mod h1:+SexPO1ZvdbbWUdUnyXEWv3+4NwHZjKhxOmQqHY4Pqc= +github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= +github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= +github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= @@ -248,8 +259,12 @@ golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdR golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= @@ -262,7 +277,12 @@ golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/kallsyms/kallsyms.go b/kallsyms/kallsyms.go new file mode 100644 index 000000000..943fb2f78 --- /dev/null +++ b/kallsyms/kallsyms.go @@ -0,0 +1,672 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package kallsyms provides functionality for reading /proc/kallsyms +// and using it to symbolize kernel addresses. +package kallsyms // import "go.opentelemetry.io/ebpf-profiler/kallsyms" + +import ( + "bufio" + "bytes" + "context" + "errors" + "fmt" + "hash/fnv" + "io" + "os" + "path" + "slices" + "sort" + "strconv" + "strings" + "sync/atomic" + "time" + "unsafe" + + "github.com/mdlayher/kobject" + log "github.com/sirupsen/logrus" + + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" + "go.opentelemetry.io/ebpf-profiler/stringutil" +) + +// Kernel is the internal name for "module" containing the built-in symbols +const Kernel = "vmlinux" + +// pointerBits is the number of bits for pointer. Used to validate data +// from the kernel kallsyms file. +const pointerBits = int(unsafe.Sizeof(libpf.Address(0)) * 8) + +// sysModule is the sysfs path for module metadata +const sysModule = "/sys/module" + +var ErrSymbolPermissions = errors.New("unable to read kallsyms addresses - check capabilities") + +var ErrNoModule = errors.New("module not found") + +var ErrNoSymbol = errors.New("symbol not found") + +var ErrModuleStub = errors.New("symbols are not available yet - retry later") + +// symbol is the per-symbol structure. The size should be minimal as +// a typical installation has 100k-200k kernel symbols. +type symbol struct { + // offset is the symbol offset from the Module start address + offset uint32 + // index is the offset to the symbol name within the Module names slice + index uint32 +} + +// Module contains symbols and metadata for one kernel module. +type Module struct { + start libpf.Address + end libpf.Address + mtime int64 + stub bool + + buildID string + fileID libpf.FileID + names []byte + symbols []symbol +} + +// Symbolizer provides the main API for reading, updating and querying +// the kernel symbols. +type Symbolizer struct { + modules atomic.Value + + reloadModules chan bool +} + +// NewSymbolizer creates and returns a new kallsyms symbolizer and loads +// the initial 'kallsymbols'. +func NewSymbolizer() (*Symbolizer, error) { + s := &Symbolizer{ + reloadModules: make(chan bool, 1), + } + if err := s.loadKallsyms(); err != nil { + return nil, err + } + return s, nil +} + +// addName appends the 'name' to the module's string slice, and returns +// an index suitable for storing in the `symbol` struct. +func (m *Module) addName(name string) uint32 { + index := len(m.names) + l := len(name) + // Cap the length to 255 bytes so it fits a byte. Longest seen + // symbol so far is 83 bytes. + if l > 255 { + l = 255 + } + m.names = append(m.names, byte(l)) + m.names = append(m.names, unsafe.Slice(unsafe.StringData(name), l)...) + return uint32(index) +} + +// setStub makes this module a stub entry for given module name. +func (m *Module) setStub(name string) { + m.names = make([]byte, 0, len(name)) + m.addName(name) + m.stub = true +} + +// bytesAt recovers a []byte representation of the string at `index` +// received from previous `addName` call. +func (m *Module) bytesAt(index uint32) []byte { + i := int(index) + l := int(m.names[i]) + return m.names[i+1 : i+1+l] +} + +// stringAt recovers the string at `index` received from previous `addName` call. +func (m *Module) stringAt(index uint32) string { + return stringutil.ByteSlice2String(m.bytesAt(index)) +} + +// parseSysfsUint reads a kernel module specific attribute from sysfs. +func parseSysfsUint(mod, knob string) (uint64, error) { + text, err := os.ReadFile(path.Join(sysModule, mod, knob)) + if err != nil { + return 0, err + } + return strconv.ParseUint(strings.Trim(stringutil.ByteSlice2String(text), "\n"), 0, pointerBits) +} + +// loadModuleMetadata is the function to load module bounds and fileID data. +// Overridable for the test suite. Returns true if the metadata was loaded +// successfully. +var loadModuleMetadata = func(m *Module, name string, oldMtime int64) bool { + if name == "bpf" { + // Kernel reports the BPF JIT symbols as part of 'bpf' module. + // There is no metadata available. + return true + } + + // Determine notes location and module size + notesFile := "/sys/kernel/notes" + if name != Kernel { + info, err := os.Stat(path.Join(sysModule, name)) + if err != nil { + return false + } + m.mtime = info.ModTime().UnixMilli() + if m.mtime == oldMtime { + return false + } + + notesFile = path.Join(sysModule, name, "notes/.note.gnu.build-id") + addr, err := parseSysfsUint(name, "sections/.text") + if err != nil { + return false + } + size, err := parseSysfsUint(name, "coresize") + if err != nil { + return false + } + m.start = libpf.Address(addr) + m.end = m.start + libpf.Address(size) + } else { + // No need to reload kernel symbols + if m.mtime == 1 { + return false + } + m.mtime = 1 + } + + // Require at least 16 bytes of BuildID to ensure there is enough entropy for a FileID. + // 16 bytes could happen when --build-id=md5 is passed to `ld`. This would imply a custom + // kernel. + var err error + m.buildID, err = pfelf.GetBuildIDFromNotesFile(notesFile) + if err == nil && len(m.buildID) >= 16 { + m.fileID = libpf.FileIDFromKernelBuildID(m.buildID) + } + return true +} + +// finish will finalize the Module. The 'symbols' slice is sorted, and +// a fallback fileID is synthesized if buildID is not available. +func (m *Module) finish() { + if m.end == 0 || m.end == ^libpf.Address(0) { + // Synthesize the end address at last symbol rounded up to page size + // because it could not be reliably determined. + lastSymbol := m.start + libpf.Address(m.symbols[len(m.symbols)-1].offset) + m.end = (lastSymbol + 4095) & ^libpf.Address(4095) + } + + sort.Slice(m.symbols, func(i, j int) bool { + return m.symbols[i].offset >= m.symbols[j].offset + }) + + // Synthesize fileID if it was not available via /sys + if m.fileID.Compare(libpf.FileID{}) == 0 && len(m.symbols) > 0 { + // Hash exports and their normalized addresses. + h := fnv.New128a() + + h.Write(m.bytesAt(0)) // module name + size := uint64(m.end - m.start) + h.Write(libpf.SliceFrom(&size)) + + for _, sym := range m.symbols { + h.Write(m.bytesAt(sym.index)) + addr := uint64(sym.offset) + h.Write(libpf.SliceFrom(&addr)) + } + + var hash [16]byte + fileID, err := libpf.FileIDFromBytes(h.Sum(hash[:0])) + if err != nil { + panic("kernel module fallback fileID construction is broken") + } + + log.Debugf("Fallback module ID for module %s is '%s' (num syms: %d)", + m.Name(), fileID.Base64(), len(m.symbols)) + } +} + +func (m *Module) Name() string { + return m.stringAt(0) +} + +func (m *Module) Start() libpf.Address { + return m.start +} + +func (m *Module) End() libpf.Address { + return m.end +} + +func (m *Module) BuildID() string { + return m.buildID +} + +func (m *Module) FileID() libpf.FileID { + return m.fileID +} + +// LookupSymbolByAddress resolves the `pc` address to the function and offset from it. +// On error, an empty string with zero offset is returned. +func (m *Module) LookupSymbolByAddress(pc libpf.Address) (funcName string, offset uint, err error) { + if m.stub { + return "", 0, ErrModuleStub + } + pcOffs := uint32(pc - m.start) + symIdx := sort.Search(len(m.symbols), func(i int) bool { + return pcOffs >= m.symbols[i].offset + }) + if symIdx >= len(m.symbols) { + return "", 0, ErrNoSymbol + } + sym := &m.symbols[symIdx] + symName := m.stringAt(sym.index) + return symName, uint(pcOffs - sym.offset), nil +} + +// LookupSymbol finds a symbol with 'name' from the Module. +func (m *Module) LookupSymbol(name string) (libpf.Address, error) { + for _, sym := range m.symbols { + if m.stringAt(sym.index) == name { + return m.start + libpf.Address(sym.offset), nil + } + } + return 0, ErrNoSymbol +} + +// LookupSymbolsByPrefix finds all symbols with the given prefix in from the Module. +func (m *Module) LookupSymbolsByPrefix(prefix string) []*libpf.Symbol { + res := make([]*libpf.Symbol, 0, 8) + for _, sym := range m.symbols { + symName := m.stringAt(sym.index) + if strings.HasPrefix(symName, prefix) { + symAddr := m.start + libpf.Address(sym.offset) + res = append(res, &libpf.Symbol{ + Name: libpf.SymbolName(symName), + Address: libpf.SymbolValue(symAddr), + }) + } + } + return res +} + +// updateSymbolsFrom parses /proc/kallsyms format data from the reader 'r'. +// If possible the data from previous reads is re-used to avoid allocations. +// The Symbolizer internal state is updated only if the input data is parsed +// successfully. +func (s *Symbolizer) updateSymbolsFrom(r io.Reader) error { + var mod *Module + var curName string + var syms []symbol + var names []byte + + noSymbols := true + modules, _ := s.modules.Load().([]Module) + + // The kallsyms symbol order is the following: + // 1. kernel symbols (from compressed kallsyms) + // 2. kernel arch symbols (if any) + // 3. module symbols (grouped by module from all loaded modules) + // 4. module symbols ftrace cloned from __init section + // (all __init symbols ftrace traced during module load) + // 5. bpf module symbols (dynamically generated from JITted bpf programs) + // + // We load the per-module symbols from group #3 in one go. We also generally + // do not care about the symbols in group #4 as they are only the __init + // symbols after they have been freed. Trying to use these symbols is + // problematic: + // 1. the symbol data is normally not present at all + // 2. they are used during init only (getting traces with them is unlikely) + // 3. after the __init data is freed, the same VMA range can be reused for + // another newly loaded module. deciding afterwards if it was the now + // released __init symbol or the newly loaded module code is non-trivial. + // 4. loading these symbols means we would have potentially overlapping symbols. + // + // For the above reasons, it is better to just ignore these ftrace cloned + // __init symbols. This is done with the 'seen' set to avoid loading symbols + // for a module if has been already processed. + seen := make(libpf.Set[string]) + + // Allocate buffers which should be able to hold the symbol data + // from the vmlinux main image (or large modules on reloads) without + // resizing based on normal distribution kernel. These are later + // cloned to the exact size needed, so these are stack allocated. + + // The modules (typical systems have 200-300) + mods := make([]Module, 0, 400) + if len(modules) == 0 { + // - 2.5MB for symbol names + // - 100k symbols + names = make([]byte, 0, 3*1024*1024) + syms = make([]symbol, 0, 128*1024) + } else { + // - 0.5MB for symbol names (e.g. i915 needs 400k) + // - 64k symbols (e.g. i915 has 12k symbols) + names = make([]byte, 0, 512*1024) + syms = make([]symbol, 0, 64*1024) + + // Copy the static symbols here. The kallsyms often starts + // with symbols not within kernel .text, and the logic below + // would not correctly detect already seen kernel symbols. + for _, mod := range modules { + if mod.Name() == Kernel { + mods = append(mods, mod) + seen[Kernel] = libpf.Void{} + break + } + } + } + + for scanner := bufio.NewScanner(r); scanner.Scan(); { + // Avoid heap allocation by not using scanner.Text(). + // NOTE: The underlying bytes will change with the next call to scanner.Scan(), + // so make sure to not keep any references after the end of the loop iteration. + line := stringutil.ByteSlice2String(scanner.Bytes()) + + // Avoid heap allocations here - do not use strings.FieldsN() + var fields [4]string + nFields := stringutil.FieldsN(line, fields[:]) + if nFields < 3 { + return fmt.Errorf("unexpected line in kallsyms: '%s'", line) + } + + // Skip non-text symbols, see 'man nm'. + // Special case for 'etext', which can be of type `D` (data) in some kernels. + if strings.IndexByte("TtVvWw", fields[1][0]) == -1 && fields[2] != "_etext" { + continue + } + + address, err := strconv.ParseUint(fields[0], 16, pointerBits) + if err != nil { + return fmt.Errorf("failed to parse address value: '%s'", fields[0]) + } + if address != 0 { + noSymbols = false + } + + moduleName := Kernel + if fields[3] != "" { + moduleName = fields[3] + if moduleName[0] != '[' && moduleName[len(moduleName)-1] != ']' { + return fmt.Errorf("failed to parse module name: '%s'", moduleName) + } + moduleName = moduleName[1 : len(moduleName)-1] + } + + if curName != moduleName { + if curName == Kernel && noSymbols { + return ErrSymbolPermissions + } + if mod != nil && len(mod.symbols) > 0 { + // Update the working buffers from potentially reallocated + // slices to avoid continuous reallocations. + names = mod.names[0:0] + syms = mod.symbols[0:0] + // Clone a copy of the data to the module so that it does not + // overlap with the working buffer, and is sized exactly the + // needed size. + mod.names = bytes.Clone(mod.names) + mod.symbols = slices.Clone(mod.symbols) + mod.finish() + // Update seen map with the cloned module name string so + // it does not get overwritten later on. + seen[mod.Name()] = libpf.Void{} + } + mod = nil + + if _, ok := seen[moduleName]; !ok { + var oldMod *Module + var oldMtime int64 + newMod := Module{ + end: ^libpf.Address(0), + symbols: syms[0:0], + names: names[0:0], + } + if moduleName != "bpf" { + oldMod, _ = getModuleByAddress(modules, libpf.Address(address)) + if oldMod != nil && !oldMod.stub && oldMod.Name() == moduleName { + oldMtime = oldMod.mtime + } else { + oldMod = nil + } + } + if loadModuleMetadata(&newMod, moduleName, oldMtime) { + // Module metadata was updated. Parse this module symbols. + mods = append(mods, newMod) + mod = &mods[len(mods)-1] + mod.addName(moduleName) + curName = mod.Name() + } else if oldMod != nil { + // Reuse the existing module data if any. + mods = append(mods, *oldMod) + curName = oldMod.Name() + } + } + } + + if mod == nil { + continue + } + + switch fields[2] { + case "_stext", "_text": + if mod.start == 0 { + mod.start = libpf.Address(address) + } + case "_etext": + if mod.end == ^libpf.Address(0) { + mod.end = libpf.Address(address) + } + case "_sinittext", "_einittext": + default: + if mod.start == 0 { + mod.start = libpf.Address(address) + } + if addr := libpf.Address(address); addr >= mod.start && addr < mod.end { + // Add symbol to the module symbols + mod.symbols = append(mod.symbols, symbol{ + offset: uint32(addr - mod.start), + index: mod.addName(fields[2]), + }) + } + } + } + if mod != nil { + mod.finish() + } + if noSymbols { + return ErrSymbolPermissions + } + + sort.Slice(mods, func(i, j int) bool { + return mods[i].start >= mods[j].start + }) + // Heap allocate the exact amount needed. This also makes the initial + // buffer stack allocated. + s.modules.Store(slices.Clone(mods)) + return nil +} + +// loadKallsyms will reload kernel symbols. This function can run concurrently with +// module and symbol lookups. The reload result is visible atomically after success. +func (s *Symbolizer) loadKallsyms() error { + file, err := os.Open("/proc/kallsyms") + if err != nil { + return fmt.Errorf("unable to open kallsyms: %v", err) + } + defer file.Close() + + return s.updateSymbolsFrom(file) +} + +var nonsyfsModules = libpf.Set[string]{ + Kernel: libpf.Void{}, + "bpf": libpf.Void{}, +} + +// loadModules will reload module metadata. +func (s *Symbolizer) loadModules() (bool, error) { + dir, err := os.Open(sysModule) + if err != nil { + return false, err + } + defer dir.Close() + + needReloadSymbols := false + modules, _ := s.modules.Load().([]Module) + mods := make([]Module, 0, 400) + + // Copy the modules not present in sysfs + for _, mod := range modules { + if _, ok := nonsyfsModules[mod.Name()]; ok { + mods = append(mods, mod) + } + } + + // Scan sysfs for current module listing and its metadata + for { + dirEntries, err := dir.ReadDir(64) + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return false, err + } + for _, dirEnt := range dirEntries { + if !dirEnt.IsDir() { + continue + } + + moduleName := dirEnt.Name() + curMod := Module{} + if !loadModuleMetadata(&curMod, moduleName, 0) { + // sysfs contains directories also for statically built + // kernel modules. Ignore these. + continue + } + + oldMod, _ := getModuleByAddress(modules, curMod.start) + if oldMod != nil && oldMod.Name() == moduleName && oldMod.mtime == curMod.mtime { + // Reuse the old module + mods = append(mods, *oldMod) + } else { + // Create a stub module without symbols + curMod.setStub(moduleName) + mods = append(mods, curMod) + needReloadSymbols = true + } + } + } + + sort.Slice(mods, func(i, j int) bool { + return mods[i].start >= mods[j].start + }) + // Heap allocate the exact amount needed. This also makes the initial + // buffer stack allocated. + s.modules.Store(slices.Clone(mods)) + + return needReloadSymbols, nil +} + +// reloadWorker is the goroutine handling the reloads of the kallsyms. +func (s *Symbolizer) reloadWorker(ctx context.Context, kobjectClient *kobject.Client) { + noTimeout := make(<-chan time.Time) + nextKallsymsReload := noTimeout + nextModulesReload := noTimeout + for { + select { + case <-s.reloadModules: + // Just trigger reloading of modules with small delay to batch + // potentially multiple module loads. + if nextModulesReload == noTimeout { + nextModulesReload = time.After(100 * time.Millisecond) + } + case <-nextModulesReload: + if reloadSymbols, err := s.loadModules(); err == nil { + log.Debugf("Kernel modules metadata reloaded, new symbols: %v", reloadSymbols) + nextModulesReload = noTimeout + if reloadSymbols && nextKallsymsReload == noTimeout { + nextKallsymsReload = time.After(time.Minute) + } + } else { + log.Warnf("Failed to reload kernel modules metadata: %v", err) + nextModulesReload = time.After(10 * time.Second) + } + case <-nextKallsymsReload: + if err := s.loadKallsyms(); err == nil { + log.Debugf("Kernel symbols reloaded") + nextKallsymsReload = noTimeout + } else { + log.Warnf("Failed to reload kernel symbols: %v", err) + nextKallsymsReload = time.After(time.Minute) + } + case <-ctx.Done(): + // Terminate also the kobject poller thread + _ = kobjectClient.Close() + return + } + } +} + +// pollKobjectClient listens for kernel kobject events to reload kallsyms when needed. +func (s *Symbolizer) pollKobjectClient(kobjectClient *kobject.Client) { + for { + event, err := kobjectClient.Receive() + if err != nil { + return + } + if event.Subsystem == "module" { + log.Debugf("Kernel modules changed") + // Notify worker thread without blocking + select { + case s.reloadModules <- true: + default: + } + } + } +} + +// Reload will trigger asynchronous update of modules and symbols. +func (s *Symbolizer) StartMonitor(ctx context.Context) error { + kobjectClient, err := kobject.New() + if err != nil { + return fmt.Errorf("failed to create kobject netlink socket: %v", err) + } + go s.reloadWorker(ctx, kobjectClient) + go s.pollKobjectClient(kobjectClient) + return nil +} + +// getModuleByAddress is a helper to find a Module from the sorted 'modules' +// slice matching the address 'pc'. +func getModuleByAddress(modules []Module, pc libpf.Address) (*Module, error) { + modIdx := sort.Search(len(modules), func(i int) bool { + return pc >= modules[i].start + }) + if modIdx >= len(modules) { + return nil, ErrNoModule + } + m := &modules[modIdx] + if pc < m.start || pc >= m.end { + return nil, ErrNoModule + } + return m, nil +} + +// GetModuleByAddress finds the Module containing the address 'pc'. +func (s *Symbolizer) GetModuleByAddress(pc libpf.Address) (*Module, error) { + return getModuleByAddress(s.modules.Load().([]Module), pc) +} + +// GetModuleByAddress finds the Module containing the module 'module'. +func (s *Symbolizer) GetModuleByName(module string) (*Module, error) { + modules := s.modules.Load().([]Module) + for i := range modules { + kmod := &modules[i] + if kmod.Name() == module { + return kmod, nil + } + } + return nil, ErrNoModule +} diff --git a/kallsyms/kallsyms_test.go b/kallsyms/kallsyms_test.go new file mode 100644 index 000000000..dda0975fb --- /dev/null +++ b/kallsyms/kallsyms_test.go @@ -0,0 +1,105 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package kallsyms + +import ( + "strings" + "testing" + + "go.opentelemetry.io/ebpf-profiler/libpf" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func assertSymbol(t *testing.T, s *Symbolizer, pc libpf.Address, + eModName, eFuncName string, eOffset uint) { + kmod, err := s.GetModuleByAddress(pc) + if assert.NoError(t, err) && assert.Equal(t, kmod.Name(), eModName) { + funcName, offset, err := kmod.LookupSymbolByAddress(pc) + if assert.NoError(t, err) { + assert.Equal(t, eFuncName, funcName) + assert.Equal(t, eOffset, offset) + } + } +} + +func TestKallSyms(t *testing.T) { + // override the metadata loading to avoid mixing data from running system + loadModuleMetadata = func(_ *Module, _ string, _ int64) bool { return true } + + s := &Symbolizer{} + + err := s.updateSymbolsFrom(strings.NewReader(`0000000000000000 t pvh_start_xen +0000000000000000 T _stext +0000000000000000 T _text +0000000000000000 T startup_64 +0000000000000000 T __pfx___startup_64 +0000000000000000 T _etext`)) + assert.Equal(t, ErrSymbolPermissions, err) + + err = s.updateSymbolsFrom(strings.NewReader(`0000000000000000 A __per_cpu_start +0000000000001000 A cpu_debug_store +0000000000002000 A irq_stack_backing_store +ffffffffb5000000 t pvh_start_xen +ffffffffb5000000 T _stext +ffffffffb5000000 T _text +ffffffffb5000123 T startup_64 +ffffffffb5000180 T __pfx___startup_64 +ffffffffb5000190 T __startup_64 +ffffffffb5000460 T __pfx_startup_64_setup_gdt_idt +ffffffffb5000470 T startup_64_setup_gdt_idt +ffffffffb5001000 T __pfx___traceiter_initcall_level +ffffffffb6000000 T _etext +ffffffffc03cc610 t perf_trace_xfs_attr_list_class [xfs] +ffffffffc03cc770 t perf_trace_xfs_perag_class [xfs] +ffffffffc03cc8b0 t perf_trace_xfs_inodegc_worker [xfs] +ffffffffc03cc9d0 t perf_trace_xfs_fs_class [xfs] +ffffffffc03ccb20 t perf_trace_xfs_inodegc_shrinker_scan [xfs]`)) + require.NoError(t, err) + + _, err = s.GetModuleByName("foo") + assert.Equal(t, err, ErrNoModule) + + _, err = s.GetModuleByAddress(0x1010) + assert.Equal(t, err, ErrNoModule) + + _, err = s.GetModuleByAddress(0xffffffffffff0000) + assert.Equal(t, err, ErrNoModule) + + assertSymbol(t, s, 0xffffffffb5000470, Kernel, "startup_64_setup_gdt_idt", 0) + assertSymbol(t, s, 0xffffffffc03cc610, "xfs", "perf_trace_xfs_attr_list_class", 0) + assertSymbol(t, s, 0xffffffffc03cc610+1, "xfs", "perf_trace_xfs_attr_list_class", 1) + + err = s.updateSymbolsFrom(strings.NewReader(`0000000000000000 A __per_cpu_start +0000000000001000 A cpu_debug_store +0000000000002000 A irq_stack_backing_store +ffffffffb5000000 t pvh_start_xen +ffffffffb5000000 T _stext +ffffffffb5000000 T _text +ffffffffb5000123 T startup_64 +ffffffffb5000180 T __pfx___startup_64 +ffffffffb5000190 T __startup_64 +ffffffffb5000460 T __pfx_startup_64_setup_gdt_idt +ffffffffb5000470 T startup_64_setup_gdt_idt +ffffffffb5001000 T __pfx___traceiter_initcall_level +ffffffffb6000000 T _etext +ffffffffc13cc610 t perf_trace_xfs_attr_list_class [xfs] +ffffffffc13cc770 t perf_trace_xfs_perag_class [xfs] +ffffffffc13cc8b0 t perf_trace_xfs_inodegc_worker [xfs] +ffffffffc13cc9d0 t perf_trace_xfs_fs_class [xfs] +ffffffffc13ccb20 t perf_trace_xfs_inodegc_shrinker_scan [xfs] +ffffffffc1400000 t foo [foo] +ffffffffc13fcb20 t init_xfs_fs [xfs]`)) + require.NoError(t, err) + + _, err = s.GetModuleByAddress(0xffffffffc03cc610 + 1) + assert.Equal(t, ErrNoModule, err) + + _, err = s.GetModuleByAddress(0xffffffffc13fcb20) + assert.Equal(t, ErrNoModule, err) + + assertSymbol(t, s, 0xffffffffb5000470, "vmlinux", "startup_64_setup_gdt_idt", 0) + assertSymbol(t, s, 0xffffffffc13cc610+1, "xfs", "perf_trace_xfs_attr_list_class", 1) +} diff --git a/maccess/maccess_amd64.go b/maccess/maccess_amd64.go index c701c8a4f..2787b1b91 100644 --- a/maccess/maccess_amd64.go +++ b/maccess/maccess_amd64.go @@ -18,6 +18,9 @@ func CopyFromUserNoFaultIsPatched(codeblob []byte, if len(codeblob) == 0 { return false, errors.New("empty code blob") } + if newCheckFuncAddr == 0 { + return false, errors.New("nmi_uaccess_okay function not found") + } for i := 0; i < len(codeblob); { idx, offset := getRelativeOffset(codeblob[i:]) diff --git a/proc/proc.go b/proc/proc.go deleted file mode 100644 index b178e92f9..000000000 --- a/proc/proc.go +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -// Package proc provides functionality for retrieving kallsyms, modules and -// executable mappings via /proc. -package proc // import "go.opentelemetry.io/ebpf-profiler/proc" - -import ( - "bufio" - "errors" - "fmt" - "os" - "strconv" - "strings" - - log "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" - - "go.opentelemetry.io/ebpf-profiler/libpf" - "go.opentelemetry.io/ebpf-profiler/stringutil" -) - -const defaultMountPoint = "/proc" - -// GetKallsyms returns SymbolMap for kernel symbols from /proc/kallsyms. -func GetKallsyms(kallsymsPath string) (*libpf.SymbolMap, error) { - var address uint64 - var symbol string - - // As an example, the Debian 6.10.11 kernel has ~180k text symbols. - symmap := libpf.NewSymbolMap(200 * 1024) - noSymbols := true - - file, err := os.Open(kallsymsPath) - if err != nil { - return nil, fmt.Errorf("unable to open %s: %v", kallsymsPath, err) - } - defer file.Close() - - var scanner = bufio.NewScanner(file) - for scanner.Scan() { - // Avoid heap allocation by not using scanner.Text(). - // NOTE: The underlying bytes will change with the next call to scanner.Scan(), - // so make sure to not keep any references after the end of the loop iteration. - line := stringutil.ByteSlice2String(scanner.Bytes()) - - // Avoid heap allocations here - do not use strings.FieldsN() - var fields [4]string - nFields := stringutil.FieldsN(line, fields[:]) - - if nFields < 3 { - return nil, fmt.Errorf("unexpected line in kallsyms: '%s'", line) - } - - // Skip non-text symbols, see 'man nm'. - // Special case for 'etext', which can be of type `D` (data) in some kernels. - if strings.IndexByte("TtVvWwA", fields[1][0]) == -1 && fields[2] != "_etext" { - continue - } - - if address, err = strconv.ParseUint(fields[0], 16, 64); err != nil { - return nil, fmt.Errorf("failed to parse address value: '%s'", fields[0]) - } - - if address != 0 { - noSymbols = false - } - - symbol = strings.Clone(fields[2]) - - symmap.Add(libpf.Symbol{ - Name: libpf.SymbolName(symbol), - Address: libpf.SymbolValue(address), - }) - } - symmap.Finalize() - - if noSymbols { - return nil, errors.New( - "all addresses from kallsyms are zero - check process permissions") - } - - return symmap, nil -} - -// GetKernelModules returns SymbolMap for kernel modules from /proc/modules. -func GetKernelModules(modulesPath string, - kernelSymbols *libpf.SymbolMap) (*libpf.SymbolMap, error) { - symmap := libpf.SymbolMap{} - - file, err := os.Open(modulesPath) - if err != nil { - return nil, fmt.Errorf("unable to open %s: %v", modulesPath, err) - } - defer file.Close() - - stext, err := kernelSymbols.LookupSymbol("_stext") - if err != nil { - return nil, fmt.Errorf("unable to find kernel text section start: %v", err) - } - etext, err := kernelSymbols.LookupSymbol("_etext") - if err != nil { - return nil, fmt.Errorf("unable to find kernel text section end: %v", err) - } - log.Debugf("Found KERNEL TEXT at %x-%x", stext.Address, etext.Address) - symmap.Add(libpf.Symbol{ - Name: "vmlinux", - Address: stext.Address, - Size: uint64(etext.Address - stext.Address), - }) - - modules, err := parseKernelModules(bufio.NewScanner(file)) - if err != nil { - return nil, fmt.Errorf("failed to parse kernel modules: %v", err) - } - - for _, kmod := range modules { - symmap.Add(libpf.Symbol{ - Name: libpf.SymbolName(kmod.name), - Address: libpf.SymbolValue(kmod.address), - Size: kmod.size, - }) - } - - symmap.Finalize() - - return &symmap, nil -} - -func parseKernelModules(scanner *bufio.Scanner) ([]kernelModule, error) { - var ( - modules []kernelModule - atLeastOneValidAddress = false - count = 0 - ) - - for scanner.Scan() { - line := scanner.Text() - - count++ - - kmod, err := parseKernelModuleLine(line) - if err != nil { - return nil, fmt.Errorf("failed to parse kernel module line '%s': %v", line, err) - } - if kmod.address == 0 { - continue - } - atLeastOneValidAddress = true - - modules = append(modules, kmod) - } - - if count > 0 && !atLeastOneValidAddress { - return nil, errors.New("addresses from all modules is zero - check process permissions") - } - - return modules, nil -} - -type kernelModule struct { - name string - size uint64 - address uint64 -} - -func parseKernelModuleLine(line string) (kernelModule, error) { - // The format is: "name size refcount dependencies state address" - // The string is split into 7 parts as after address there can be an optional string. - parts := strings.SplitN(line, " ", 7) - if len(parts) < 6 { - return kernelModule{}, fmt.Errorf("unexpected line in modules: '%s'", line) - } - - size, err := parseSize(parts[1]) - if err != nil { - return kernelModule{}, err - } - - address, err := parseAddress(parts[5]) - if err != nil { - return kernelModule{}, err - } - - return kernelModule{ - name: parts[0], - size: size, - address: address, - }, nil -} - -func parseAddress(addressStr string) (uint64, error) { - address, err := strconv.ParseUint(strings.TrimPrefix(addressStr, "0x"), 16, 64) - if err != nil { - return 0, fmt.Errorf("failed to parse address '%s' as hex value: %v", - addressStr, err) - } - - return address, nil -} - -func parseSize(sizeStr string) (uint64, error) { - size, err := strconv.ParseUint(sizeStr, 10, 64) - if err != nil { - return 0, fmt.Errorf("failed to parse size int value: %q: %v", sizeStr, err) - } - - return size, nil -} - -// IsPIDLive checks if a PID belongs to a live process. It will never produce a false negative but -// may produce a false positive (e.g. due to permissions) in which case an error will also be -// returned. -func IsPIDLive(pid libpf.PID) (bool, error) { - // A kill syscall with a 0 signal is documented to still do the check - // whether the process exists: https://linux.die.net/man/2/kill - err := unix.Kill(int(pid), 0) - if err == nil { - return true, nil - } - - var errno unix.Errno - if errors.As(err, &errno) { - switch errno { - case unix.ESRCH: - return false, nil - case unix.EPERM: - // continue with procfs fallback - default: - return true, err - } - } - - path := fmt.Sprintf("%s/%d/maps", defaultMountPoint, pid) - _, err = os.Stat(path) - - if err != nil && os.IsNotExist(err) { - return false, nil - } - - return true, err -} diff --git a/proc/proc_test.go b/proc/proc_test.go deleted file mode 100644 index 5d4bbc5a5..000000000 --- a/proc/proc_test.go +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package proc - -import ( - "bufio" - "bytes" - "testing" - - "go.opentelemetry.io/ebpf-profiler/libpf" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func assertSymbol(t *testing.T, symmap *libpf.SymbolMap, name libpf.SymbolName, - expectedAddress libpf.SymbolValue) { - sym, err := symmap.LookupSymbol(name) - require.NoError(t, err) - assert.Equal(t, expectedAddress, sym.Address) -} - -func TestParseKallSyms(t *testing.T) { - // Check parsing as if we were non-root - symmap, err := GetKallsyms("testdata/kallsyms_0") - require.Error(t, err) - require.Nil(t, symmap) - - // Check parsing invalid file - symmap, err = GetKallsyms("testdata/kallsyms_invalid") - require.Error(t, err) - require.Nil(t, symmap) - - // Happy case - symmap, err = GetKallsyms("testdata/kallsyms") - require.NoError(t, err) - require.NotNil(t, symmap) - - assertSymbol(t, symmap, "cpu_tss_rw", 0x6000) - assertSymbol(t, symmap, "hid_add_device", 0xffffffffc033e550) -} - -func TestParseKernelModules(t *testing.T) { - content := []byte(`i40e 589824 - - Live 0xffffffffc0321000 -mpt3sas 405504 - - Live 0xffffffffc02ab000 -ahci 45056 - - Live 0xffffffffc0294000 -libahci 49152 - - Live 0xffffffffc027f000 -sp5100_tco 12288 - - Live 0xffffffffc0274000 -watchdog 40960 - - Live 0xffffffffc025f000 -k10temp 12288 - - Live 0xffffffffc0254000`) - - kmods, err := parseKernelModules(bufio.NewScanner(bytes.NewReader(content))) - require.NoError(t, err) - - require.Len(t, kmods, 7) - require.Equal(t, []kernelModule{ - { - name: "i40e", - size: 589824, - address: 0xffffffffc0321000, - }, - { - name: "mpt3sas", - size: 405504, - address: 0xffffffffc02ab000, - }, - { - name: "ahci", - size: 45056, - address: 0xffffffffc0294000, - }, - { - name: "libahci", - size: 49152, - address: 0xffffffffc027f000, - }, - { - name: "sp5100_tco", - size: 12288, - address: 0xffffffffc0274000, - }, - { - name: "watchdog", - size: 40960, - address: 0xffffffffc025f000, - }, - { - name: "k10temp", - size: 12288, - address: 0xffffffffc0254000, - }, - }, kmods) -} - -func TestParseKernelModuleLine(t *testing.T) { - tests := map[string]struct { - line string - expected kernelModule - }{ - "i40e": { - line: "i40e 589824 - - Live 0xffffffffc0364000", - expected: kernelModule{ - name: "i40e", - size: 589824, - address: 0xffffffffc0364000, - }, - }, - "nvidia": { - line: "nvidia_drm 102400 2 - Live 0xffffffffc11c0000 (POE)", - expected: kernelModule{ - name: "nvidia_drm", - size: 102400, - address: 0xffffffffc11c0000, - }, - }, - } - - for name, test := range tests { - name := name - test := test - t.Run(name, func(t *testing.T) { - kmod, err := parseKernelModuleLine(test.line) - require.NoError(t, err) - require.Equal(t, test.expected, kmod) - }) - } -} diff --git a/proc/testdata/kallsyms b/proc/testdata/kallsyms deleted file mode 100644 index 6b6f67365..000000000 --- a/proc/testdata/kallsyms +++ /dev/null @@ -1,13 +0,0 @@ -0000000000000000 A fixed_percpu_data -0000000000000000 A __per_cpu_start -0000000000001000 A cpu_debug_store -0000000000002000 A irq_stack_backing_store -0000000000006000 A cpu_tss_rw -ffffffffc0346f40 t hidraw_connect [hid] -ffffffffc0340470 t hidinput_find_field [hid] -ffffffffc033d150 t hid_parse_report [hid] -ffffffffc033f9a0 t hid_open_report [hid] -ffffffffc03459b0 t hid_quirks_init [hid] -ffffffffc0345720 t hid_ignore [hid] -ffffffffc033e550 t hid_add_device [hid] -ffffffffc0346e20 t hidraw_report_event [hid] diff --git a/proc/testdata/kallsyms_0 b/proc/testdata/kallsyms_0 deleted file mode 100644 index c4e26e86d..000000000 --- a/proc/testdata/kallsyms_0 +++ /dev/null @@ -1,5 +0,0 @@ -0000000000000000 A fixed_percpu_data -0000000000000000 A __per_cpu_start -0000000000000000 A cpu_debug_store -0000000000000000 A irq_stack_backing_store -0000000000000000 A cpu_tss_rw diff --git a/proc/testdata/kallsyms_invalid b/proc/testdata/kallsyms_invalid deleted file mode 100644 index 1b9c31a72..000000000 --- a/proc/testdata/kallsyms_invalid +++ /dev/null @@ -1,5 +0,0 @@ -0000000000000000 A fixed_percpu_data -0000000000000000 A __per_cpu_start -0000000000001000 A cpu_debug_store -0000000000002000 irq_stack_backing_store -0000000000006000 A cpu_tss_rw diff --git a/processmanager/processinfo.go b/processmanager/processinfo.go index 2872f04dc..84008bccc 100644 --- a/processmanager/processinfo.go +++ b/processmanager/processinfo.go @@ -21,13 +21,13 @@ import ( "time" log "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" "go.opentelemetry.io/ebpf-profiler/host" "go.opentelemetry.io/ebpf-profiler/interpreter" "go.opentelemetry.io/ebpf-profiler/libpf" "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" "go.opentelemetry.io/ebpf-profiler/lpm" - "go.opentelemetry.io/ebpf-profiler/proc" "go.opentelemetry.io/ebpf-profiler/process" eim "go.opentelemetry.io/ebpf-profiler/processmanager/execinfomanager" "go.opentelemetry.io/ebpf-profiler/reporter" @@ -37,6 +37,40 @@ import ( "go.opentelemetry.io/ebpf-profiler/util" ) +// isPIDLive checks if a PID belongs to a live process. It will never produce a false negative but +// may produce a false positive (e.g. due to permissions) in which case an error will also be +// returned. +func isPIDLive(pid libpf.PID) (bool, error) { + // Check first with the kill syscall which is the fastest route. + // A kill syscall with a 0 signal is documented to still do the check + // whether the process exists: https://linux.die.net/man/2/kill + err := unix.Kill(int(pid), 0) + if err == nil { + return true, nil + } + + var errno unix.Errno + if errors.As(err, &errno) { + switch errno { + case unix.ESRCH: + return false, nil + case unix.EPERM: + // It seems that in some rare cases this check can fail with + // a permission error. Fallback to a procfs check. + default: + return true, err + } + } + + path := fmt.Sprintf("/proc/%d/maps", pid) + _, err = os.Stat(path) + if err != nil && os.IsNotExist(err) { + return false, nil + } + + return true, err +} + // assignTSDInfo updates the TSDInfo for the Interpreters on given PID. // Caller must hold pm.mu write lock. func (pm *ProcessManager) assignTSDInfo(pid libpf.PID, tsdInfo *tpbase.TSDInfo) { @@ -514,7 +548,7 @@ func (pm *ProcessManager) synchronizeMappings(pr process.Process, for _, instance := range pm.interpreters[pid] { err := instance.SynchronizeMappings(pm.ebpf, pm.reporter, pr, mappings) if err != nil { - if alive, _ := proc.IsPIDLive(pid); alive { + if alive, _ := isPIDLive(pid); alive { log.Errorf("Failed to handle new anonymous mapping for PID %d: %v", pid, err) } else { log.Debugf("Failed to handle new anonymous mapping for PID %d: process exited", @@ -665,7 +699,7 @@ func (pm *ProcessManager) CleanupPIDs() { pm.mu.RLock() for pid := range pm.pidToProcessInfo { - if live, _ := proc.IsPIDLive(pid); !live { + if live, _ := isPIDLive(pid); !live { deadPids = append(deadPids, pid) } } diff --git a/stringutil/stringutil.go b/stringutil/stringutil.go index 06a199882..3be749285 100644 --- a/stringutil/stringutil.go +++ b/stringutil/stringutil.go @@ -82,5 +82,5 @@ func SplitN(s, sep string, f []string) int { // Be aware that the byte slice and the string share the same memory - which makes // the string mutable. func ByteSlice2String(b []byte) string { - return *(*string)(unsafe.Pointer(&b)) + return unsafe.String(unsafe.SliceData(b), len(b)) } diff --git a/tracer/ebpf_integration_test.go b/tracer/ebpf_integration_test.go index 124c1679b..a527fc685 100644 --- a/tracer/ebpf_integration_test.go +++ b/tracer/ebpf_integration_test.go @@ -19,8 +19,8 @@ import ( "github.com/stretchr/testify/require" "go.opentelemetry.io/ebpf-profiler/host" + "go.opentelemetry.io/ebpf-profiler/kallsyms" "go.opentelemetry.io/ebpf-profiler/libpf" - "go.opentelemetry.io/ebpf-profiler/proc" "go.opentelemetry.io/ebpf-profiler/reporter" "go.opentelemetry.io/ebpf-profiler/rlimit" "go.opentelemetry.io/ebpf-profiler/support" @@ -253,10 +253,13 @@ Loop: } func TestAllTracers(t *testing.T) { - kernelSymbols, err := proc.GetKallsyms("/proc/kallsyms") + s, err := kallsyms.NewSymbolizer() require.NoError(t, err) - _, _, err = initializeMapsAndPrograms(kernelSymbols, &Config{ + kmod, err := s.GetModuleByName(kallsyms.Kernel) + require.NoError(t, err) + + _, _, err = initializeMapsAndPrograms(kmod, &Config{ IncludeTracers: tracertypes.AllTracers(), MapScaleFactor: 1, FilterErrorFrames: false, diff --git a/tracer/maccess.go b/tracer/maccess.go index efc68c08b..f8968cb99 100644 --- a/tracer/maccess.go +++ b/tracer/maccess.go @@ -6,9 +6,9 @@ package tracer // import "go.opentelemetry.io/ebpf-profiler/tracer" import ( "errors" "fmt" - "runtime" cebpf "github.com/cilium/ebpf" + "go.opentelemetry.io/ebpf-profiler/kallsyms" "go.opentelemetry.io/ebpf-profiler/libpf" "go.opentelemetry.io/ebpf-profiler/maccess" ) @@ -16,42 +16,21 @@ import ( // checkForMmaccessPatch validates if a Linux kernel function is patched by // extracting the kernel code of the function and analyzing it. func checkForMaccessPatch(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, - kernelSymbols *libpf.SymbolMap) error { - faultyFunc, err := kernelSymbols.LookupSymbol( - libpf.SymbolName("copy_from_user_nofault")) + kmod *kallsyms.Module) error { + faultyFunc, err := kmod.LookupSymbol("copy_from_user_nofault") if err != nil { - return fmt.Errorf("failed to look up Linux kernel symbol "+ - "'copy_from_user_nofault': %v", err) + return fmt.Errorf("failed to lookup 'copy_from_user_nofault': %v", err) } - - code, err := loadKernelCode(coll, maps, faultyFunc.Address) - if err != nil { - return fmt.Errorf("failed to load kernel code for %s: %v", faultyFunc.Name, err) - } - - newCheckFunc, err := kernelSymbols.LookupSymbol( - libpf.SymbolName("nmi_uaccess_okay")) + code, err := loadKernelCode(coll, maps, libpf.SymbolValue(faultyFunc)) if err != nil { - //nolint:goconst - if runtime.GOARCH == "arm64" { - // On arm64 this symbol might not be available and we do not use - // the symbol address in the arm64 case to check for the patch. - // As there was an error getting the symbol, newCheckFunc is nil. - // To still be able to access newCheckFunc safely, create a dummy element. - newCheckFunc = &libpf.Symbol{ - Address: 0, - } - } else { - // Without the symbol information, we can not continue with checking the - // function and determine whether it got patched. - return fmt.Errorf("failed to look up Linux kernel symbol 'nmi_uaccess_okay': %v", err) - } + return fmt.Errorf("failed to load kernel code for 'copy_from_user_nofault': %v", err) } - patched, err := maccess.CopyFromUserNoFaultIsPatched(code, uint64(faultyFunc.Address), - uint64(newCheckFunc.Address)) + newCheckFunc, _ := kmod.LookupSymbol("nmi_uaccess_okay") + patched, err := maccess.CopyFromUserNoFaultIsPatched(code, + uint64(faultyFunc), uint64(newCheckFunc)) if err != nil { - return fmt.Errorf("failed to check if %s is patched: %v", faultyFunc.Name, err) + return fmt.Errorf("failed to check if 'copy_from_user_nofault' is patched: %v", err) } if !patched { return errors.New("kernel is not patched") diff --git a/tracer/systemconfig.go b/tracer/systemconfig.go index b8dc3ad48..031ee5c37 100644 --- a/tracer/systemconfig.go +++ b/tracer/systemconfig.go @@ -12,6 +12,9 @@ import ( "strings" "unsafe" + "go.opentelemetry.io/ebpf-profiler/kallsyms" + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/pacmask" "go.opentelemetry.io/ebpf-profiler/rlimit" "go.opentelemetry.io/ebpf-profiler/tracer/types" @@ -19,9 +22,6 @@ import ( "github.com/cilium/ebpf/btf" "github.com/cilium/ebpf/link" log "github.com/sirupsen/logrus" - - "go.opentelemetry.io/ebpf-profiler/libpf" - "go.opentelemetry.io/ebpf-profiler/pacmask" ) // #include "../support/ebpf/types.h" @@ -226,7 +226,7 @@ func determineStackLayout(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map } func loadSystemConfig(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, - kernelSymbols *libpf.SymbolMap, includeTracers types.IncludedTracers, + kmod *kallsyms.Module, includeTracers types.IncludedTracers, offCPUThreshold uint32, filterErrorFrames bool) error { pacMask := pacmask.GetPACMask() if pacMask != 0 { @@ -249,7 +249,7 @@ func loadSystemConfig(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, if includeTracers.Has(types.PerlTracer) || includeTracers.Has(types.PythonTracer) { var tpbaseOffset uint64 - tpbaseOffset, err = loadTPBaseOffset(coll, maps, kernelSymbols) + tpbaseOffset, err = loadTPBaseOffset(coll, maps, kmod) if err != nil { return err } diff --git a/tracer/tpbase.go b/tracer/tpbase.go index f39f2d06b..5a797c74e 100644 --- a/tracer/tpbase.go +++ b/tracer/tpbase.go @@ -11,6 +11,7 @@ import ( cebpf "github.com/cilium/ebpf" log "github.com/sirupsen/logrus" + "go.opentelemetry.io/ebpf-profiler/kallsyms" "go.opentelemetry.io/ebpf-profiler/libpf" "go.opentelemetry.io/ebpf-profiler/tpbase" ) @@ -35,15 +36,15 @@ import ( // kernel struct. This offset varies depending on kernel configuration, so we have to learn // it dynamically at runtime. func loadTPBaseOffset(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, - kernelSymbols *libpf.SymbolMap) (uint64, error) { + kmod *kallsyms.Module) (uint64, error) { var tpbaseOffset uint32 for _, analyzer := range tpbase.GetAnalyzers() { - sym, err := kernelSymbols.LookupSymbol(libpf.SymbolName(analyzer.FunctionName)) + sym, err := kmod.LookupSymbol(analyzer.FunctionName) if err != nil { continue } - code, err := loadKernelCode(coll, maps, sym.Address) + code, err := loadKernelCode(coll, maps, libpf.SymbolValue(sym)) if err != nil { return 0, err } diff --git a/tracer/tracer.go b/tracer/tracer.go index c827dfb96..cc57fd15f 100644 --- a/tracer/tracer.go +++ b/tracer/tracer.go @@ -9,10 +9,7 @@ import ( "context" "errors" "fmt" - "hash/fnv" - "math" "math/rand/v2" - "sort" "strings" "sync/atomic" "time" @@ -26,13 +23,12 @@ import ( "github.com/zeebo/xxh3" "go.opentelemetry.io/ebpf-profiler/host" + "go.opentelemetry.io/ebpf-profiler/kallsyms" "go.opentelemetry.io/ebpf-profiler/libpf" - "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" "go.opentelemetry.io/ebpf-profiler/libpf/xsync" "go.opentelemetry.io/ebpf-profiler/metrics" "go.opentelemetry.io/ebpf-profiler/nativeunwind/elfunwindinfo" "go.opentelemetry.io/ebpf-profiler/periodiccaller" - "go.opentelemetry.io/ebpf-profiler/proc" pm "go.opentelemetry.io/ebpf-profiler/processmanager" pmebpf "go.opentelemetry.io/ebpf-profiler/processmanager/ebpf" "go.opentelemetry.io/ebpf-profiler/reporter" @@ -41,7 +37,6 @@ import ( "go.opentelemetry.io/ebpf-profiler/times" "go.opentelemetry.io/ebpf-profiler/tracehandler" "go.opentelemetry.io/ebpf-profiler/tracer/types" - "go.opentelemetry.io/ebpf-profiler/util" ) /* @@ -83,11 +78,8 @@ type Tracer struct { // ebpfProgs holds the currently loaded eBPF programs. ebpfProgs map[string]*cebpf.Program - // kernelSymbols is used to hold the kernel symbol addresses we are tracking - kernelSymbols *libpf.SymbolMap - - // kernelModules holds symbols/addresses for the kernel module address space - kernelModules *libpf.SymbolMap + // kernelSymbolizer does kernel fallback symbolization + kernelSymbolizer *kallsyms.Symbolizer // perfEntrypoints holds a list of frequency based perf events that are opened on the system. perfEntrypoints xsync.RWMutex[[]*perf.Event] @@ -116,9 +108,6 @@ type Tracer struct { hasBatchOperations bool - // moduleFileIDs maps kernel module names to their respective FileID. - moduleFileIDs map[string]libpf.FileID - // reporter allows swapping out the reporter implementation. reporter reporter.SymbolReporter @@ -179,114 +168,20 @@ type progLoaderHelper struct { noTailCallTarget bool } -// processKernelModulesMetadata computes the FileID of kernel files and reports executable metadata -// for all kernel modules and the vmlinux image. -func processKernelModulesMetadata(rep reporter.SymbolReporter, kernelModules *libpf.SymbolMap, - kernelSymbols *libpf.SymbolMap) (map[string]libpf.FileID, error) { - result := make(map[string]libpf.FileID, kernelModules.Len()) - kernelModules.VisitAll(func(moduleSym libpf.Symbol) { - nameStr := string(moduleSym.Name) - if !util.IsValidString(nameStr) { - log.Errorf("Invalid string representation of file name in "+ - "processKernelModulesMetadata: %v", []byte(nameStr)) - return - } - - // Read the kernel and modules ELF notes from sysfs (works since Linux 2.6.24) - notesFile := fmt.Sprintf("/sys/module/%s/notes/.note.gnu.build-id", nameStr) - - // The vmlinux notes section is in a different location - if nameStr == "vmlinux" { - notesFile = "/sys/kernel/notes" - } - - buildID, err := pfelf.GetBuildIDFromNotesFile(notesFile) - var fileID libpf.FileID - // Require at least 16 bytes of BuildID to ensure there is enough entropy for a FileID. - // 16 bytes could happen when --build-id=md5 is passed to `ld`. This would imply a custom - // kernel. - if err == nil && len(buildID) >= 16 { - fileID = libpf.FileIDFromKernelBuildID(buildID) - } else { - fileID = calcFallbackModuleID(moduleSym, kernelSymbols) - buildID = "" - } - - result[nameStr] = fileID - rep.ExecutableMetadata(&reporter.ExecutableMetadataArgs{ - FileID: fileID, - FileName: nameStr, - GnuBuildID: buildID, - DebuglinkFileName: "", - Interp: libpf.Kernel, - }) - }) - - return result, nil -} - -// calcFallbackModuleID computes a fallback file ID for kernel modules that do not -// have a GNU build ID. Getting the actual file for the kernel module isn't always -// possible since they don't necessarily reside on disk, e.g. when modules are loaded -// from the initramfs that is later unmounted again. -// -// This fallback checksum locates all symbols exported by a given driver, normalizes -// them to offsets and hashes over that. Additionally, the module's name and size are -// hashed as well. This isn't perfect, and we can't do any server-side symbolization -// with these IDs, but at least it provides a stable unique key for the kernel fallback -// symbols that we send. -func calcFallbackModuleID(moduleSym libpf.Symbol, kernelSymbols *libpf.SymbolMap) libpf.FileID { - modStart := moduleSym.Address - modEnd := moduleSym.Address + libpf.SymbolValue(moduleSym.Size) - - // Collect symbols belonging to this module + track minimum address. - var moduleSymbols []libpf.Symbol - minAddr := libpf.SymbolValue(math.MaxUint64) - kernelSymbols.VisitAll(func(symbol libpf.Symbol) { - if symbol.Address >= modStart && symbol.Address < modEnd { - moduleSymbols = append(moduleSymbols, symbol) - minAddr = min(minAddr, symbol.Address) - } - }) - - // Ensure consistent order. - sort.Slice(moduleSymbols, func(a, b int) bool { - return moduleSymbols[a].Address < moduleSymbols[b].Address - }) - - // Hash exports and their normalized addresses. - h := fnv.New128a() - h.Write([]byte(moduleSym.Name)) - h.Write(libpf.SliceFrom(&moduleSym.Size)) - - for _, sym := range moduleSymbols { - sym.Address -= minAddr // KASLR normalization - - h.Write([]byte(sym.Name)) - h.Write(libpf.SliceFrom(&sym.Address)) - } - - var hash [16]byte - fileID, err := libpf.FileIDFromBytes(h.Sum(hash[:0])) +// NewTracer loads eBPF code and map definitions from the ELF module at the configured path. +func NewTracer(ctx context.Context, cfg *Config) (*Tracer, error) { + kernelSymbolizer, err := kallsyms.NewSymbolizer() if err != nil { - panic("calcFallbackModuleID file ID construction is broken") + return nil, fmt.Errorf("failed to read kernel symbols: %v", err) } - log.Debugf("Fallback module ID for module %s is '%s' (min addr: 0x%08X, num exports: %d)", - moduleSym.Name, fileID.Base64(), minAddr, len(moduleSymbols)) - - return fileID -} - -// NewTracer loads eBPF code and map definitions from the ELF module at the configured path. -func NewTracer(ctx context.Context, cfg *Config) (*Tracer, error) { - kernelSymbols, err := proc.GetKallsyms("/proc/kallsyms") + kmod, err := kernelSymbolizer.GetModuleByName(kallsyms.Kernel) if err != nil { return nil, fmt.Errorf("failed to read kernel symbols: %v", err) } // Based on includeTracers we decide later which are loaded into the kernel. - ebpfMaps, ebpfProgs, err := initializeMapsAndPrograms(kernelSymbols, cfg) + ebpfMaps, ebpfProgs, err := initializeMapsAndPrograms(kmod, cfg) if err != nil { return nil, fmt.Errorf("failed to load eBPF code: %v", err) } @@ -307,22 +202,11 @@ func NewTracer(ctx context.Context, cfg *Config) (*Tracer, error) { const fallbackSymbolsCacheSize = 16384 - kernelModules, err := proc.GetKernelModules("/proc/modules", kernelSymbols) - if err != nil { - return nil, fmt.Errorf("failed to read kernel modules: %v", err) - } - - moduleFileIDs, err := processKernelModulesMetadata(cfg.Reporter, kernelModules, kernelSymbols) - if err != nil { - return nil, fmt.Errorf("failed to extract kernel modules metadata: %v", err) - } - perfEventList := []*perf.Event{} - return &Tracer{ + tracer := &Tracer{ + kernelSymbolizer: kernelSymbolizer, processManager: processManager, - kernelSymbols: kernelSymbols, - kernelModules: kernelModules, triggerPIDProcessing: make(chan bool, 1), pidEvents: make(chan libpf.PIDTID, pidEventBufferSize), ebpfMaps: ebpfMaps, @@ -331,12 +215,13 @@ func NewTracer(ctx context.Context, cfg *Config) (*Tracer, error) { intervals: cfg.Intervals, hasBatchOperations: hasBatchOperations, perfEntrypoints: xsync.NewRWMutex(perfEventList), - moduleFileIDs: moduleFileIDs, reporter: cfg.Reporter, samplesPerSecond: cfg.SamplesPerSecond, probabilisticInterval: cfg.ProbabilisticInterval, probabilisticThreshold: cfg.ProbabilisticThreshold, - }, nil + } + + return tracer, nil } // Close provides functionality for Tracer to perform cleanup tasks. @@ -386,7 +271,7 @@ func buildStackDeltaTemplates(coll *cebpf.CollectionSpec) error { // initializeMapsAndPrograms loads the definitions for the eBPF maps and programs provided // by the embedded elf file and loads these into the kernel. -func initializeMapsAndPrograms(kernelSymbols *libpf.SymbolMap, cfg *Config) ( +func initializeMapsAndPrograms(kmod *kallsyms.Module, cfg *Config) ( ebpfMaps map[string]*cebpf.Map, ebpfProgs map[string]*cebpf.Program, err error) { // Loading specifications about eBPF programs and maps from the embedded elf file // does not load them into the kernel. @@ -428,7 +313,7 @@ func initializeMapsAndPrograms(kernelSymbols *libpf.SymbolMap, cfg *Config) ( return nil, nil, fmt.Errorf("failed to get kernel version: %v", err) } if hasProbeReadBug(major, minor, patch) { - if err = checkForMaccessPatch(coll, ebpfMaps, kernelSymbols); err != nil { + if err = checkForMaccessPatch(coll, ebpfMaps, kmod); err != nil { return nil, nil, fmt.Errorf("your kernel version %d.%d.%d may be "+ "affected by a Linux kernel bug that can lead to system "+ "freezes, terminating host agent now to avoid "+ @@ -501,7 +386,7 @@ func initializeMapsAndPrograms(kernelSymbols *libpf.SymbolMap, cfg *Config) ( } } - if err = loadSystemConfig(coll, ebpfMaps, kernelSymbols, cfg.IncludeTracers, + if err = loadSystemConfig(coll, ebpfMaps, kmod, cfg.IncludeTracers, cfg.OffCPUThreshold, cfg.FilterErrorFrames); err != nil { return nil, nil, fmt.Errorf("failed to load system config: %v", err) } @@ -775,19 +660,12 @@ func (t *Tracer) insertKernelFrames(trace *host.Trace, ustackLen uint32, var kernelSymbolCacheHit, kernelSymbolCacheMiss uint64 for i := uint32(0); i < kstackLen; i++ { - // Translate the kernel address into something that can be - // later symbolized. The address is made relative to - // matching module's ELF .text section: - // - main image should have .text section at start of the code segment - // - modules are ELF object files (.o) without program headers and - // LOAD segments. the address is relative to the .text section - mod, addr, _ := t.kernelModules.LookupByAddress( - libpf.SymbolValue(kstackVal[i])) - - fileID, foundFileID := t.moduleFileIDs[string(mod)] - - if !foundFileID { - fileID = libpf.UnknownKernelFileID + fileID := libpf.UnknownKernelFileID + address := libpf.Address(kstackVal[i]) + kmod, err := t.kernelSymbolizer.GetModuleByAddress(address) + if err == nil { + fileID = kmod.FileID() + address -= kmod.Start() } hostFileID := host.FileIDFromLibpf(fileID) @@ -795,7 +673,7 @@ func (t *Tracer) insertKernelFrames(trace *host.Trace, ustackLen uint32, trace.Frames[i] = host.Frame{ File: hostFileID, - Lineno: libpf.AddressOrLineno(addr), + Lineno: libpf.AddressOrLineno(address), Type: libpf.KernelFrame, // For all kernel frames, the kernel unwinder will always produce a @@ -804,10 +682,6 @@ func (t *Tracer) insertKernelFrames(trace *host.Trace, ustackLen uint32, ReturnAddress: true, } - if !foundFileID { - continue - } - // Kernel frame PCs need to be adjusted by -1. This duplicates logic done in the trace // converter. This should be fixed with PF-1042. frameID := libpf.NewFrameID(fileID, trace.Frames[i].Lineno-1) @@ -817,11 +691,19 @@ func (t *Tracer) insertKernelFrames(trace *host.Trace, ustackLen uint32, } kernelSymbolCacheMiss++ - if symbol, _, foundSymbol := t.kernelSymbols.LookupByAddress( - libpf.SymbolValue(kstackVal[i])); foundSymbol { + if kmod == nil { + continue + } + if funcName, _, err := kmod.LookupSymbolByAddress(libpf.Address(kstackVal[i])); err == nil { t.reporter.FrameMetadata(&reporter.FrameMetadataArgs{ FrameID: frameID, - FunctionName: string(symbol), + FunctionName: funcName, + }) + t.reporter.ExecutableMetadata(&reporter.ExecutableMetadataArgs{ + FileID: kmod.FileID(), + FileName: kmod.Name(), + GnuBuildID: kmod.BuildID(), + Interp: libpf.Kernel, }) } } @@ -1046,6 +928,9 @@ func (t *Tracer) loadBpfTrace(raw []byte, cpu int) *host.Trace { // StartMapMonitors starts goroutines for collecting metrics and monitoring eBPF // maps for tracepoints, new traces, trace count updates and unknown PCs. func (t *Tracer) StartMapMonitors(ctx context.Context, traceOutChan chan<- *host.Trace) error { + if err := t.kernelSymbolizer.StartMonitor(ctx); err != nil { + log.Warnf("Failed to start kallsyms monitor: %v", err) + } eventMetricCollector := t.startEventMonitor(ctx) traceEventMetricCollector := t.startTraceEventMonitor(ctx, traceOutChan) @@ -1300,12 +1185,17 @@ func (t *Tracer) StartOffCPUProfiling() error { return errors.New("off-cpu program finish_task_switch is not available") } - hookSymbolPrefix := "finish_task_switch" - kprobeSymbs, err := t.kernelSymbols.LookupSymbolsByPrefix(hookSymbolPrefix) + kmod, err := t.kernelSymbolizer.GetModuleByName(kallsyms.Kernel) if err != nil { return err } + hookSymbolPrefix := "finish_task_switch" + kprobeSymbs := kmod.LookupSymbolsByPrefix(hookSymbolPrefix) + if len(kprobeSymbs) == 0 { + return errors.New("no finish_task_switch symbols found") + } + attached := false // Attach to all symbols with the prefix finish_task_switch. for _, symb := range kprobeSymbs {