diff --git a/.github/workflows/unit-test-on-pull-request.yml b/.github/workflows/unit-test-on-pull-request.yml index 79cd4c4b..ae4bf888 100644 --- a/.github/workflows/unit-test-on-pull-request.yml +++ b/.github/workflows/unit-test-on-pull-request.yml @@ -59,12 +59,12 @@ jobs: - name: Linter run: | go version - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.54.2 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.56.2 make lint test: name: Test - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: fail-fast: true max-parallel: 2 @@ -72,7 +72,7 @@ jobs: go: ["stable"] steps: - name: Install dependencies - run: sudo apt-get install -y llvm clang dwz cmake curl unzip + run: sudo apt-get install -y llvm clang-16 dwz cmake curl unzip - name: Install Zydis shell: bash run: | diff --git a/.gitignore b/.gitignore index 60d3f165..b66e0db3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,3 @@ /.idea /go otel-profiling-agent -tracer.ebpf -tracer.ebpf.arm64 -tracer.ebpf.x86 diff --git a/.golangci.yml b/.golangci.yml index 365cd445..8019559a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -39,7 +39,6 @@ linters: - depguard - dupword - durationcheck # might be worth fixing - - errname # might be worth fixing - errorlint # might be worth fixing - exhaustive - exhaustivestruct @@ -50,16 +49,16 @@ linters: - gochecknoglobals - gochecknoinits - gocognit + - goconst - gocyclo - godot - godox # complains about TODO etc - goerr113 - gofumpt - - goimports # might be worth fixing - - golint # might be worth fixing - gomnd - gomoddirectives - ifshort + - inamedparam - interfacebloat - ireturn - maintidx @@ -73,6 +72,7 @@ linters: - nonamedreturns - nosnakecase - paralleltest + - protogetter - scopelint # might be worth fixing - sqlclosecheck # might be worth fixing - tagalign @@ -115,7 +115,6 @@ linters-settings: - octalLiteral - whyNoLint - wrapperFunc - - sloppyTestFuncName - sloppyReassign - uncheckedInlineErr # Experimental rule with high false positive rate. diff --git a/Dockerfile b/Dockerfile index ea63e110..15a49483 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,8 @@ WORKDIR /agent ARG arch=amd64 RUN apt-get update -y && apt-get dist-upgrade -y && apt-get install -y \ - curl wget cmake dwz lsb-release software-properties-common gnupg git clang llvm \ - golang unzip jq + curl wget cmake dwz lsb-release software-properties-common gnupg git clang-16 llvm \ + golang unzip jq && apt-get clean autoclean && apt-get autoremove --yes RUN git clone --depth 1 --branch v3.1.0 --recursive https://github.com/zyantific/zydis.git && \ cd zydis && mkdir build && cd build && \ @@ -14,7 +14,7 @@ RUN git clone --depth 1 --branch v3.1.0 --recursive https://github.com/zyantific cd zycore && make install && \ cd ../../.. && rm -rf zydis -RUN wget -qO- https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.54.2 +RUN wget -qO- https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.56.2 # gRPC dependencies diff --git a/Makefile b/Makefile index a1ceecf8..ba548954 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ clean: @chmod -Rf u+w go/ || true @rm -rf go .cache -generate: protobuf +generate: go install github.com/florianl/bluebox@v0.0.1 go generate ./... @@ -31,7 +31,7 @@ test: generate ebpf test-deps go test ./... TESTDATA_DIRS:= \ - libpf/nativeunwind/elfunwindinfo/testdata \ + nativeunwind/elfunwindinfo/testdata \ libpf/pfelf/testdata \ reporter/testdata @@ -40,9 +40,6 @@ test-deps: ($(MAKE) -C "$(testdata_dir)") || exit ; \ ) -protobuf: - cd proto && ./buildpb.sh - # Detect native architecture. UNAME_NATIVE_ARCH:=$(shell uname -m) diff --git a/README.md b/README.md index 7a4b5c80..b5796666 100644 --- a/README.md +++ b/README.md @@ -93,9 +93,9 @@ devfiler spins up a local server that listens on `0.0.0.0:11000`. To run it, simply download and unpack the archive from the following URL: -https://upload.elastic.co/d/0891b6a006b1ee8224e638d2454b967f7c1ac596110b5c149ac7c98107655d9b +https://upload.elastic.co/d/308473059574c7b2855dc9a26e86f81336f51ad03d1792050eccf096d319f0af -Authentication token: `a217abfdd7c438e9` +Authentication token: `c9ecd93c7de4f032` The archive contains a build for each of the following platforms: diff --git a/libpf/armhelpers/arm_helpers.go b/armhelpers/arm_helpers.go similarity index 98% rename from libpf/armhelpers/arm_helpers.go rename to armhelpers/arm_helpers.go index f133d24d..63245046 100644 --- a/libpf/armhelpers/arm_helpers.go +++ b/armhelpers/arm_helpers.go @@ -12,7 +12,7 @@ import ( "strconv" "strings" - "github.com/elastic/otel-profiling-agent/libpf/stringutil" + "github.com/elastic/otel-profiling-agent/stringutil" aa "golang.org/x/arch/arm64/arm64asm" ) diff --git a/cli_flags.go b/cli_flags.go index 68a5941e..d4a31019 100644 --- a/cli_flags.go +++ b/cli_flags.go @@ -10,15 +10,12 @@ import ( "flag" "fmt" "os" - "runtime" - "strings" "time" cebpf "github.com/cilium/ebpf" "github.com/peterbourgon/ff/v3" + log "github.com/sirupsen/logrus" - "github.com/elastic/otel-profiling-agent/config" - "github.com/elastic/otel-profiling-agent/debug/log" "github.com/elastic/otel-profiling-agent/hostmetadata/host" "github.com/elastic/otel-profiling-agent/tracer" ) @@ -55,7 +52,7 @@ var ( configFileHelp = "Path to the profiling agent configuration file." projectIDHelp = "The project ID to split profiling data into logical groups. " + "Its value should be larger than 0 and smaller than 4096." - cacheDirectoryHelp = "The directory where profiling agent can store cached data." + cacheDirectoryHelp = "The directory where the profiling agent can store cached data." secretTokenHelp = "The secret token associated with the project id." tagsHelp = fmt.Sprintf("User-specified tags separated by ';'. "+ "Each tag should match '%v'.", host.ValidTagRegex) @@ -117,7 +114,7 @@ func parseArgs() error { fs.IntVar(&argBpfVerifierLogSize, "bpf-log-size", cebpf.DefaultVerifierLogSize, bpfVerifierLogSizeHelp) - fs.StringVar(&argCacheDirectory, "cache-directory", config.CacheDirectory(), + fs.StringVar(&argCacheDirectory, "cache-directory", "/var/cache/otel/profiling-agent", cacheDirectoryHelp) fs.StringVar(&argCollAgentAddr, "collection-agent", "", collAgentAddrHelp) @@ -167,72 +164,6 @@ func parseArgs() error { return err } -// parseTracers parses a string that specifies one or more eBPF tracers to enable. -// Valid inputs are 'all', 'native', 'python', 'php', or any comma-delimited combination of these. -// The return value is a boolean lookup table that represents the input strings. -// E.g. to check if the Python tracer was requested: `if result[config.PythonTracer]...`. -func parseTracers(tracers string) ([]bool, error) { - fields := strings.Split(tracers, ",") - if len(fields) == 0 { - return nil, fmt.Errorf("invalid tracer specification '%s'", tracers) - } - - result := make([]bool, config.MaxTracers) - tracerNameToType := map[string]config.TracerType{ - "v8": config.V8Tracer, - "php": config.PHPTracer, - "perl": config.PerlTracer, - "ruby": config.RubyTracer, - "python": config.PythonTracer, - "hotspot": config.HotspotTracer, - } - - // Parse and validate tracers string - for _, name := range fields { - name = strings.ToLower(name) - - //nolint:goconst - if runtime.GOARCH == "arm64" && name == "v8" { - return nil, fmt.Errorf("the V8 tracer is currently not supported on ARM64") - } - - if tracerType, ok := tracerNameToType[name]; ok { - result[tracerType] = true - continue - } - - if name == "all" { - for i := range result { - result[i] = true - } - result[config.V8Tracer] = runtime.GOARCH != "arm64" //nolint:goconst - continue - } - if name == "native" { - log.Warn("Enabling the `native` tracer explicitly is deprecated (it's now always-on)") - continue - } - - if name != "" { - return nil, fmt.Errorf("unknown tracer: %s", name) - } - } - - tracersEnabled := make([]string, 0, config.MaxTracers) - for _, tracerType := range config.AllTracers() { - if result[tracerType] { - tracersEnabled = append(tracersEnabled, tracerType.GetString()) - } - } - - if len(tracersEnabled) > 0 { - log.Debugf("Tracer string: %v", tracers) - log.Infof("Interpreter tracers: %v", strings.Join(tracersEnabled, ",")) - } - - return result, nil -} - func dumpArgs() { log.Debug("Config:") fs.VisitAll(func(f *flag.Flag) { diff --git a/config/config.go b/config/config.go index d6c97628..58b93d25 100644 --- a/config/config.go +++ b/config/config.go @@ -7,6 +7,7 @@ package config import ( + "errors" "fmt" "os" "strconv" @@ -14,7 +15,7 @@ import ( log "github.com/sirupsen/logrus" - "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/util" ) const ( @@ -23,6 +24,11 @@ const ( // Config is the structure to pass the configuration into host-agent. type Config struct { + // TODO: the three variables below are currently not actually initialized yet + Version string + Revision string + BuildTimestamp string + EnvironmentType string MachineID string SecretToken string @@ -41,7 +47,6 @@ type Config struct { SamplesPerSecond uint16 PresentCPUCores uint16 DisableTLS bool - UploadSymbols bool NoKernelVersionCheck bool TraceCacheIntervals uint8 Verbose bool @@ -61,6 +66,10 @@ type Config struct { // To avoid passing them as argument to every function, they are declared // on package scope. var ( + version string + revision string + buildTimestamp string + // hostID represents project wide unique id to identify the host. hostID uint64 // projectID is read from the provided configuration file and sent to the collection agent @@ -86,8 +95,6 @@ var ( disableTLS bool // noKernelVersionCheck indicates if kernel version checking for eBPF support is disabled noKernelVersionCheck bool - // uploadSymbols indicates whether automatic uploading of symbols is enabled - uploadSymbols bool // bpfVerifierLogLevel holds the defined log level of the eBPF verifier. // Currently there are three different log levels applied by the kernel verifier: // 0 - no logging @@ -143,14 +150,14 @@ var configurationSet = false func SetConfiguration(conf *Config) error { var err error - projectID = conf.ProjectID + version = conf.Version + revision = conf.Revision + buildTimestamp = conf.BuildTimestamp - if projectID == 0 || projectID > 4095 { - return fmt.Errorf("invalid project id %d (need > 0 and < 4096)", projectID) - } + projectID = conf.ProjectID if conf.SecretToken == "" { - return fmt.Errorf("missing SecretToken") + return errors.New("missing SecretToken") } secretToken = conf.SecretToken @@ -179,7 +186,7 @@ func SetConfiguration(conf *Config) error { return fmt.Errorf("invalid machine ID '%s': %s", conf.MachineID, err) } if machineID == 0 { - return fmt.Errorf( + return errors.New( "the machine ID must be specified with the environment (and non-zero)") } log.Debugf("User provided environment (%s) and machine ID (0x%x)", environment, @@ -187,7 +194,7 @@ func SetConfiguration(conf *Config) error { setEnvironment(environment) hostID = machineID } else if conf.MachineID != "" { - return fmt.Errorf("you can only specify the machine ID if you also provide the environment") + return errors.New("you can only specify the machine ID if you also provide the environment") } cacheDirectory = conf.CacheDirectory @@ -202,7 +209,6 @@ func SetConfiguration(conf *Config) error { configurationFile = conf.ConfigurationFile disableTLS = conf.DisableTLS noKernelVersionCheck = conf.NoKernelVersionCheck - uploadSymbols = conf.UploadSymbols tracers = conf.Tracers startTime = conf.StartTime mapScaleFactor = conf.MapScaleFactor @@ -224,6 +230,21 @@ func SetConfiguration(conf *Config) error { return nil } +// Version returns the version of the agent. +func Version() string { + return version +} + +// Revision returns the revision of the agent. +func Revision() string { + return revision +} + +// BuildTimestamp returns the build timestamp of the agent. +func BuildTimestamp() string { + return buildTimestamp +} + // SamplesPerSecond returns the configured samples per second. func SamplesPerSecond() uint16 { return samplesPerSecond @@ -244,14 +265,13 @@ func MaxElementsPerInterval() uint32 { // non-optimal map sizing (reduced cache_hit:cache_miss ratio and increased processing overhead). // Simply increasing traceCacheIntervals is problematic when maxElementsPerInterval is large // (e.g. too many CPU cores present) as we end up using too much memory. A minimum size is -// therefore used here. Also see: -// https://github.com/elastic/otel-profiling-agent/pull/2120#issuecomment-1043024813 +// therefore used here. func TraceCacheEntries() uint32 { size := maxElementsPerInterval * uint32(traceCacheIntervals) if size < traceCacheMinSize { size = traceCacheMinSize } - return libpf.NextPowerOfTwo(size) + return util.NextPowerOfTwo(size) } // Verbose indicates if the agent is running with verbose enabled. @@ -290,7 +310,15 @@ func SecretToken() string { } // CacheDirectory returns the cacheDirectory. +// It has a default value of /var/cache/otel/profiling-agent, but this can be changed via +// the configuration file. func CacheDirectory() string { + // Even though the cache directory has a default value, we don't want the config being + // used unless a configuration file has been read. + if !configurationSet { + log.Fatal("Cannot access CacheDirectory. Configuration has not been read") + } + return cacheDirectory } @@ -309,7 +337,7 @@ func CollectionAgentAddr() string { return collectionAgentAddr } -// Path to profiling agent's configuration file +// Path to the profiling agent's configuration file func ConfigurationFile() string { return configurationFile } @@ -324,11 +352,6 @@ func NoKernelVersionCheck() bool { return noKernelVersionCheck } -// Indicates whether automatic uploading of symbols is enabled -func UploadSymbols() bool { - return uploadSymbols -} - // User-specified tracers to enable func Tracers() string { return tracers diff --git a/config/config_test.go b/config/config_test.go index aa242d82..2179e604 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -9,13 +9,13 @@ package config import ( "os" "testing" + + "github.com/stretchr/testify/require" ) func TestSetConfiguration(t *testing.T) { cwd, err := os.Getwd() - if err != nil { - t.Fatalf("Failed to get current working directory: %v", err) - } + require.NoError(t, err) cfg := Config{ ProjectID: 42, @@ -28,36 +28,26 @@ func TestSetConfiguration(t *testing.T) { // Test setting environment to "aws". err = SetConfiguration(&cfg) - if err != nil { - t.Fatalf("failure to set environment to \"aws\"") - } + require.NoError(t, err) cfg2 := cfg cfg2.EnvironmentType = "bla" err = SetConfiguration(&cfg2) - if err == nil { - t.Fatalf("expected failure using invalid environment (%s)", err) - } + require.Error(t, err) cfg3 := cfg cfg3.MachineID = "" err = SetConfiguration(&cfg3) - if err == nil { - t.Fatalf("expected failure using empty machineID for environment (%s)", err) - } + require.Error(t, err) cfg4 := cfg cfg4.EnvironmentType = "" err = SetConfiguration(&cfg4) - if err == nil { - t.Fatalf("expected failure using empty environment for machineID (%s)", err) - } + require.Error(t, err) cfg5 := cfg cfg5.EnvironmentType = "aws" cfg5.SecretToken = "" err = SetConfiguration(&cfg5) - if err == nil { - t.Fatalf("expected failure using empty secretToken for environment") - } + require.Error(t, err) } diff --git a/config/env.go b/config/env.go index 4188911c..035f9c5b 100644 --- a/config/env.go +++ b/config/env.go @@ -7,8 +7,10 @@ package config import ( + "context" "encoding/binary" "encoding/json" + "errors" "fmt" "io" "net" @@ -20,16 +22,18 @@ import ( "sync" "time" + "github.com/elastic/otel-profiling-agent/pfnamespaces" + "github.com/elastic/otel-profiling-agent/util" + "github.com/jsimonetti/rtnetlink" "golang.org/x/sys/unix" gcp "cloud.google.com/go/compute/metadata" - awsec2 "github.com/aws/aws-sdk-go/aws/ec2metadata" - awssession "github.com/aws/aws-sdk-go/aws/session" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + ec2imds "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" log "github.com/sirupsen/logrus" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/pfnamespaces" ) // EnvironmentType indicates the environment, the agent is running on. @@ -134,11 +138,12 @@ func checkCGroups() (EnvironmentType, error) { return envUnspec, fmt.Errorf("failed to read /proc/1/cgroup: %s", err) } - if strings.Contains(data, "docker") { + switch { + case strings.Contains(data, "docker"): return envDocker, nil - } else if strings.Contains(data, "lxc") { + case strings.Contains(data, "lxc"): return envLXC, nil - } else if strings.Contains(data, "kvm") { + case strings.Contains(data, "kvm"): return envKVM, nil } @@ -167,7 +172,7 @@ func getInterfaceIndexFromIP(conn *rtnetlink.Conn, ip net.IP) (index int, err er return -1, fmt.Errorf("failed to get route: %s", err) } if len(msgs) == 0 { - return -1, fmt.Errorf("empty routing table") + return -1, errors.New("empty routing table") } return int(msgs[0].Attributes.OutIface), nil @@ -187,13 +192,13 @@ func getMACFromRouting(destination string) (mac uint64, err error) { return 0, fmt.Errorf("failed to look up IP: %s", err) } if len(addrs) == 0 { - return 0, fmt.Errorf("failed to look up IP: no address") + return 0, errors.New("failed to look up IP: no address") } // Dial a connection to the rtnetlink socket conn, err := rtnetlink.Dial(nil) if err != nil { - return 0, fmt.Errorf("failed to connect to netlink layer") + return 0, errors.New("failed to connect to netlink layer") } defer conn.Close() @@ -224,7 +229,7 @@ func getMACFromRouting(destination string) (mac uint64, err error) { return hwAddrToUint64(hwAddr), nil } - return 0, fmt.Errorf("failed to retrieve MAC from routing interface") + return 0, errors.New("failed to retrieve MAC from routing interface") } // getMACFromSystem returns a MAC address by iterating over all system @@ -270,7 +275,7 @@ func getMACFromSystem() (mac uint64, err error) { return macs[0], nil } - return 0, fmt.Errorf("failed to find a MAC") + return 0, errors.New("failed to find a MAC") } // getNonCloudEnvironmentAndMachineID tries to detect if the agent is running in a @@ -330,7 +335,7 @@ func getNonCloudEnvironmentAndMachineID() (uint64, EnvironmentType, error) { // idFromString generates a number, that will be part of the hostID, from a given string. func idFromString(s string) uint64 { - return libpf.HashString(s) + return util.HashString(s) } // gcpInfo collects information about the GCP environment @@ -345,11 +350,17 @@ func gcpInfo() (uint64, EnvironmentType, error) { // awsInfo collects information about the AWS environment func awsInfo() (uint64, EnvironmentType, error) { - svc := awsec2.New(awssession.Must(awssession.NewSession())) - document, err := svc.GetInstanceIdentityDocument() + cfg, err := awsconfig.LoadDefaultConfig(context.Background()) if err != nil { - return 0, envAWS, fmt.Errorf("failed to get aws metadata: %w", err) + return 0, envAWS, fmt.Errorf("failed to prepare aws configuration: %v", err) } + + client := ec2imds.NewFromConfig(cfg) + document, err := client.GetInstanceIdentityDocument(context.Background(), nil) + if err != nil { + return 0, envAWS, fmt.Errorf("failed to fetch instance document: %v", err) + } + return idFromString(document.InstanceID), envAWS, nil } diff --git a/config/times.go b/config/times.go index 0118e1d1..601aa4fb 100644 --- a/config/times.go +++ b/config/times.go @@ -7,25 +7,57 @@ package config import ( + "context" + "math" + "runtime" + "sort" + "sync/atomic" "time" log "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" + + "github.com/elastic/otel-profiling-agent/periodiccaller" + "github.com/elastic/otel-profiling-agent/util" ) -var times = Times{ - reportMetricsInterval: 1 * time.Minute, +const ( + // Number of timing samples to use when retrieving system boot time. + sampleSize = 5 + // In the kernel, we retrieve timestamps from a monotonic clock + // (bpf_ktime_get_ns) that does not count system suspend time. + // In userspace, we try to detect system suspend events by diffing + // with values retrieved from a monotonic clock that does count + // system suspend time. If the delta exceeds the following threshold + // (10s), we add it to the monotonic 'delta' that we keep track of. + monotonicThresholdNs = 10 * 1e9 + // GRPCAuthErrorDelay defines the delay before triggering a global process exit after a // gRPC auth error. - grpcAuthErrorDelay: 10 * time.Minute, + GRPCAuthErrorDelay = 10 * time.Minute // GRPCConnectionTimeout defines the timeout for each established gRPC connection. - grpcConnectionTimeout: 3 * time.Second, + GRPCConnectionTimeout = 3 * time.Second // GRPCOperationTimeout defines the timeout for each gRPC operation. - grpcOperationTimeout: 5 * time.Second, + GRPCOperationTimeout = 5 * time.Second // GRPCStartupBackoffTimeout defines the time between failed gRPC requests during startup // phase. - grpcStartupBackoffTimeout: 1 * time.Minute, + GRPCStartupBackoffTimeout = 1 * time.Minute +) + +var ( + monotonicDeltaNs atomic.Int64 + monotonicSyncInterval = 1 * time.Minute +) + +var times = Times{ + reportMetricsInterval: 1 * time.Minute, + grpcAuthErrorDelay: GRPCAuthErrorDelay, + grpcConnectionTimeout: GRPCConnectionTimeout, + grpcOperationTimeout: GRPCOperationTimeout, + grpcStartupBackoffTimeout: GRPCStartupBackoffTimeout, pidCleanupInterval: 5 * time.Minute, tracePollInterval: 250 * time.Millisecond, + bootTimeUnixNano: getBootTimeUnixNano(), } // Compile time check for interface adherence @@ -44,6 +76,7 @@ type Times struct { grpcAuthErrorDelay time.Duration pidCleanupInterval time.Duration probabilisticInterval time.Duration + bootTimeUnixNano int64 } // IntervalsAndTimers is a meta interface that exists purely to document its functionality. @@ -73,6 +106,10 @@ type IntervalsAndTimers interface { // ProbabilisticInterval defines the interval for which probabilistic profiling will // be enabled or disabled. ProbabilisticInterval() time.Duration + // BootTimeUnixNano defines the system boot time in nanoseconds since the epoch. This value + // can be used to convert monotonic time (e.g. util.GetKTime) to Unix time, by adding it + // as a delta. + BootTimeUnixNano() int64 } func (t *Times) MonitorInterval() time.Duration { return t.monitorInterval } @@ -95,6 +132,10 @@ func (t *Times) PIDCleanupInterval() time.Duration { return t.pidCleanupInterval func (t *Times) ProbabilisticInterval() time.Duration { return t.probabilisticInterval } +func (t *Times) BootTimeUnixNano() int64 { + return t.bootTimeUnixNano + monotonicDeltaNs.Load() +} + // GetTimes provides access to all timers and intervals. func GetTimes() *Times { if !configurationSet { @@ -102,3 +143,85 @@ func GetTimes() *Times { } return × } + +// StartMonotonicSync starts a goroutine that periodically calculates a delta +// between the two monotonic clocks (CLOCK_MONOTONIC and CLOCK_BOOTTIME). This +// delta can be introduced by system suspend events. For more information, see +// clock_gettime(2). +func StartMonotonicSync(ctx context.Context) { + initialDelta := getDelta(0) + log.Debugf("Initial monotonic clock delta: %v", initialDelta) + + periodiccaller.Start(ctx, monotonicSyncInterval, func() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + minDelta := int64(math.MaxInt64) + for i := 0; i < sampleSize; i++ { + d := getDelta(initialDelta) + if d < 0 { + d = -d + } + // We're interested in the minimum absolute delta between the two clocks + if d < minDelta { + minDelta = d + } + } + + if minDelta >= monotonicThresholdNs { + monotonicDeltaNs.Add(minDelta) + } + }) +} + +func getDelta(compensationValue int64) int64 { + var ts unix.Timespec + + // Does not include suspend time + kt := int64(util.GetKTime()) + // Does include suspend time + if err := unix.ClockGettime(unix.CLOCK_BOOTTIME, &ts); err != nil { + // This should never happen in our target environments. + return 0 + } + + delta := (kt + monotonicDeltaNs.Load()) - ts.Nano() - compensationValue + + return delta +} + +// getBootTimeUnixNano returns system boot time in nanoseconds since the +// epoch, temporarily locking the calling goroutine to its OS thread. +func getBootTimeUnixNano() int64 { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + samples := make([]struct { + t1 time.Time + ktime int64 + t2 time.Time + }, sampleSize) + + for i := range samples { + // To avoid noise from scheduling / other delays, we perform a + // series of measurements and pick the one with lowest delta. + samples[i].t1 = time.Now() + samples[i].ktime = int64(util.GetKTime()) + samples[i].t2 = time.Now() + } + + sort.Slice(samples, func(i, j int) bool { + di := samples[i].t2.UnixNano() - samples[i].t1.UnixNano() + dj := samples[j].t2.UnixNano() - samples[j].t1.UnixNano() + if di < 0 { + di = -di + } + if dj < 0 { + dj = -dj + } + return di < dj + }) + + // This should never be negative, as t1.UnixNano() >> ktime + return samples[0].t1.UnixNano() - samples[0].ktime +} diff --git a/config/tracer.go b/config/tracer.go index d9141ec6..2d1755b5 100644 --- a/config/tracer.go +++ b/config/tracer.go @@ -6,55 +6,156 @@ package config -// TracerType values identify tracers, such as the native code tracer, or PHP tracer -type TracerType int +import ( + "fmt" + "runtime" + "strings" + + log "github.com/sirupsen/logrus" +) + +// tracerType values identify tracers, such as the native code tracer, or PHP tracer +type tracerType int const ( - PerlTracer TracerType = iota + PerlTracer tracerType = iota PHPTracer PythonTracer HotspotTracer RubyTracer V8Tracer + DotnetTracer - // MaxTracers indicates the max. number of different tracers - MaxTracers + // maxTracers indicates the max. number of different tracers + maxTracers ) -var tracerTypeToString = map[TracerType]string{ +var tracerTypeToName = map[tracerType]string{ PerlTracer: "perl", PHPTracer: "php", PythonTracer: "python", HotspotTracer: "hotspot", RubyTracer: "ruby", V8Tracer: "v8", + DotnetTracer: "dotnet", } -// allTracers is returned by a call to AllTracers(). To avoid allocating memory every time the -// function is called we keep the returned array outside of the function. -var allTracers []TracerType +var tracerNameToType = make(map[string]tracerType, maxTracers) -// AllTracers returns a slice containing all supported tracers. -func AllTracers() []TracerType { - // As allTracers is not immutable we first check if it still holds all - // expected values before returning it. - if len(allTracers) != int(MaxTracers) { - allTracers = make([]TracerType, MaxTracers) +func init() { + for k, v := range tracerTypeToName { + tracerNameToType[v] = k } +} - for i := 0; i < int(MaxTracers); i++ { - if allTracers[i] != TracerType(i) { - allTracers[i] = TracerType(i) - } - } - return allTracers +// tracerTypeFromName returns the tracer type for the given name. +func tracerTypeFromName(s string) (tracerType, bool) { + tt, ok := tracerNameToType[s] + return tt, ok } -// GetString converts the tracer type t to its related string value. -func (t TracerType) GetString() string { - if result, ok := tracerTypeToString[t]; ok { +// String returns the tracer's name. +// It returns '' in case the tracer is unknown. +func (t tracerType) String() string { + if result, ok := tracerTypeToName[t]; ok { return result } return "" } + +// IncludedTracers holds information about which tracers are enabled. +type IncludedTracers uint16 + +// String returns a comma-separated list of enabled tracers. +func (t *IncludedTracers) String() string { + var names []string + for tracer := range maxTracers { + if t.Has(tracer) { + names = append(names, tracer.String()) + } + } + return strings.Join(names, ",") +} + +// Has returns true if the given tracer is enabled. +func (t *IncludedTracers) Has(tracer tracerType) bool { + return *t&(1< container ID cache. + // The timeout exists to avoid collisions in case of PID reuse. + containerIDCacheTimeout = 1 * time.Minute + + // deferredTimeout is the timeout to prevent busy loops. + deferredTimeout = 1 * time.Minute + + // deferredLRUSize defines the size of LRUs deferring look ups. + deferredLRUSize = 8192 ) var ( @@ -74,7 +84,7 @@ var ( `\d+:.*:/.*/*kubepods.*?/[^/]+/docker-([0-9a-f]{64})`) // The systemd cgroupDriver needs a different regex pattern: systemdKubePattern = regexp.MustCompile(`\d+:.*:/.*/*kubepods-.*([0-9a-f]{64})`) - dockerPattern = regexp.MustCompile(`\d+:.*:/.*/*docker[-|/]([0-9a-f]{64})`) + dockerPattern = regexp.MustCompile(`\d+:.*:/.*?/*docker[-|/]([0-9a-f]{64})`) dockerBuildkitPattern = regexp.MustCompile(`\d+:.*:/.*/*docker/buildkit/([0-9a-z]+)`) lxcPattern = regexp.MustCompile(`\d+::/lxc\.(monitor|payload)\.([a-zA-Z]+)/`) containerdPattern = regexp.MustCompile(`\d+:.+:/([a-zA-Z0-9_-]+)/+([a-zA-Z0-9_-]+)`) @@ -82,10 +92,19 @@ var ( containerIDPattern = regexp.MustCompile(`.+://([0-9a-f]{64})`) cgroup = "/proc/%d/cgroup" + + ErrDeferred = errors.New("lookup deferred due to previous failure") ) -// Handler does the retrieval of container metadata for a particular pid. -type Handler struct { +// Handler implementations support retrieving container metadata for a particular PID. +type Handler interface { + // GetContainerMetadata returns the pod name and container name metadata associated with the + // provided pid. Returns an empty object if no container metadata exists. + GetContainerMetadata(pid util.PID) (ContainerMetadata, error) +} + +// handler does the retrieval of container metadata for a particular pid. +type handler struct { // Counters to keep track how often external APIs are called. kubernetesClientQueryCount atomic.Uint64 dockerClientQueryCount atomic.Uint64 @@ -98,12 +117,15 @@ type Handler struct { containerMetadataCache *lru.SyncedLRU[string, ContainerMetadata] // containerIDCache stores per process container ID information. - containerIDCache *lru.SyncedLRU[libpf.OnDiskFileIdentifier, containerIDEntry] + containerIDCache *lru.SyncedLRU[util.PID, containerIDEntry] kubeClientSet kubernetes.Interface dockerClient *client.Client containerdClient *containerd.Client + + // deferredPID prevents busy loops for PIDs where the cgroup extraction fails. + deferredPID *lru.SyncedLRU[util.PID, libpf.Void] } // ContainerMetadata contains the container and/or pod metadata. @@ -145,19 +167,27 @@ type containerIDEntry struct { } // GetHandler returns a new Handler instance used for retrieving container metadata. -func GetHandler(ctx context.Context, monitorInterval time.Duration) (*Handler, error) { - containerIDCache, err := lru.NewSynced[libpf.OnDiskFileIdentifier, containerIDEntry]( - containerIDCacheSize, libpf.OnDiskFileIdentifier.Hash32) +func GetHandler(ctx context.Context, monitorInterval time.Duration) (Handler, error) { + containerIDCache, err := lru.NewSynced[util.PID, containerIDEntry]( + containerIDCacheSize, util.PID.Hash32) if err != nil { return nil, fmt.Errorf("unable to create container id cache: %v", err) } + containerIDCache.SetLifetime(containerIDCacheTimeout) - instance := &Handler{ + instance := &handler{ containerIDCache: containerIDCache, dockerClient: getDockerClient(), containerdClient: getContainerdClient(), } + instance.deferredPID, err = lru.NewSynced[util.PID, libpf.Void](deferredLRUSize, + util.PID.Hash32) + if err != nil { + return nil, err + } + instance.deferredPID.SetLifetime(deferredTimeout) + if os.Getenv(kubernetesServiceHost) != "" { err = createKubernetesClient(ctx, instance) if err != nil { @@ -200,7 +230,7 @@ func GetHandler(ctx context.Context, monitorInterval time.Duration) (*Handler, e // getPodsPerNode returns the number of pods per node. // Depending on the configuration of the kubernetes environment, we may not be allowed to query // for the allocatable information of the nodes. -func getPodsPerNode(ctx context.Context, instance *Handler) (int, error) { +func getPodsPerNode(ctx context.Context, instance *handler) (int, error) { instance.kubernetesClientQueryCount.Add(1) nodeList, err := instance.kubeClientSet.CoreV1().Nodes().List(ctx, v1.ListOptions{ FieldSelector: "spec.nodeName=" + instance.nodeName, @@ -211,7 +241,7 @@ func getPodsPerNode(ctx context.Context, instance *Handler) (int, error) { } if len(nodeList.Items) == 0 { - return 0, fmt.Errorf("empty node list") + return 0, errors.New("empty node list") } // With the ListOptions filter in place, there should be only one node listed in the @@ -225,7 +255,7 @@ func getPodsPerNode(ctx context.Context, instance *Handler) (int, error) { return int(quantity.Value()), nil } -func getContainerMetadataCache(ctx context.Context, instance *Handler) ( +func getContainerMetadataCache(ctx context.Context, instance *handler) ( *lru.SyncedLRU[string, ContainerMetadata], error) { cacheSize := containerMetadataCacheSize @@ -240,7 +270,7 @@ func getContainerMetadataCache(ctx context.Context, instance *Handler) ( uint32(cacheSize), hashString) } -func createKubernetesClient(ctx context.Context, instance *Handler) error { +func createKubernetesClient(ctx context.Context, instance *handler) error { log.Debugf("Create Kubernetes client") config, err := rest.InClusterConfig() @@ -287,7 +317,7 @@ func createKubernetesClient(ctx context.Context, instance *Handler) error { } instance.putCache(pod) }, - UpdateFunc: func(oldObj any, newObj any) { + UpdateFunc: func(_ any, newObj any) { pod, ok := newObj.(*corev1.Pod) if !ok { log.Errorf("Received unknown object in UpdateFunc handler: %#v", @@ -301,7 +331,7 @@ func createKubernetesClient(ctx context.Context, instance *Handler) error { return fmt.Errorf("failed to attach event handler: %v", err) } - // Shutdown the informer when the context attached to this Handler expires + // Shutdown the informer when the context attached to this handler expires stopper := make(chan struct{}) go func() { <-ctx.Done() @@ -360,7 +390,7 @@ func getDockerClient() *client.Client { } // putCache updates the container id metadata cache for the provided pod. -func (h *Handler) putCache(pod *corev1.Pod) { +func (h *handler) putCache(pod *corev1.Pod) { log.Debugf("Update container metadata cache for pod %s", pod.Name) podName := getPodName(pod) @@ -427,15 +457,14 @@ func getNodeName() (string, error) { // Therefore, we check for both environment variables. nodeName = os.Getenv(genericNodeName) if nodeName == "" { - return "", fmt.Errorf("kubernetes node name not configured") + return "", errors.New("kubernetes node name not configured") } return nodeName, nil } -// GetContainerMetadata returns the pod name and container name metadata associated with the -// provided pid. Returns an empty object if no container metadata exists. -func (h *Handler) GetContainerMetadata(pid libpf.PID) (ContainerMetadata, error) { +// GetContainerMetadata implements the Handler interface. +func (h *handler) GetContainerMetadata(pid util.PID) (ContainerMetadata, error) { // Fast path, check container metadata has been cached // For kubernetes pods, the shared informer may have updated // the container id to container metadata cache, so retrieve the container ID for this pid. @@ -457,13 +486,14 @@ func (h *Handler) GetContainerMetadata(pid libpf.PID) (ContainerMetadata, error) // trace but the shared informer has been delayed in updating the container id metadata cache. // If it is not a kubernetes pod then we need to look up the container id in the configured // client. - if isContainerEnvironment(env, envKubernetes) && h.kubeClientSet != nil { + switch { + case isContainerEnvironment(env, envKubernetes) && h.kubeClientSet != nil: return h.getKubernetesPodMetadata(pidContainerID) - } else if isContainerEnvironment(env, envDocker) && h.dockerClient != nil { + case isContainerEnvironment(env, envDocker) && h.dockerClient != nil: return h.getDockerContainerMetadata(pidContainerID) - } else if isContainerEnvironment(env, envContainerd) && h.containerdClient != nil { + case isContainerEnvironment(env, envContainerd) && h.containerdClient != nil: return h.getContainerdContainerMetadata(pidContainerID) - } else if isContainerEnvironment(env, envDockerBuildkit) { + case isContainerEnvironment(env, envDockerBuildkit): // If DOCKER_BUILDKIT is set we can not retrieve information about this container // from the docker socket. Therefore, we populate container ID and container name // with the information we have. @@ -471,19 +501,20 @@ func (h *Handler) GetContainerMetadata(pid libpf.PID) (ContainerMetadata, error) containerID: pidContainerID, ContainerName: pidContainerID, }, nil - } else if isContainerEnvironment(env, envLxc) { + case isContainerEnvironment(env, envLxc): // As lxc does not use different identifiers we populate container ID and container // name of metadata with the same information. return ContainerMetadata{ containerID: pidContainerID, ContainerName: pidContainerID, }, nil + default: + return ContainerMetadata{}, + fmt.Errorf("failed to handle unknown container technology %d", env) } - - return ContainerMetadata{}, fmt.Errorf("failed to handle unknown container technology %d", env) } -func (h *Handler) getKubernetesPodMetadata(pidContainerID string) ( +func (h *handler) getKubernetesPodMetadata(pidContainerID string) ( ContainerMetadata, error) { log.Debugf("Get kubernetes pod metadata for container id %v", pidContainerID) @@ -525,7 +556,7 @@ func (h *Handler) getKubernetesPodMetadata(pidContainerID string) ( "containerID '%v' in %d pods", pidContainerID, len(pods.Items)) } -func (h *Handler) getDockerContainerMetadata(pidContainerID string) ( +func (h *handler) getDockerContainerMetadata(pidContainerID string) ( ContainerMetadata, error) { log.Debugf("Get docker container metadata for container id %v", pidContainerID) @@ -550,11 +581,11 @@ func (h *Handler) getDockerContainerMetadata(pidContainerID string) ( } return ContainerMetadata{}, - fmt.Errorf("failed to find matching container metadata for containerID, %v", + fmt.Errorf("failed to find matching docker container metadata for containerID, %v", pidContainerID) } -func (h *Handler) getContainerdContainerMetadata(pidContainerID string) ( +func (h *handler) getContainerdContainerMetadata(pidContainerID string) ( ContainerMetadata, error) { log.Debugf("Get containerd container metadata for container id %v", pidContainerID) @@ -590,32 +621,30 @@ func (h *Handler) getContainerdContainerMetadata(pidContainerID string) ( } return ContainerMetadata{}, - fmt.Errorf("failed to find matching container metadata for containerID, %v", + fmt.Errorf("failed to find matching containerd container metadata for containerID, %v", pidContainerID) } // lookupContainerID looks up a process ID from the host PID namespace, // returning its container ID and the used container technology. -func (h *Handler) lookupContainerID(pid libpf.PID) (containerID string, env containerEnvironment, +func (h *handler) lookupContainerID(pid util.PID) (containerID string, env containerEnvironment, err error) { - cgroupFilePath := fmt.Sprintf(cgroup, pid) - - fileIdentifier, err := libpf.GetOnDiskFileIdentifier(cgroupFilePath) - if err != nil { - return "", envUndefined, nil + if entry, exists := h.containerIDCache.Get(pid); exists { + return entry.containerID, entry.env, nil } - if entry, exists := h.containerIDCache.Get(fileIdentifier); exists { - return entry.containerID, entry.env, nil + if _, exists := h.deferredPID.Get(pid); exists { + return "", envUndefined, ErrDeferred } - containerID, env, err = h.extractContainerIDFromFile(cgroupFilePath) + containerID, env, err = h.extractContainerIDFromFile(fmt.Sprintf(cgroup, pid)) if err != nil { + h.deferredPID.Add(pid, libpf.Void{}) return "", envUndefined, err } // Store the result in the cache. - h.containerIDCache.Add(fileIdentifier, containerIDEntry{ + h.containerIDCache.Add(pid, containerIDEntry{ containerID: containerID, env: env, }) @@ -623,7 +652,7 @@ func (h *Handler) lookupContainerID(pid libpf.PID) (containerID string, env cont return containerID, env, nil } -func (h *Handler) extractContainerIDFromFile(cgroupFilePath string) ( +func (h *handler) extractContainerIDFromFile(cgroupFilePath string) ( containerID string, env containerEnvironment, err error) { f, err := os.Open(cgroupFilePath) if err != nil { diff --git a/containermetadata/containermetadata_test.go b/containermetadata/containermetadata_test.go index b646734a..5180af31 100644 --- a/containermetadata/containermetadata_test.go +++ b/containermetadata/containermetadata_test.go @@ -8,6 +8,7 @@ package containermetadata import ( "context" + "errors" "fmt" "os" "strconv" @@ -23,15 +24,24 @@ import ( "k8s.io/client-go/kubernetes/fake" "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestExtractContainerIDFromFile(t *testing.T) { + containerIDCache, err := lru.NewSynced[util.PID, containerIDEntry]( + containerIDCacheSize, util.PID.Hash32) + require.NoError(t, err) + tests := []struct { name string cgroupname string expContainerID string - pid libpf.PID + pid util.PID expEnv containerEnvironment + customHandler *handler }{ { name: "dockerv1", @@ -123,15 +133,22 @@ func TestExtractContainerIDFromFile(t *testing.T) { expContainerID: "vy53ljgivqn5q9axwrx1mf40l", expEnv: envDockerBuildkit, }, - } + { + name: "minikube", + cgroupname: "testdata/cgroupv2minikube-docker", + expContainerID: "90b200f66e7a7c6d3ee264d905001c37b7dd9d08e2d35aa669c2a8b092fe1a64", + expEnv: envDocker, + customHandler: &handler{ + containerIDCache: containerIDCache, - containerIDCache, err := lru.NewSynced[libpf.OnDiskFileIdentifier, containerIDEntry]( - containerIDCacheSize, libpf.OnDiskFileIdentifier.Hash32) - if err != nil { - t.Fatalf("failed to provide cache: %v", err) + // In minikube environment k8 client is not available + dockerClient: &client.Client{}, + containerdClient: &containerd.Client{}, + }, + }, } - h := &Handler{ + defaultHandler := &handler{ containerIDCache: containerIDCache, // Use dummy clients to trigger the regex match in the test. @@ -143,19 +160,14 @@ func TestExtractContainerIDFromFile(t *testing.T) { for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - containerID, env, err := h.extractContainerIDFromFile(test.cgroupname) - if err != nil { - t.Fatal(err) - } - if test.expContainerID != containerID { - t.Fatalf("expected containerID %v but found %v", - test.expContainerID, containerID) - } - - if test.expEnv != env { - t.Fatalf("expected container technology %v but got %v", - test.expEnv, env) + h := defaultHandler + if test.customHandler != nil { + h = test.customHandler } + containerID, env, err := h.extractContainerIDFromFile(test.cgroupname) + require.NoError(t, err) + assert.Equal(t, test.expContainerID, containerID) + assert.Equal(t, test.expEnv, env) }) } } @@ -165,7 +177,7 @@ func TestGetKubernetesPodMetadata(t *testing.T) { tests := []struct { name string clientset kubernetes.Interface - pid libpf.PID + pid util.PID expContainerID string expContainerName string expPodName string @@ -250,7 +262,7 @@ func TestGetKubernetesPodMetadata(t *testing.T) { }), pid: 1, expContainerID: "ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997", - err: fmt.Errorf("failed to get kubernetes pod metadata, failed to " + + err: errors.New("failed to get kubernetes pod metadata, failed to " + "find matching kubernetes pod/container metadata for containerID, " + "ed89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe1997"), }, @@ -261,60 +273,40 @@ func TestGetKubernetesPodMetadata(t *testing.T) { t.Run(test.name, func(t *testing.T) { containerMetadataCache, err := lru.NewSynced[string, ContainerMetadata]( containerMetadataCacheSize, hashString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - containerIDCache, err := lru.NewSynced[libpf.OnDiskFileIdentifier, containerIDEntry]( - containerIDCacheSize, libpf.OnDiskFileIdentifier.Hash32) - if err != nil { - t.Fatalf("failed to provide cache: %v", err) - } + containerIDCache, err := lru.NewSynced[util.PID, containerIDEntry]( + containerIDCacheSize, util.PID.Hash32) + require.NoError(t, err) - instance := &Handler{ + instance := &handler{ containerMetadataCache: containerMetadataCache, kubeClientSet: test.clientset, dockerClient: nil, containerIDCache: containerIDCache, } + instance.deferredPID, err = lru.NewSynced[util.PID, libpf.Void](1024, + func(u util.PID) uint32 { return uint32(u) }) + require.NoError(t, err) cgroup = "testdata/cgroupv%dkubernetes" meta, err := instance.GetContainerMetadata(test.pid) - if err != nil { - if meta != (ContainerMetadata{}) { - t.Fatal("GetContainerMetadata errored but returned non-default object") - } - if test.err == nil { - t.Fatal(err) - } - } - - if meta.ContainerName != test.expContainerName { - t.Fatalf("expected container name %v but got %v", - test.expContainerName, meta.ContainerName) - } - if meta.PodName != test.expPodName { - t.Fatalf("expected pod name %v but got %v", test.expPodName, meta.PodName) + if test.err != nil { + require.Error(t, err) + assert.Equal(t, ContainerMetadata{}, meta) + } else { + require.NoError(t, err) } + assert.Equal(t, test.expContainerName, meta.ContainerName) + assert.Equal(t, test.expPodName, meta.PodName) if test.err == nil { // check the item has been added correctly to the container metadata cache value, ok := instance.containerMetadataCache.Get(test.expContainerID) - if !ok { - t.Fatal("container metadata should be in the container metadata cache") - } - if value.containerID != test.expContainerID { - t.Fatalf("expected container name %v but got %v", - test.expContainerID, value.containerID) - } - if value.ContainerName != test.expContainerName { - t.Fatalf("expected container name %v but got %v", - test.expContainerName, value.ContainerName) - } - if value.PodName != test.expPodName { - t.Fatalf("expected pod name %v but got %v", test.expPodName, - value.PodName) - } + assert.True(t, ok, "container metadata should be in the container metadata cache") + assert.Equal(t, test.expContainerID, value.containerID) + assert.Equal(t, test.expContainerName, value.ContainerName) + assert.Equal(t, test.expPodName, value.PodName) } }) } @@ -325,22 +317,22 @@ func BenchmarkGetKubernetesPodMetadata(b *testing.B) { clientset := fake.NewSimpleClientset() containerMetadataCache, err := lru.NewSynced[string, ContainerMetadata]( containerMetadataCacheSize, hashString) - if err != nil { - b.Fatal(err) - } + require.NoError(b, err) - containerIDCache, err := lru.NewSynced[libpf.OnDiskFileIdentifier, containerIDEntry]( - containerIDCacheSize, libpf.OnDiskFileIdentifier.Hash32) - if err != nil { - b.Fatalf("failed to provide cache: %v", err) - } + containerIDCache, err := lru.NewSynced[util.PID, containerIDEntry]( + containerIDCacheSize, util.PID.Hash32) + require.NoError(b, err) - instance := &Handler{ + instance := &handler{ containerMetadataCache: containerMetadataCache, kubeClientSet: clientset, dockerClient: nil, containerIDCache: containerIDCache, } + instance.deferredPID, err = lru.NewSynced[util.PID, libpf.Void](1024, + func(u util.PID) uint32 { return uint32(u) }) + require.NoError(b, err) + for j := 100; j < 700; j++ { testPod := fmt.Sprintf("testpod-abc%d", j) @@ -369,37 +361,27 @@ func BenchmarkGetKubernetesPodMetadata(b *testing.B) { } file, err := os.CreateTemp("", "test_containermetadata_cgroup*") - if err != nil { - b.Fatal(err) - } + require.NoError(b, err) defer os.Remove(file.Name()) // nolint: gocritic _, err = fmt.Fprintf(file, "0::/kubepods/besteffort/poda9c80282-3f6b-4d5b-84d5-a137a6668011/"+ "%dd89697807a981b82f6245ac3a13be232c1e13435d52bc3f53060d61babe19", j) - if err != nil { - b.Fatal(err) - } + require.NoError(b, err) cgroup = "/tmp/test_containermetadata_cgroup%d" opts := v1.CreateOptions{} clientsetPod, err := clientset.CoreV1().Pods("default").Create( context.Background(), pod, opts) - if err != nil { - b.Fatal(err) - } + require.NoError(b, err) instance.putCache(clientsetPod) split := strings.Split(file.Name(), "test_containermetadata_cgroup") pid, err := strconv.Atoi(split[1]) - if err != nil { - b.Fatal(err) - } + require.NoError(b, err) - _, err = instance.GetContainerMetadata(libpf.PID(pid)) - if err != nil { - b.Fatal(err) - } + _, err = instance.GetContainerMetadata(util.PID(pid)) + require.NoError(b, err) } } } diff --git a/containermetadata/testdata/cgroupv2minikube-docker b/containermetadata/testdata/cgroupv2minikube-docker new file mode 100644 index 00000000..e7be725d --- /dev/null +++ b/containermetadata/testdata/cgroupv2minikube-docker @@ -0,0 +1 @@ +0::/system.slice/docker-90b200f66e7a7c6d3ee264d905001c37b7dd9d08e2d35aa669c2a8b092fe1a64.scope/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod7099802f_73fa_458a_9941_d3aaae7df23d.slice/docker-b2c7024b6f161fb48efb73804d46c9cd4fa22c09c115976e311b5d3667955819.scope \ No newline at end of file diff --git a/debug/log/log.go b/debug/log/log.go deleted file mode 100644 index b157e3e4..00000000 --- a/debug/log/log.go +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -package log - -import ( - "github.com/sirupsen/logrus" -) - -const ( - PanicLevel = logrus.PanicLevel - FatalLevel = logrus.FatalLevel - ErrorLevel = logrus.ErrorLevel - WarnLevel = logrus.WarnLevel - InfoLevel = logrus.InfoLevel - DebugLevel = logrus.DebugLevel - - // time.RFC3339Nano removes trailing zeros from the seconds field. - // The following format doesn't (fixed-width output). - timeStampFormat = "2006-01-02T15:04:05.000000000Z07:00" -) - -type JSONFormatter struct { - formatter logrus.JSONFormatter - serviceName string -} - -func (l JSONFormatter) Format(entry *logrus.Entry) ([]byte, error) { - if l.serviceName != "" { - entry.Data["service.name"] = l.serviceName - } - return l.formatter.Format(entry) -} - -// The default logger sets properties to work with the rest of the platform: -// log collection happens from StdOut, with precise timestamps and full level names. -// This variable is a pointer to the logger singleton offered by the underlying library -// and should be shared across the whole application that wants to consume it, rather than copied. -var logger = StandardLogger() - -// StandardLogger provides a global instance of the logger used in this package: -// it should be the only logger used inside an application. -// This function mirrors the library API currently used in our codebase, applying -// default settings to the logger that conforms to the structured logging practices -// we want to adopt: always-quoted fields (for easier parsing), microsecond-resolution -// timestamps. -func StandardLogger() Logger { - l := logrus.StandardLogger() - // TextFormatter is the key/value pair format that allows for logs labeling; - // here we define the format that will have to be parsed by other components, - // updating these properties will require reviewing the rest of the log processing pipeline. - l.SetFormatter(&logrus.TextFormatter{ - DisableColors: true, - FullTimestamp: true, - ForceQuote: false, - TimestampFormat: timeStampFormat, - DisableSorting: true, - DisableLevelTruncation: true, - QuoteEmptyFields: true, - }) - // Default Level - l.SetLevel(InfoLevel) - // Allow concurrent writes to log destination (os.Stdout). - l.SetNoLock() - // Explicitly disable method/package fields to every message line, - // because there will be no use of them - l.SetReportCaller(false) - return l -} - -// Logger is the type to encapsulate structured logging, embeds the logging library interface. -type Logger interface { - logrus.FieldLogger -} - -// Labels to add key/value pairs to messages, to be used later in the pipeline for filtering. -type Labels map[string]any - -// With augments the structured log message using the provided key/value map, -// every entry will be written as a separate label, and we should avoid -// inserting values with unbound number of unique occurrences. -// Using high-cardinality values (in the order of tens of unique values) -// does not pose a performance problem when writing the logs, but when reading them. -// We risk hogging the parsing/querying part of the log pipeline, requiring high -// resource consumption when filtering based on many unique label values. -func With(labels Labels) Logger { - return logger.WithFields(logrus.Fields(labels)) -} - -// Printf mirrors the library function, using the global logger. -func Printf(format string, args ...any) { - logger.Printf(format, args...) -} - -// Fatalf mirrors the library function, using the global logger. -func Fatalf(format string, args ...any) { - logger.Fatalf(format, args...) -} - -// Errorf mirrors the library function, using the global logger. -func Errorf(format string, args ...any) { - logger.Errorf(format, args...) -} - -// Warnf mirrors the library function, using the global logger. -func Warnf(format string, args ...any) { - logger.Warnf(format, args...) -} - -// Infof mirrors the library function, using the global logger. -func Infof(format string, args ...any) { - logger.Infof(format, args...) -} - -// Debugf mirrors the library function, using the global logger. -func Debugf(format string, args ...any) { - logger.Debugf(format, args...) -} - -// Print mirrors the library function, using the global logger. -func Print(args ...any) { - logger.Print(args...) -} - -// Fatal mirrors the library function, using the global logger. -func Fatal(args ...any) { - logger.Fatal(args...) -} - -// Error mirrors the library function, using the global logger. -func Error(args ...any) { - logger.Error(args...) -} - -// Warn mirrors the library function, using the global logger. -func Warn(args ...any) { - logger.Warn(args...) -} - -// Info mirrors the library function, using the global logger. -func Info(args ...any) { - logger.Info(args...) -} - -// Debug mirrors the library function, using the global logger. -func Debug(args ...any) { - logger.Debug(args...) -} - -// SetLevel of the global logger. -func SetLevel(level logrus.Level) { - logger.(*logrus.Logger).SetLevel(level) -} - -// SetJSONFormatter replaces the default Formatter settings with the given ones. -func SetJSONFormatter(formatter logrus.JSONFormatter, serviceName string) { - logger.(*logrus.Logger).SetFormatter(JSONFormatter{ - formatter: formatter, - serviceName: serviceName, - }) -} diff --git a/debug/log/log_bench_test.go b/debug/log/log_bench_test.go deleted file mode 100644 index 4141135e..00000000 --- a/debug/log/log_bench_test.go +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -package log_test - -import ( - "testing" - - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - - "github.com/elastic/otel-profiling-agent/debug/log" -) - -func BenchmarkBaselineLogrus(b *testing.B) { - cases := []struct { - name string - run func(string) - }{ - {"Infof", func(s string) { logrus.Infof(s) }}, - {"With_Dot_Infof", func(s string) { - logrus.WithFields(logrus.Fields{"a": "b"}).Infof(s) - }}, - } - for i := range cases { - bench := cases[i] - b.Run(bench.name, func(b *testing.B) { - loggingCall(b, bench.run, logrus.StandardLogger()) - }) - } -} - -func BenchmarkLogger(b *testing.B) { - cases := []struct { - name string - run func(string) - }{ - {"Infof", - func(s string) { log.Infof(s) }, - }, - {"With_Dot_Infof", - func(s string) { - log.With(log.Labels{"a": "b"}).Infof(s) - }, - }, - } - for i := range cases { - bench := cases[i] - b.Run(bench.name, func(b *testing.B) { - loggingCall(b, bench.run, log.StandardLogger()) - }) - } -} - -func loggingCall(b *testing.B, underTest func(string), logger log.Logger) { - b.StopTimer() - output := setupLogger(logger, b) - for i := 0; i < b.N; i++ { - b.StartTimer() - underTest("AAA") - b.StopTimer() - if !assert.Contains(b, output.String(), "AAA") { - b.Fatalf("mismatch in output text") - } - } - output.Reset() -} diff --git a/debug/log/log_test.go b/debug/log/log_test.go deleted file mode 100644 index 39acef42..00000000 --- a/debug/log/log_test.go +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -package log_test - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "os" - "testing" - - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - - "github.com/elastic/otel-profiling-agent/debug/log" -) - -// SetLevel can be used to instruct the logger which type of levels should direct -// the log message to the log output. -func ExampleSetLevel() { - // Set the logger to DEBUG, store every message - log.SetLevel(log.DebugLevel) - log.Infof("This will be logged") - - log.SetLevel(log.ErrorLevel) - log.Infof("Now this will not be logged") -} - -// With can be used to add arbitrary key/value pairs (fields) to log messages, -// enabling fine-grained filtering based on fields' values. -func ExampleWith() { - aFile, err := os.CreateTemp("", "content_to_be_read") - if err != nil { - panic(err) - } - defer os.Remove(aFile.Name()) - contentOf := func(reader io.Reader) []byte { - b, err := io.ReadAll(reader) - if err != nil { - // We record in a log the read error, - // adding a label with the file name we failed to read. - log.With(log.Labels{"file_name": aFile.Name()}).Errorf("failed: %v", err) - return nil - } - return b - } - fmt.Fprint(os.Stdout, contentOf(aFile)) -} - -func TestLogging_sharedLoggerHasDefaults(t *testing.T) { - logger := log.StandardLogger() - assert.NotNil(t, logger) - assert.Equal(t, logger.(*logrus.Logger).Level, logrus.InfoLevel) - log.SetLevel(log.WarnLevel) - assert.Equal(t, logger.(*logrus.Logger).Level, logrus.WarnLevel) -} - -func TestLogging_logsHasRFC3339NanoTimestamp(t *testing.T) { - output := setupLogger(log.StandardLogger(), t) - log.Infof("Something: %s", "test") - assert.Regexp(t, `time="[0-9\-]+T[0-9:]+\.[0-9]{9}(\+|\-|Z)([0-9:]+)?"`, output.String()) -} - -func TestLogging_logLinesCanBeRecordedWithMultipleArgs(t *testing.T) { - output := setupLogger(log.StandardLogger(), t) - log.Infof("Something: %s - %d - %f", "test1", 2, 3.4) - assert.Contains(t, output.String(), - fmt.Sprintf(`msg="Something: %s - %d - %f"`, "test1", 2, 3.4)) -} - -// We want to test all levels but Fatalf requires a separate test -func TestLogging_leveledLoggerOnAllLevelsButFatal(t *testing.T) { - output := setupLogger(log.StandardLogger(), t) - tests := map[string]func(string, ...any){ - "fatal": log.Fatalf, - "error": log.Errorf, - "warning": log.Warnf, - "info": log.Infof, - "debug": log.Debugf, - } - for name, run := range tests { - level := name - run := run - t.Run(name, func(t *testing.T) { - run("%s-test", level) - assert.Contains(t, output.String(), fmt.Sprintf(`level=%s`, level)) - assert.Contains(t, output.String(), - fmt.Sprintf(`msg=%s-test`, level)) - }) - } -} - -func TestLogging_logWithLabels(t *testing.T) { - output := setupLogger(log.StandardLogger(), t) - log.With(log.Labels{ - "key": "val", - }).Infof("test") - assert.Contains(t, output.String(), fmt.Sprintf(`%s=%s`, "key", "val")) -} - -func TestLogging_logWithNumericLabelValues(t *testing.T) { - output := setupLogger(log.StandardLogger(), t) - log.With(log.Labels{ - "key": 123, - }).Infof("test") - assert.Contains(t, output.String(), fmt.Sprintf(`%s=%d`, "key", 123)) -} - -func TestLogging_logStateWithLabels(t *testing.T) { - output := setupLogger(log.StandardLogger(), t) - const emptyKey = "empty" - log.With(log.Labels{ - "key": "val", - emptyKey: "", - }).Infof("test") - assert.Contains(t, output.String(), fmt.Sprintf(`%s=%s`, "key", "val")) - // Ensure empty fields are in quotes for later parsing - assert.Contains(t, output.String(), fmt.Sprintf(`%s=""`, emptyKey)) -} - -func TestLogging_logJSONFormatter(t *testing.T) { - const ( - fieldKeyLevel = "log.level" - fieldKeyMsg = "message" - serviceName = "testService" - testMsg = "testMsg" - ) - - output := setupLogger(log.StandardLogger(), t) - - log.SetJSONFormatter( - logrus.JSONFormatter{ - DisableTimestamp: true, - FieldMap: logrus.FieldMap{ - logrus.FieldKeyLevel: fieldKeyLevel, - logrus.FieldKeyMsg: fieldKeyMsg, - }, - }, - serviceName) - - log.Info(testMsg) - - type JSONFormatterResult struct { - Level string `json:"log.level"` - Msg string `json:"message"` - Name string `json:"service.name"` - } - - var r JSONFormatterResult - err := json.NewDecoder(output).Decode(&r) - assert.Nil(t, err) - - assert.Equal(t, "info", r.Level) - assert.Equal(t, testMsg, r.Msg) - assert.Equal(t, serviceName, r.Name) -} - -func setupLogger(logger log.Logger, tb testing.TB) *bytes.Buffer { - b := bytes.NewBufferString("") - logger.(*logrus.Logger).Out = b - logger.(*logrus.Logger).SetLevel(logrus.DebugLevel) - logger.(*logrus.Logger).ExitFunc = func(int) { - if tb.Failed() { - tb.Fatalf("error running test %s", tb.Name()) - } - } - return b -} diff --git a/KNOWN_KERNEL_LIMITATIONS.md b/doc/KNOWN_KERNEL_LIMITATIONS.md similarity index 100% rename from KNOWN_KERNEL_LIMITATIONS.md rename to doc/KNOWN_KERNEL_LIMITATIONS.md diff --git a/docs/bpf-trace.drawio.svg b/doc/bpf-trace.drawio.svg similarity index 100% rename from docs/bpf-trace.drawio.svg rename to doc/bpf-trace.drawio.svg diff --git a/docs/devfiler.png b/doc/devfiler.png similarity index 100% rename from docs/devfiler.png rename to doc/devfiler.png diff --git a/docs/gopclntab.md b/doc/gopclntab.md similarity index 100% rename from docs/gopclntab.md rename to doc/gopclntab.md diff --git a/docs/network-trace.drawio.svg b/doc/network-trace.drawio.svg similarity index 100% rename from docs/network-trace.drawio.svg rename to doc/network-trace.drawio.svg diff --git a/docs/symb-proto/README.md b/doc/symb-proto/README.md similarity index 100% rename from docs/symb-proto/README.md rename to doc/symb-proto/README.md diff --git a/docs/symb-proto/symbfile.proto b/doc/symb-proto/symbfile.proto similarity index 100% rename from docs/symb-proto/symbfile.proto rename to doc/symb-proto/symbfile.proto diff --git a/docs/trace-pipe.drawio.svg b/doc/trace-pipe.drawio.svg similarity index 100% rename from docs/trace-pipe.drawio.svg rename to doc/trace-pipe.drawio.svg diff --git a/go.mod b/go.mod index 9b8b4a40..1346468c 100644 --- a/go.mod +++ b/go.mod @@ -1,81 +1,94 @@ module github.com/elastic/otel-profiling-agent -go 1.21.6 +go 1.22.4 require ( - cloud.google.com/go/compute/metadata v0.2.3 - github.com/DataDog/zstd v1.5.5 - github.com/aws/aws-sdk-go v1.50.5 - github.com/cespare/xxhash/v2 v2.2.0 - github.com/cilium/ebpf v0.12.3 - github.com/containerd/containerd v1.7.12 - github.com/docker/docker v25.0.1+incompatible - github.com/elastic/go-freelru v0.11.0 + cloud.google.com/go/compute/metadata v0.3.0 + github.com/aws/aws-sdk-go v1.54.4 + github.com/aws/aws-sdk-go-v2/config v1.27.20 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.7 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.165.0 + github.com/cespare/xxhash/v2 v2.3.0 + github.com/cilium/ebpf v0.15.0 + github.com/containerd/containerd v1.7.18 + github.com/docker/docker v27.0.0+incompatible + github.com/elastic/go-freelru v0.13.0 github.com/elastic/go-perf v0.0.0-20191212140718-9c656876f595 - github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 - github.com/jsimonetti/rtnetlink v1.4.1 - github.com/klauspost/cpuid/v2 v2.2.6 + github.com/jsimonetti/rtnetlink v1.4.2 + github.com/klauspost/compress v1.17.9 + github.com/klauspost/cpuid/v2 v2.2.8 github.com/minio/sha256-simd v1.0.1 github.com/peterbourgon/ff/v3 v3.4.0 - github.com/prometheus/procfs v0.12.0 + github.com/prometheus/procfs v0.15.1 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 + github.com/tklauser/go-sysconf v0.3.14 github.com/zeebo/xxh3 v1.0.2 - go.opentelemetry.io/proto/otlp v1.0.0 - go.uber.org/goleak v1.3.0 - go.uber.org/multierr v1.11.0 - golang.org/x/arch v0.7.0 - golang.org/x/sync v0.6.0 - golang.org/x/sys v0.16.0 - google.golang.org/grpc v1.61.0 - google.golang.org/protobuf v1.32.0 - k8s.io/api v0.29.1 - k8s.io/apimachinery v0.29.1 - k8s.io/client-go v0.29.1 + go.opentelemetry.io/otel v1.27.0 + go.opentelemetry.io/proto/otlp v1.3.1 + golang.org/x/arch v0.8.0 + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 + golang.org/x/sync v0.7.0 + golang.org/x/sys v0.21.0 + google.golang.org/grpc v1.64.0 + k8s.io/api v0.30.2 + k8s.io/apimachinery v0.30.2 + k8s.io/client-go v0.30.2 ) require ( - cloud.google.com/go/compute v1.23.3 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect - github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/Microsoft/hcsshim v0.11.4 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Microsoft/hcsshim v0.11.5 // indirect + github.com/aws/aws-sdk-go-v2 v1.29.0 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.11 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.11 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.21.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.29.0 // indirect + github.com/aws/smithy-go v1.20.2 // indirect github.com/containerd/cgroups v1.1.0 // indirect github.com/containerd/continuity v0.4.2 // indirect + github.com/containerd/errdefs v0.1.0 // indirect github.com/containerd/fifo v1.1.0 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/containerd/ttrpc v1.2.2 // indirect + github.com/containerd/ttrpc v1.2.4 // indirect github.com/containerd/typeurl/v2 v2.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/distribution/reference v0.5.0 // indirect + github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/felixge/httpsnoop v1.0.3 // indirect - github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/pprof v0.0.0-20230426061923-93006964c1fc // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/josharian/native v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.5 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.4.1 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/sys/mountinfo v0.6.2 // indirect github.com/moby/sys/sequential v0.5.0 // indirect @@ -87,34 +100,31 @@ require ( github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runtime-spec v1.1.0 // indirect github.com/opencontainers/selinux v1.11.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect - go.opentelemetry.io/otel v1.21.0 // indirect - go.opentelemetry.io/otel/metric v1.21.0 // indirect - go.opentelemetry.io/otel/sdk v1.21.0 // indirect - go.opentelemetry.io/otel/trace v1.21.0 // indirect - golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/oauth2 v0.14.0 // indirect - golang.org/x/term v0.16.0 // indirect - golang.org/x/text v0.14.0 // indirect + go.opentelemetry.io/otel/metric v1.27.0 // indirect + go.opentelemetry.io/otel/trace v1.27.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/oauth2 v0.20.0 // indirect + golang.org/x/term v0.18.0 // indirect + golang.org/x/text v0.15.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.16.1 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.1 // indirect - k8s.io/klog/v2 v2.110.1 // indirect - k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/klog/v2 v2.120.1 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect diff --git a/go.sum b/go.sum index e752d45c..a6714881 100644 --- a/go.sum +++ b/go.sum @@ -1,62 +1,88 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA= github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= -github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= -github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= -github.com/aws/aws-sdk-go v1.50.5 h1:H2Aadcgwr7a2aqS6ZwcE+l1mA6ZrTseYCvjw2QLmxIA= -github.com/aws/aws-sdk-go v1.50.5/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.11.5 h1:haEcLNpj9Ka1gd3B3tAEs9CpE0c+1IhoL59w/exYU38= +github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= +github.com/aws/aws-sdk-go v1.54.4 h1:xZga3fPu7uxVgh83DIaQlb7r0cixFx1xKiiROTWAhpU= +github.com/aws/aws-sdk-go v1.54.4/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.29.0 h1:uMlEecEwgp2gs6CsM6ugquNHr6mg0LHylPBR8u5Ojac= +github.com/aws/aws-sdk-go-v2 v1.29.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= +github.com/aws/aws-sdk-go-v2/config v1.27.20 h1:oQSn/KNUMV54X0FBEDQQ2ymNfcKyMT81ar8gyvMzzqs= +github.com/aws/aws-sdk-go-v2/config v1.27.20/go.mod h1:IbEMotJrWc3Bh7++HXZDlviHZP7kHrkHU3PNl9e17po= +github.com/aws/aws-sdk-go-v2/credentials v1.17.20 h1:VYTCplAeOeBv5InTtrmF61OIwD4aHKryg3KZ6hf7dsI= +github.com/aws/aws-sdk-go-v2/credentials v1.17.20/go.mod h1:ktubcFYvbN8++72jVM9IJoQH6Q2TP+Z7r2VbV1AaESU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.7 h1:54QUEXjkE1SlxHmRA3gBXA52j/ZSAgdOfAFGv1NsPCY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.7/go.mod h1:bQRjJsdSMzmo/qbtGeBtPbIMp1IgQ+9R9jYJLm12uJA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.11 h1:ltkhl3I9ddcRR3Dsy+7bOFFq546O8OYsfNEXVIyuOSE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.11/go.mod h1:H4D8JoCFNJwnT7U5U8iwgG24n71Fx2I/ZP/18eYFr9g= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.11 h1:+BgX2AY7yV4ggSwa80z/yZIJX+e0jnNxjMLVyfpSXM0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.11/go.mod h1:DlBATBSDCz30BCdRFldmyLsAzJwi2pdQ+YSdJTHhTUI= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.165.0 h1:FQpJS76mmmo21FZn9FAutjAIxotNkiGXUYfUQN/RfGA= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.165.0/go.mod h1:+dDvvbkwmJCZGzsSlsqEtJ6XhyG/hD2FHjIfpqcNl+o= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.13 h1:3A8vxp65nZy6aMlSCBvpIyxIbAN0DOSxaPDZuzasxuU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.13/go.mod h1:IxJ/pMQ/Y+MDFGo6pQRyqzKKwtGMHb5IWp5PXSQr8dM= +github.com/aws/aws-sdk-go-v2/service/sso v1.21.0 h1:P0zUA+5liaoNILI/btBBQHC09PFPyRJr+w+Xt9KHKck= +github.com/aws/aws-sdk-go-v2/service/sso v1.21.0/go.mod h1:0bmRzdsq9/iNyP02H4UV0ZRjFx6qQBqRvfCJ4trFgjE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.0 h1:jPV8U9r3msO9ECm9geW8PGjU/rz8vfPTPmIBbA83W3M= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.0/go.mod h1:B3G77bQDCmhp0RV0P/J9Kd4/qsymdWVhzTe3btAtywE= +github.com/aws/aws-sdk-go-v2/service/sts v1.29.0 h1:dqW4XRwPE/poWSqVntpeXLHzpPK6AOfKmL9QWDYl9aw= +github.com/aws/aws-sdk-go-v2/service/sts v1.29.0/go.mod h1:j8+hrxlmLR8ZQo6ytTAls/JFrt5bVisuS6PD8gw2VBw= +github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= +github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= -github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= +github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= -github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0= -github.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk= +github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= +github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM= github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM= +github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/ttrpc v1.2.2 h1:9vqZr0pxwOF5koz6N0N3kJ0zDHokrcPxIR/ZR2YFtOs= -github.com/containerd/ttrpc v1.2.2/go.mod h1:sIT6l32Ph/H9cvnJsfXM5drIVzTr5A2flTf1G5tYZak= +github.com/containerd/ttrpc v1.2.4 h1:eQCQK4h9dxDmpOb9QOOMh2NHTfzroH1IkmHiKZi05Oo= +github.com/containerd/ttrpc v1.2.4/go.mod h1:ojvb8SJBSch0XkqNO0L0YX/5NxR3UnVk2LzFKBK0upc= github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= -github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v25.0.1+incompatible h1:k5TYd5rIVQRSqcTwCID+cyVA0yRg86+Pcrz1ls0/frA= -github.com/docker/docker v25.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.0.0+incompatible h1:JRugTYuelmWlW0M3jakcIadDx2HUoUO6+Tf2C5jVfwA= +github.com/docker/docker v27.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/elastic/go-freelru v0.11.0 h1:8TU/uwnB+z//znXVPpT+p4VN9XooF5Z+qlwr/wePpGg= -github.com/elastic/go-freelru v0.11.0/go.mod h1:bSdWT4M0lW79K8QbX6XY2heQYSCqD7THoYf82pT/H3I= +github.com/elastic/go-freelru v0.13.0 h1:TKKY6yCfNNNky7Pj9xZAOEpBcdNgZJfihEftOb55omg= +github.com/elastic/go-freelru v0.13.0/go.mod h1:bSdWT4M0lW79K8QbX6XY2heQYSCqD7THoYf82pT/H3I= github.com/elastic/go-perf v0.0.0-20191212140718-9c656876f595 h1:q8n4QjcLa4q39Q3fqHRknTBXBtegjriHFrB42YKgXGI= github.com/elastic/go-perf v0.0.0-20191212140718-9c656876f595/go.mod h1:s09U1b4P1ZxnKx2OsqY7KlHdCesqZWIhyq0Gs/QC/Us= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= @@ -69,11 +95,9 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= -github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= @@ -82,6 +106,8 @@ github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2Kv github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -93,7 +119,6 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4er github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -102,9 +127,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -113,7 +137,6 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -121,13 +144,13 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20230426061923-93006964c1fc h1:AGDHt781oIcL4EFk7cPnvBUYTwU8BEU6GDTO3ZMn1sE= -github.com/google/pprof v0.0.0-20230426061923-93006964c1fc/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -136,16 +159,16 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 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 v1.4.1 h1:JfD4jthWBqZMEffc5RjgmlzpYttAVw1sdnmiNaPO3hE= -github.com/jsimonetti/rtnetlink v1.4.1/go.mod h1:xJjT7t59UIZ62GLZbv6PLLo8VFrostJMPBAheR6OM8w= +github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5GTEn90= +github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.5 h1:d4vBd+7CHydUqpFBgUEKkSdtSugf9YFmSkvUYPquI5E= -github.com/klauspost/compress v1.17.5/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= -github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -161,6 +184,8 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= @@ -182,14 +207,14 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= -github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= -github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= -github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= +github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= +github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8= -github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= @@ -201,12 +226,10 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -214,19 +237,21 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= @@ -235,41 +260,33 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= -go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= -go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= -go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= -go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= -go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= -go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= -go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= -golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= -golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -279,49 +296,34 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL 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.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= -golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -332,35 +334,30 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= -golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= -google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= -google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= -google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= +google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 h1:vlzZttNJGVqTsRFU9AmdnrcO1Znh8Ew9kCD//yjigk0= +google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:CCviP9RmpZ1mxVr8MUjCnSiY09IbAXZxhLE6EhHIdPU= +google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 h1:W5Xj/70xIA4x60O/IFyXivR5MGqblAb8R3w26pnD6No= +google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 h1:mxSlqyb8ZAHsYDCfiXN1EDdNTdvjUJSLY+OnAUtYNYA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= -google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -371,10 +368,9 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -390,16 +386,16 @@ gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.29.1 h1:DAjwWX/9YT7NQD4INu49ROJuZAAAP/Ijki48GUPzxqw= -k8s.io/api v0.29.1/go.mod h1:7Kl10vBRUXhnQQI8YR/R327zXC8eJ7887/+Ybta+RoQ= -k8s.io/apimachinery v0.29.1 h1:KY4/E6km/wLBguvCZv8cKTeOwwOBqFNjwJIdMkMbbRc= -k8s.io/apimachinery v0.29.1/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= -k8s.io/client-go v0.29.1 h1:19B/+2NGEwnFLzt0uB5kNJnfTsbV8w6TgQRz9l7ti7A= -k8s.io/client-go v0.29.1/go.mod h1:TDG/psL9hdet0TI9mGyHJSgRkW3H9JZk2dNEUS7bRks= -k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= -k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= -k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= -k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= +k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= +k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= +k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= +k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/host/host.go b/host/host.go index 772d0417..ce3f3f34 100644 --- a/host/host.go +++ b/host/host.go @@ -12,7 +12,7 @@ import ( "fmt" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/pfelf" + "github.com/elastic/otel-profiling-agent/util" ) // TraceHash is used for unique identifiers for traces, and is required to be 64-bits @@ -37,30 +37,24 @@ func (fid FileID) StringNoQuotes() string { return fmt.Sprintf("%016x%016x", uint64(fid), uint64(fid)) } -// CalculateKernelFileID calculates an ID for a kernel image or module given its libpf.FileID. -func CalculateKernelFileID(id libpf.FileID) FileID { +// FileIDFromLibpf truncates a libpf.FileID to be a host.FileID. +func FileIDFromLibpf(id libpf.FileID) FileID { return FileID(id.Hi()) } -// CalculateID calculates a 64-bit executable ID of the contents of a file. -func CalculateID(fileName string) (FileID, error) { - hash, err := pfelf.FileHash(fileName) - if err != nil { - return FileID(0), err - } - return FileIDFromBytes(hash[0:8]) -} - type Frame struct { - File FileID - Lineno libpf.AddressOrLineno - Type libpf.FrameType + File FileID + Lineno libpf.AddressOrLineno + Type libpf.FrameType + ReturnAddress bool } type Trace struct { - Comm string - Frames []Frame - Hash TraceHash - KTime libpf.KTime - PID libpf.PID + Comm string + Frames []Frame + Hash TraceHash + KTime util.KTime + PID util.PID + APMTraceID libpf.APMTraceID + APMTransactionID libpf.APMTransactionID } diff --git a/hostmetadata/agent/agent.go b/hostmetadata/agent/agent.go index 0aeb6a34..b49f7937 100644 --- a/hostmetadata/agent/agent.go +++ b/hostmetadata/agent/agent.go @@ -7,13 +7,13 @@ package agent import ( - "fmt" "os" + "strconv" "github.com/elastic/otel-profiling-agent/config" - "github.com/elastic/otel-profiling-agent/libpf/vc" ) +// TODO: Change to semconv / ECS // Agent metadata keys const ( // Build metadata @@ -34,7 +34,6 @@ const ( keyAgentConfigTags = "agent:config_tags" keyAgentConfigDisableTLS = "agent:config_disable_tls" keyAgentConfigNoKernelVersionCheck = "agent:config_no_kernel_version_check" - keyAgentConfigUploadSymbols = "agent:config_upload_symbols" keyAgentConfigTracers = "agent:config_tracers" keyAgentConfigKnownTracesEntries = "agent:config_known_traces_entries" keyAgentConfigMapScaleFactor = "agent:config_map_scale_factor" @@ -48,34 +47,34 @@ const ( // AddMetadata adds agent metadata to the result map. func AddMetadata(result map[string]string) { - result[keyAgentVersion] = vc.Version() - result[keyAgentRevision] = vc.Revision() + result[keyAgentVersion] = config.Version() + result[keyAgentRevision] = config.Revision() + result[keyAgentBuildTimestamp] = config.BuildTimestamp() - result[keyAgentBuildTimestamp] = vc.BuildTimestamp() - result[keyAgentStartTimeMilli] = fmt.Sprintf("%d", config.StartTime().UnixMilli()) + result[keyAgentStartTimeMilli] = strconv.FormatInt(config.StartTime().UnixMilli(), 10) bpfLogLevel, bpfLogSize := config.BpfVerifierLogSetting() - result[keyAgentConfigBpfLoglevel] = fmt.Sprintf("%d", bpfLogLevel) - result[keyAgentConfigBpfLogSize] = fmt.Sprintf("%d", bpfLogSize) + result[keyAgentConfigBpfLoglevel] = strconv.FormatUint(uint64(bpfLogLevel), 10) + result[keyAgentConfigBpfLogSize] = strconv.Itoa(bpfLogSize) result[keyAgentConfigCacheDirectory] = config.CacheDirectory() result[keyAgentConfigCollectionAgentAddr] = config.CollectionAgentAddr() result[keyAgentConfigurationFile] = config.ConfigurationFile() - result[keyAgentConfigDisableTLS] = fmt.Sprintf("%v", config.DisableTLS()) - result[keyAgentConfigNoKernelVersionCheck] = fmt.Sprintf("%v", config.NoKernelVersionCheck()) - result[keyAgentConfigUploadSymbols] = fmt.Sprintf("%v", config.UploadSymbols()) + result[keyAgentConfigDisableTLS] = strconv.FormatBool(config.DisableTLS()) + result[keyAgentConfigNoKernelVersionCheck] = strconv.FormatBool(config.NoKernelVersionCheck()) result[keyAgentConfigTags] = config.Tags() result[keyAgentConfigTracers] = config.Tracers() - result[keyAgentConfigKnownTracesEntries] = fmt.Sprintf("%d", config.TraceCacheEntries()) - result[keyAgentConfigMapScaleFactor] = fmt.Sprintf("%d", config.MapScaleFactor()) + result[keyAgentConfigKnownTracesEntries] = + strconv.FormatUint(uint64(config.TraceCacheEntries()), 10) + result[keyAgentConfigMapScaleFactor] = strconv.FormatUint(uint64(config.MapScaleFactor()), 10) result[keyAgentConfigMaxElementsPerInterval] = - fmt.Sprintf("%d", config.MaxElementsPerInterval()) - result[keyAgentConfigVerbose] = fmt.Sprintf("%v", config.Verbose()) + strconv.FormatUint(uint64(config.MaxElementsPerInterval()), 10) + result[keyAgentConfigVerbose] = strconv.FormatBool(config.Verbose()) result[keyAgentConfigProbabilisticInterval] = config.GetTimes().ProbabilisticInterval().String() result[keyAgentConfigProbabilisticThreshold] = - fmt.Sprintf("%d", config.ProbabilisticThreshold()) + strconv.FormatUint(uint64(config.ProbabilisticThreshold()), 10) result[keyAgentConfigPresentCPUCores] = - fmt.Sprintf("%d", config.PresentCPUCores()) + strconv.FormatUint(uint64(config.PresentCPUCores()), 10) result[keyAgentEnvHTTPSProxy] = os.Getenv("HTTPS_PROXY") } diff --git a/hostmetadata/azure/azure.go b/hostmetadata/azure/azure.go index cfe79003..dcea9407 100644 --- a/hostmetadata/azure/azure.go +++ b/hostmetadata/azure/azure.go @@ -15,8 +15,9 @@ import ( "strings" "time" - "github.com/elastic/otel-profiling-agent/hostmetadata/instance" log "github.com/sirupsen/logrus" + + "github.com/elastic/otel-profiling-agent/hostmetadata/instance" ) const azurePrefix = "azure:" @@ -132,6 +133,8 @@ func AddMetadata(result map[string]string) { // populateResult converts the given answer from Azure in imds into // our internal representation in result. func populateResult(result map[string]string, imds *IMDS) { + result[instance.KeyCloudProvider] = "azure" + v := reflect.ValueOf(imds.Compute) t := reflect.TypeOf(imds.Compute) for i := 0; i < v.NumField(); i++ { @@ -144,6 +147,9 @@ func populateResult(result map[string]string, imds *IMDS) { result[azurePrefix+"compute/"+strings.ToLower(fieldName)] = fieldValue } + addCloudRegion(result) + addHostType(result) + // Used to temporarily hold synthetic metadata ipAddrs := map[string][]string{ instance.KeyPrivateIPV4s: make([]string, 0), @@ -206,3 +212,17 @@ func populateResult(result map[string]string, imds *IMDS) { instance.AddToResult(ipAddrs, result) } + +func addCloudRegion(result map[string]string) { + // example: "azure.compute.location": "eastus2" + if region, ok := result[azurePrefix+"compute/location"]; ok { + result[instance.KeyCloudRegion] = region + } +} + +func addHostType(result map[string]string) { + // example: "azure.compute.vmsize": "Standard_D2s_v3" + if hostType, ok := result[azurePrefix+"compute/vmsize"]; ok { + result[instance.KeyHostType] = hostType + } +} diff --git a/hostmetadata/azure/azure_test.go b/hostmetadata/azure/azure_test.go index bea29fe5..0766f88b 100644 --- a/hostmetadata/azure/azure_test.go +++ b/hostmetadata/azure/azure_test.go @@ -11,7 +11,8 @@ import ( "strings" "testing" - "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // nolint:lll @@ -126,6 +127,9 @@ const fakeAzureAnswer = `{ }` var expectedResult = map[string]string{ + "cloud:provider": "azure", + "cloud:region": "westeurope", + "host:type": "Standard_DS1_v2", "azure:compute/environment": "AzurePublicCloud", "azure:compute/location": "westeurope", "azure:compute/name": "bar-test", @@ -156,13 +160,9 @@ func TestPopulateResult(t *testing.T) { azure := strings.NewReader(fakeAzureAnswer) - if err := json.NewDecoder(azure).Decode(&imds); err != nil { - t.Fatalf("Failed to parse Azure metadata: %v", err) - } + err := json.NewDecoder(azure).Decode(&imds) + require.NoError(t, err) populateResult(result, &imds) - - if diff := cmp.Diff(expectedResult, result); diff != "" { - t.Fatalf("Metadata mismatch (-want +got):\n%s", diff) - } + assert.Equal(t, expectedResult, result) } diff --git a/hostmetadata/collector.go b/hostmetadata/collector.go index ff9cab85..e48e048f 100644 --- a/hostmetadata/collector.go +++ b/hostmetadata/collector.go @@ -28,21 +28,30 @@ type Collector struct { caEndpoint string // collectionInterval specifies the duration between host metadata collections. collectionInterval time.Duration + + // customData is a map of custom key/value pairs that can be added to the host metadata. + customData map[string]string } // NewCollector returns a new Collector for the specified collection agent endpoint. func NewCollector(caEndpoint string) *Collector { return &Collector{ caEndpoint: caEndpoint, + customData: make(map[string]string), - // Changing this significantly must be done in coordination with pf-web-service, as - // it bounds the minimum time for which host metadata must be retrieved. - // 23021 is 6h23m41s - picked randomly so we don't do the collection at the same + // Changing this significantly must be done in coordination with readers of the host + // metadata, as it bounds the minimum time for which host metadata must be retrieved. + // 23021 is 6h23m41s - picked randomly, so we don't do the collection at the same // time every day. collectionInterval: 23021 * time.Second, } } +// AddCustomData adds a custom key/value pair to the host metadata. +func (c *Collector) AddCustomData(key, value string) { + c.customData[key] = value +} + // GetHostMetadata returns a map that contains all host metadata key/value pairs. func (c *Collector) GetHostMetadata() map[string]string { result := make(map[string]string) @@ -65,6 +74,10 @@ func (c *Collector) GetHostMetadata() map[string]string { default: } + for k, v := range c.customData { + result[k] = v + } + return result } diff --git a/hostmetadata/ec2/ec2.go b/hostmetadata/ec2/ec2.go index 7777f7b3..4bdae019 100644 --- a/hostmetadata/ec2/ec2.go +++ b/hostmetadata/ec2/ec2.go @@ -7,70 +7,98 @@ package ec2 import ( + "context" "fmt" + "io" "path" "strings" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/aws/ec2metadata" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/ec2" - "github.com/elastic/otel-profiling-agent/hostmetadata/instance" log "github.com/sirupsen/logrus" + + awsconfig "github.com/aws/aws-sdk-go-v2/config" + ec2imds "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" + + ec2service "github.com/aws/aws-sdk-go-v2/service/ec2" + ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" + + "github.com/elastic/otel-profiling-agent/hostmetadata/instance" ) // ec2MetadataIface is an interface for the EC2 metadata service. // Its purpose is to allow faking the implementation in unit tests. type ec2MetadataIface interface { - GetMetadata(string) (string, error) - GetInstanceIdentityDocument() (ec2metadata.EC2InstanceIdentityDocument, error) + FetchMetadata(string) (string, error) + FetchInstanceIdentityDocument() (ec2imds.InstanceIdentityDocument, error) } type ec2Iface interface { - DescribeTags(*ec2.DescribeTagsInput) (*ec2.DescribeTagsOutput, error) + DescribeTags(context.Context, *ec2service.DescribeTagsInput, + ...func(*ec2service.Options)) (*ec2service.DescribeTagsOutput, error) } // ec2MetadataClient can be nil if it cannot be built. -var ec2MetadataClient = buildMetadataClient() +var ec2MetadataClient, _ = buildMetadataClient() // ec2Client is lazily initialized inside addTags() var ec2Client ec2Iface const ec2Prefix = "ec2:" -func buildMetadataClient() ec2MetadataIface { - se := session.Must(session.NewSession()) - // Retries make things slow needlessly. Since the metadata service runs on the same network - // link, no need to worry about an unreliable network. - // We collect metadata often enough for errors to be tolerable. - return ec2metadata.New(se, aws.NewConfig().WithMaxRetries(0)) +type ec2MetadataWrapper struct { + *ec2imds.Client } -func buildClient(region string) ec2Iface { - se := session.Must(session.NewSession()) - return ec2.New(se, aws.NewConfig().WithMaxRetries(0).WithRegion(region)) +func (c *ec2MetadataWrapper) FetchMetadata(input string) (string, error) { + metadataOutput, err := c.GetMetadata(context.Background(), + &ec2imds.GetMetadataInput{ + Path: input, + }) + if err != nil { + return "", err + } + valueBytes, err := io.ReadAll(metadataOutput.Content) + if err != nil { + return "", err + } + return string(valueBytes), nil } -// awsErrorMessage rewrites a 404 AWS error message to reduce verbosity. -// If the error is not a 404, the full error string is returned. -func awsErrorMessage(err error) string { - if awsErr, ok := err.(awserr.RequestFailure); ok { - if awsErr.StatusCode() == 404 { - return "not found" - } +func (c *ec2MetadataWrapper) FetchInstanceIdentityDocument() ( + ec2imds.InstanceIdentityDocument, error) { + doc, err := c.GetInstanceIdentityDocument(context.Background(), nil) + if err != nil { + return ec2imds.InstanceIdentityDocument{}, err + } + return doc.InstanceIdentityDocument, nil +} + +func buildMetadataClient() (ec2MetadataIface, error) { + cfg, err := awsconfig.LoadDefaultConfig(context.Background()) + if err != nil { + log.Errorf("Failed to create default config for AWS: %v", err) + return nil, err } - return err.Error() + + return &ec2MetadataWrapper{ec2imds.NewFromConfig(cfg)}, nil +} + +func buildClient() (ec2Iface, error) { + cfg, err := awsconfig.LoadDefaultConfig(context.Background()) + if err != nil { + return nil, err + } + + return ec2service.NewFromConfig(cfg), nil } func getMetadataForKeys(prefix string, suffix []string, result map[string]string) { for i := range suffix { keyPath := path.Join(prefix, suffix[i]) - value, err := ec2MetadataClient.GetMetadata(keyPath) + value, err := ec2MetadataClient.FetchMetadata(keyPath) // This is normal, as some keys do not exist if err != nil { - log.Debugf("Unable to get metadata key: %s: %s", keyPath, awsErrorMessage(err)) + log.Debugf("Unable to get metadata key: %s: %v", keyPath, err) continue } result[ec2Prefix+keyPath] = value @@ -79,35 +107,40 @@ func getMetadataForKeys(prefix string, suffix []string, result map[string]string // list returns the list of keys at the given instance metadata service path. func list(urlPath string) ([]string, error) { - value, err := ec2MetadataClient.GetMetadata(urlPath) + value, err := ec2MetadataClient.FetchMetadata(urlPath) if err != nil { - return nil, fmt.Errorf("unable to list %v: %s", urlPath, awsErrorMessage(err)) + return nil, fmt.Errorf("unable to list %v: %s", urlPath, err) } return instance.Enumerate(value), nil } +func stringPtr(v string) *string { + return &v +} + // addTags retrieves and adds EC2 instance tags into the provided map. // Tags are added separately, prefixed with 'ec2:tags/{key}' for each tag key. // In order for this operation to succeed, the instance needs to have an // IAM role assigned, with a policy that grants ec2:DescribeTags. -func addTags(region, instanceID string, result map[string]string) { +func addTags(instanceID string, result map[string]string) { if ec2Client == nil { - ec2Client = buildClient(region) - if ec2Client == nil { + var err error + ec2Client, err = buildClient() + if err != nil { log.Warnf("EC2 client couldn't be created, skipping tag collection") return } } - descTagsOut, err := ec2Client.DescribeTags( - &ec2.DescribeTagsInput{ - Filters: []*ec2.Filter{ + descTagsOut, err := ec2Client.DescribeTags(context.Background(), + &ec2service.DescribeTagsInput{ + Filters: []ec2types.Filter{ { - Name: aws.String("resource-id"), - Values: []*string{ - aws.String(instanceID), + Name: stringPtr("resource-id"), + Values: []string{ + instanceID, }, }, }, @@ -136,17 +169,17 @@ func AddMetadata(result map[string]string) { return } - var region string var instanceID string - if idDoc, err := ec2MetadataClient.GetInstanceIdentityDocument(); err == nil { - region = idDoc.Region + if idDoc, err := ec2MetadataClient.FetchInstanceIdentityDocument(); err == nil { instanceID = idDoc.InstanceID } else { log.Warnf("EC2 metadata could not be collected: %v", err) return } + result[instance.KeyCloudProvider] = "aws" + getMetadataForKeys("", []string{ "ami-id", "ami-manifest-path", @@ -172,6 +205,9 @@ func AddMetadata(result map[string]string) { "region", }, result) + addCloudRegion(result) + addHostType(result) + macs, err := list("network/interfaces/macs/") if err != nil { log.Warnf("Unable to list network interfaces: %v", err) @@ -213,7 +249,7 @@ func AddMetadata(result map[string]string) { strings.ReplaceAll(ips, "\n", ",")) } - assocsPath := fmt.Sprintf("%sipv4-associations/", macPath) + assocsPath := macPath + "ipv4-associations/" assocs, err := list(assocsPath) if err != nil { // Nothing to worry about: there might not be any associations @@ -225,5 +261,17 @@ func AddMetadata(result map[string]string) { } instance.AddToResult(ipAddrs, result) - addTags(region, instanceID, result) + addTags(instanceID, result) +} + +func addCloudRegion(result map[string]string) { + if region, ok := result[ec2Prefix+"placement/region"]; ok { + result[instance.KeyCloudRegion] = region + } +} + +func addHostType(result map[string]string) { + if instanceType, ok := result[ec2Prefix+"instance-type"]; ok { + result[instance.KeyHostType] = instanceType + } } diff --git a/hostmetadata/ec2/ec2_test.go b/hostmetadata/ec2/ec2_test.go index d054b5ba..677ab92f 100644 --- a/hostmetadata/ec2/ec2_test.go +++ b/hostmetadata/ec2/ec2_test.go @@ -7,13 +7,15 @@ package ec2 import ( + "context" "fmt" "testing" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/ec2metadata" - "github.com/aws/aws-sdk-go/service/ec2" - "github.com/google/go-cmp/cmp" + ec2imds "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" + ec2service "github.com/aws/aws-sdk-go-v2/service/ec2" + ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" + + "github.com/stretchr/testify/assert" ) type fakeEC2Metadata struct { @@ -21,10 +23,10 @@ type fakeEC2Metadata struct { } type fakeEC2Tags struct { - tags []*ec2.TagDescription + tags []ec2types.TagDescription } -func (e *fakeEC2Metadata) GetMetadata(path string) (string, error) { +func (e *fakeEC2Metadata) FetchMetadata(path string) (string, error) { value, found := e.metadata[path] if !found { return "", fmt.Errorf("%s not found", path) @@ -33,14 +35,14 @@ func (e *fakeEC2Metadata) GetMetadata(path string) (string, error) { return value, nil } -func (e *fakeEC2Metadata) GetInstanceIdentityDocument() (ec2metadata.EC2InstanceIdentityDocument, - error) { - return ec2metadata.EC2InstanceIdentityDocument{}, nil +func (e *fakeEC2Metadata) FetchInstanceIdentityDocument() ( + ec2imds.InstanceIdentityDocument, error) { + return ec2imds.InstanceIdentityDocument{}, nil } -func (e *fakeEC2Tags) DescribeTags(_ *ec2.DescribeTagsInput, -) (*ec2.DescribeTagsOutput, error) { - return &ec2.DescribeTagsOutput{Tags: e.tags}, nil +func (e *fakeEC2Tags) DescribeTags(context.Context, *ec2service.DescribeTagsInput, + ...func(*ec2service.Options)) (*ec2service.DescribeTagsOutput, error) { + return &ec2service.DescribeTagsOutput{Tags: e.tags}, nil } func TestAddMetadata(t *testing.T) { @@ -79,14 +81,14 @@ func TestAddMetadata(t *testing.T) { } ec2Client = &fakeEC2Tags{ - tags: []*ec2.TagDescription{ + tags: []ec2types.TagDescription{ { - Key: aws.String("foo"), - Value: aws.String("bar"), + Key: stringPtr("foo"), + Value: stringPtr("bar"), }, { - Key: aws.String("baz"), - Value: aws.String("value1-value2"), + Key: stringPtr("baz"), + Value: stringPtr("value1-value2"), }, }, } @@ -95,6 +97,9 @@ func TestAddMetadata(t *testing.T) { AddMetadata(result) expected := map[string]string{ + "cloud:provider": "aws", + "cloud:region": "us-east-2", + "host:type": "m5.large", "ec2:ami-id": "ami-1234", "ec2:ami-manifest-path": "(unknown)", "ec2:ancestor-ami-ids": "ami-2345", @@ -129,7 +134,5 @@ func TestAddMetadata(t *testing.T) { "instance:public-ipv4s": "9.9.9.9,8.8.8.8,4.3.2.1", } - if diff := cmp.Diff(expected, result); diff != "" { - t.Fatalf("Metadata mismatch (-want +got):\n%s", diff) - } + assert.Equal(t, expected, result) } diff --git a/hostmetadata/gce/client.go b/hostmetadata/gce/client.go index fa4ac3f8..87718b31 100644 --- a/hostmetadata/gce/client.go +++ b/hostmetadata/gce/client.go @@ -6,7 +6,11 @@ package gce -import gcemetadata "cloud.google.com/go/compute/metadata" +import ( + "context" + + gcemetadata "cloud.google.com/go/compute/metadata" +) // gceMetadataClient is a type that implements the gceMetadataIface. // Its purpose is to allow unit-testing of the metadata collection logic. @@ -22,7 +26,7 @@ type gceMetadataIface interface { // Get forwards to gcemetadata.Get func (*gceMetadataClient) Get(p string) (string, error) { - return gcemetadata.Get(p) + return gcemetadata.GetWithContext(context.Background(), p) } // InstanceTags forwards to gcemetadata.InstanceTags diff --git a/hostmetadata/gce/gce.go b/hostmetadata/gce/gce.go index df7692fa..8626ce3a 100644 --- a/hostmetadata/gce/gce.go +++ b/hostmetadata/gce/gce.go @@ -9,10 +9,12 @@ package gce import ( "fmt" "path" + "regexp" "strings" - "github.com/elastic/otel-profiling-agent/hostmetadata/instance" log "github.com/sirupsen/logrus" + + "github.com/elastic/otel-profiling-agent/hostmetadata/instance" ) const gcePrefix = "gce:" @@ -52,6 +54,8 @@ func AddMetadata(result map[string]string) { return } + result[instance.KeyCloudProvider] = "gcp" + // Get metadata under instance/ getMetadataForKeys("instance/", []string{ "id", @@ -64,6 +68,9 @@ func AddMetadata(result map[string]string) { "zone", }, result) + addCloudRegion(result) + addHostType(result) + // Get the tags tags, err := gceClient.InstanceTags() if err != nil { @@ -101,7 +108,7 @@ func AddMetadata(result map[string]string) { ipAddrs[instance.KeyPrivateIPV4s] = append(ipAddrs[instance.KeyPrivateIPV4s], ip) } - accessConfigs, err := list(fmt.Sprintf("%saccess-configs/", interfacePath)) + accessConfigs, err := list(interfacePath + "access-configs/") if err != nil { // There might not be any access configurations log.Debugf("Unable to list access configurations: %v", err) @@ -110,8 +117,7 @@ func AddMetadata(result map[string]string) { // Get metadata under instance/network-interfaces/*/access-configs/*/ // (this is where we can get public IP, if there is one) for _, accessConfig := range accessConfigs { - accessConfigPath := path.Join(interfacePath, - fmt.Sprintf("access-configs/%s", accessConfig)) + accessConfigPath := path.Join(interfacePath, "access-configs", accessConfig) getMetadataForKeys(accessConfigPath, []string{"external-ip"}, result) if ip, ok := result[gcePrefix+accessConfigPath+"/external-ip"]; ok { @@ -122,3 +128,21 @@ func AddMetadata(result map[string]string) { instance.AddToResult(ipAddrs, result) } + +var regionMatcher = regexp.MustCompile(`^projects/[^/]+/zones/(([^-]+)$|([^-]+)-([^-]+))`) + +func addCloudRegion(result map[string]string) { + matches := regionMatcher.FindStringSubmatch(result[gcePrefix+"instance/zone"]) + if len(matches) >= 2 { + result[instance.KeyCloudRegion] = matches[1] + } +} + +var hostTypeMatcher = regexp.MustCompile(`^projects/[^/]+/machineTypes/(.+)$`) + +func addHostType(result map[string]string) { + matches := hostTypeMatcher.FindStringSubmatch(result[gcePrefix+"instance/machine-type"]) + if len(matches) >= 2 { + result[instance.KeyHostType] = matches[1] + } +} diff --git a/hostmetadata/gce/gce_test.go b/hostmetadata/gce/gce_test.go index 9f11c780..42b9b0ca 100644 --- a/hostmetadata/gce/gce_test.go +++ b/hostmetadata/gce/gce_test.go @@ -4,13 +4,16 @@ * See the file "LICENSE" for details. */ +//nolint:lll package gce import ( "fmt" "testing" - "github.com/google/go-cmp/cmp" + "github.com/elastic/otel-profiling-agent/hostmetadata/instance" + + "github.com/stretchr/testify/assert" ) type fakeGCEMetadata struct { @@ -40,11 +43,11 @@ func TestAddMetadata(t *testing.T) { metadata: map[string]string{ "instance/id": "1234", "instance/cpu-platform": "Intel Cascade Lake", - "instance/machine-type": "test-n2-custom-4-10240", + "instance/machine-type": "projects/123456/machineTypes/test-n2-custom-4-10240", "instance/name": "gke-mirror-cluster-api", "instance/description": "test description", "instance/hostname": "barbaz", - "instance/zone": "zones/us-east1-c", + "instance/zone": "projects/123456/zones/us-east1-c", "instance/network-interfaces/": "0\n1\n2", "instance/network-interfaces/0/ip": "1.1.1.1", "instance/network-interfaces/0/network": "networks/default", @@ -62,28 +65,132 @@ func TestAddMetadata(t *testing.T) { AddMetadata(result) expectedResult := map[string]string{ - "gce:instance/id": "1234", - "gce:instance/cpu-platform": "Intel Cascade Lake", - "gce:instance/machine-type": "test-n2-custom-4-10240", - "gce:instance/name": "gke-mirror-cluster-api", - "gce:instance/description": "test description", - "gce:instance/network-interfaces/0/ip": "1.1.1.1", - "gce:instance/network-interfaces/0/network": "networks/default", - "gce:instance/network-interfaces/0/subnetmask": "255.255.240.0", - "gce:instance/network-interfaces/1/gateway": "22.22.22.22", + "cloud:provider": "gcp", + "cloud:region": "us-east1", + "host:type": "test-n2-custom-4-10240", + "gce:instance/id": "1234", + "gce:instance/cpu-platform": "Intel Cascade Lake", + "gce:instance/machine-type": "projects/123456/machineTypes/test-n2-custom-4-10240", + "gce:instance/name": "gke-mirror-cluster-api", + "gce:instance/description": "test description", + "gce:instance/network-interfaces/0/ip": "1.1.1.1", + "gce:instance/network-interfaces/0/network": "networks/default", + "gce:instance/network-interfaces/0/subnetmask": "255.255.240.0", + "gce:instance/network-interfaces/1/gateway": "22.22.22.22", "gce:instance/network-interfaces/2/access-configs/0/external-ip": "7.7.7.7", "gce:instance/network-interfaces/2/access-configs/1/external-ip": "8.8.8.8", "gce:instance/network-interfaces/2/access-configs/2/external-ip": "9.9.9.9", "gce:instance/network-interfaces/2/mac": "3:3:3", "gce:instance/hostname": "barbaz", - "gce:instance/zone": "zones/us-east1-c", + "gce:instance/zone": "projects/123456/zones/us-east1-c", "gce:instance/image": "gke-node-images/global", "gce:instance/tags": "foo;bar;baz", "instance:private-ipv4s": "1.1.1.1", "instance:public-ipv4s": "7.7.7.7,8.8.8.8,9.9.9.9", } - if diff := cmp.Diff(expectedResult, result); diff != "" { - t.Fatalf("Metadata mismatch (-want +got):\n%s", diff) + assert.Equal(t, expectedResult, result) +} + +func TestAddCloudRegion(t *testing.T) { + tests := []struct { + name string + value string + expected string + }{ + { + name: "empty", + value: "", + expected: "", + }, + { + name: "slash only", + value: "/", + expected: "", + }, + { + name: "no region", + value: "projects/123456789/zones/", + expected: "", + }, + { + name: "no dash", + value: "projects/123456789/zones/europewest1", + expected: "europewest1", + }, + { + name: "one dash", + value: "projects/123456789/zones/europe-west1", + expected: "europe-west1", + }, + { + name: "two dashes", + value: "projects/123456789/zones/europe-west1-b", + expected: "europe-west1", + }, + { + name: "three dashes", + value: "projects/123456789/zones/europe-west1-b-c", + expected: "europe-west1", + }, + } + + for _, test := range tests { + result := make(map[string]string) + + result[gcePrefix+"instance/zone"] = test.value + addCloudRegion(result) + + expectedResult := map[string]string{ + gcePrefix + "instance/zone": test.value, + } + if test.expected != "" { + expectedResult[instance.KeyCloudRegion] = test.expected + } + assert.Equal(t, expectedResult, result) + } +} + +func TestAddHostType(t *testing.T) { + tests := []struct { + name string + value string + expected string + }{ + { + name: "empty", + value: "", + expected: "", + }, + { + name: "slash only", + value: "/", + expected: "", + }, + { + name: "no region", + value: "projects/123456/machineTypes/", + expected: "", + }, + { + name: "no dash", + value: "projects/123456/machineTypes/n1-standard-1", + expected: "n1-standard-1", + }, + } + + for _, test := range tests { + result := make(map[string]string) + + result[gcePrefix+"instance/machine-type"] = test.value + addHostType(result) + + expectedResult := map[string]string{ + gcePrefix + "instance/machine-type": test.value, + } + if test.expected != "" { + expectedResult[instance.KeyHostType] = test.expected + } + assert.Equal(t, expectedResult, result) } } diff --git a/hostmetadata/host/cpuid_test.go b/hostmetadata/host/cpuid_test.go index bbc247ff..8e716975 100644 --- a/hostmetadata/host/cpuid_test.go +++ b/hostmetadata/host/cpuid_test.go @@ -13,18 +13,16 @@ import ( "github.com/klauspost/cpuid/v2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCPUID_DetectAllCPUIDs(t *testing.T) { coreIDs, err := ParseCPUCoreIDs(CPUOnlinePath) - if err != nil { - t.Fatalf("expected to read available CPUs, got error: %v", err) - } + require.NoError(t, err) detected, err := runCPUIDOnAllCores(coreIDs) - - assert.Nil(t, err) - assert.Equal(t, runtime.NumCPU(), len(detected)) + require.NoError(t, err) + assert.Len(t, detected, runtime.NumCPU()) assert.Equal(t, cpuid.CPU.PhysicalCores, detected[0].PhysicalCores) assert.Equal(t, cpuid.CPU.LogicalCores, detected[0].LogicalCores) assert.NotEmpty(t, detected[len(coreIDs)-1].Cache.L2) @@ -37,15 +35,13 @@ func TestCPUID_ParseOnlineCPUCoreIDs(t *testing.T) { defer os.Remove(f.Name()) coreIDs, err := ParseCPUCoreIDs(f.Name()) - assert.Nil(t, err) - assert.Equal(t, 9, len(coreIDs)) + require.NoError(t, err) + assert.Len(t, coreIDs, 9) } func prepareFakeCPUOnlineFile(t *testing.T, content string) *os.File { f, err := os.CreateTemp("", "sys_device_cpu_online") - if err != nil { - t.Fatalf("could not create temporary file: %v", err) - } + require.NoError(t, err) _ = os.WriteFile(f.Name(), []byte(content), os.ModePerm) return f } diff --git a/hostmetadata/host/cpuinfo_test.go b/hostmetadata/host/cpuinfo_test.go index 886c7bef..111b3cd2 100644 --- a/hostmetadata/host/cpuinfo_test.go +++ b/hostmetadata/host/cpuinfo_test.go @@ -13,18 +13,15 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestReadCPUInfo(t *testing.T) { info, err := readCPUInfo() - assert.Nil(t, err) + require.NoError(t, err) assertions := map[string]func(t *testing.T){ "NotEmptyOnAnyCPU": func(t *testing.T) { assert.NotEmpty(t, info) }, - "BugsAreListed": func(t *testing.T) { - assert.Contains(t, info[key(keyCPUBugs)], 0) - assert.NotEmpty(t, info[key(keyCPUBugs)][0]) - }, "FlagsAreSorted": func(t *testing.T) { assert.Contains(t, info[key(keyCPUFlags)], 0) assert.True(t, @@ -47,10 +44,10 @@ func TestReadCPUInfo(t *testing.T) { "CachesIsANumber": func(t *testing.T) { assert.Contains(t, info[key(keyCPUCacheL1i)], 0) _, err := strconv.Atoi(info[key(keyCPUCacheL1i)][0]) - assert.Nil(t, err) + require.NoError(t, err) assert.Contains(t, info[key(keyCPUCacheL3)], 0) _, err = strconv.Atoi(info[key(keyCPUCacheL3)][0]) - assert.Nil(t, err) + require.NoError(t, err) }, "NumCPUs": func(t *testing.T) { assert.Contains(t, info[key(keyCPUNumCPUs)], 0) @@ -61,17 +58,15 @@ func TestReadCPUInfo(t *testing.T) { cps := info[key(keyCPUCoresPerSocket)][0] assert.NotEmpty(t, cps) i, err := strconv.Atoi(cps) - if err != nil { - t.Fatalf("%v must be a string representing a number", cps) - } - assert.True(t, i > 0) + require.NoErrorf(t, err, "%v must be parseable as a number", cps) + assert.Greater(t, i, 0) }, "OnlineCPUs": func(t *testing.T) { assert.Contains(t, info[key(keyCPUOnline)], 0) onlines := info[key(keyCPUOnline)][0] assert.NotEmpty(t, onlines) ints, err := readCPURange(onlines) - assert.Nil(t, err) + require.NoError(t, err) assert.NotEmpty(t, t, ints) }, } diff --git a/hostmetadata/host/host.go b/hostmetadata/host/host.go index c8905a10..8c18c2cf 100644 --- a/hostmetadata/host/host.go +++ b/hostmetadata/host/host.go @@ -8,6 +8,7 @@ package host import ( "bytes" + "errors" "fmt" "io" "net" @@ -19,25 +20,27 @@ import ( "strings" "sync" + "github.com/elastic/otel-profiling-agent/pfnamespaces" + "github.com/jsimonetti/rtnetlink" log "github.com/sirupsen/logrus" "github.com/syndtr/gocapability/capability" - "go.uber.org/multierr" "golang.org/x/sys/unix" "github.com/elastic/otel-profiling-agent/config" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/pfnamespaces" ) // Host metadata keys // Changing these values is a customer-visible change. const ( - KeyKernelProcVersion = "host:kernel_proc_version" - KeyKernelVersion = "host:kernel_version" - KeyHostname = "host:hostname" - KeyMachine = "host:machine" - KeyIPAddress = "host:ip" + // TODO: Change to semconv / ECS names + KeyKernelProcVersion = "host:kernel_proc_version" + KeyKernelVersion = "host:kernel_version" + KeyHostname = "host:hostname" + KeyArchitecture = "host:arch" + KeyArchitectureCompat = "host:machine" + KeyIPAddress = "host:ip" // Prefix for all the sysctl keys keyPrefixSysctl = "host:sysctl/" @@ -148,7 +151,7 @@ func AddMetadata(caEndpoint string, result map[string]string) error { for _, sysctl := range sysctls { sysctlValue, err2 := getSysctl(sysctl) if err2 != nil { - errResult = multierr.Combine(errResult, err2) + errResult = errors.Join(errResult, err2) continue } sysctlKey := keyPrefixSysctl + sysctl @@ -159,7 +162,7 @@ func AddMetadata(caEndpoint string, result map[string]string) error { var ip net.IP ip, err = getSourceIPAddress(host) if err != nil { - errResult = multierr.Combine(errResult, err) + errResult = errors.Join(errResult, err) } else { result[KeyIPAddress] = ip.String() } @@ -167,11 +170,26 @@ func AddMetadata(caEndpoint string, result map[string]string) error { // Get uname-related metadata: hostname and kernel version uname := &unix.Utsname{} if err = unix.Uname(uname); err != nil { - errResult = multierr.Combine(errResult, fmt.Errorf("error calling uname: %v", err)) + errResult = errors.Join(errResult, fmt.Errorf("error calling uname: %v", err)) } else { result[KeyKernelVersion] = sanitizeString(uname.Release[:]) result[KeyHostname] = sanitizeString(uname.Nodename[:]) - result[KeyMachine] = sanitizeString(uname.Machine[:]) + + machine := sanitizeString(uname.Machine[:]) + + // Keep sending the old field for compatibility between old collectors and new agents. + result[KeyArchitectureCompat] = machine + + // Convert to OTEL semantic conventions. + // Machine values other than x86_64, aarch64 are not converted. + switch machine { + case "x86_64": + result[KeyArchitecture] = "amd64" + case "aarch64": + result[KeyArchitecture] = "arm64" + default: + result[KeyArchitecture] = machine + } } }() @@ -251,7 +269,7 @@ func addressFamily(ip net.IP) (uint8, error) { func getSourceIPAddress(domain string) (net.IP, error) { conn, err := rtnetlink.Dial(nil) if err != nil { - return nil, fmt.Errorf("unable to open netlink connection") + return nil, errors.New("unable to open netlink connection") } defer conn.Close() @@ -325,11 +343,11 @@ func getSourceIPAddress(domain string) (net.IP, error) { func hasCapSysAdmin() (bool, error) { caps, err := capability.NewPid2(0) // 0 == current process if err != nil { - return false, fmt.Errorf("unable to get process capabilities") + return false, errors.New("unable to get process capabilities") } err = caps.Load() if err != nil { - return false, fmt.Errorf("unable to load process capabilities") + return false, errors.New("unable to load process capabilities") } return caps.Get(capability.EFFECTIVE, capability.CAP_SYS_ADMIN), nil } diff --git a/hostmetadata/host/host_test.go b/hostmetadata/host/host_test.go index 10994dd3..446c8023 100644 --- a/hostmetadata/host/host_test.go +++ b/hostmetadata/host/host_test.go @@ -15,6 +15,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/elastic/otel-profiling-agent/config" "github.com/elastic/otel-profiling-agent/libpf" @@ -45,9 +46,7 @@ func TestAddMetadata(t *testing.T) { CacheDirectory: ".", SecretToken: "secret", ValidatedTags: validatedTags}) - if err != nil { - t.Fatalf("failed to set temporary config: %s", err) - } + require.NoError(t, err) // This tests checks that common metadata keys are populated metadataMap := make(map[string]string) @@ -56,64 +55,36 @@ func TestAddMetadata(t *testing.T) { // the returned map, which ensures test coverage. _ = AddMetadata("localhost:12345", metadataMap) expectedHostname, err := os.Hostname() - if err != nil { - t.Fatal(err) - } - actualHostname, found := metadataMap[KeyHostname] - if !found { - t.Fatalf("no hostname found") - } - if actualHostname != expectedHostname { - t.Fatalf("wrong hostname, expected %v, got %v", expectedHostname, actualHostname) - } - - tags, found := metadataMap[keyTags] - if !found { - t.Fatalf("no tags found") - } - - if tags != validatedTags { - t.Fatalf("added tags '%s' != validated tags '%s'", tags, validatedTags) - } + require.NoError(t, err) - ip, found := metadataMap[KeyIPAddress] - if !found { - t.Fatalf("no IP address") + if actualHostname, found := metadataMap[KeyHostname]; assert.True(t, found) { + assert.Equal(t, expectedHostname, actualHostname) } - - parsedIP := net.ParseIP(ip) - if parsedIP == nil { - t.Fatalf("got a nil IP address") - } - - procVersion, found := metadataMap[KeyKernelProcVersion] - if !found { - t.Fatalf("no kernel_proc_version") - } - - expectedProcVersion, err := os.ReadFile("/proc/version") - if err != nil { - t.Fatal(err) + if tags, found := metadataMap[keyTags]; assert.True(t, found) { + assert.Equal(t, validatedTags, tags) } - if procVersion != sanitizeString(expectedProcVersion) { - t.Fatalf("wrong kernel_proc_version, expected %v, got %v", procVersion, expectedProcVersion) + if ip, found := metadataMap[KeyIPAddress]; assert.True(t, found) { + parsedIP := net.ParseIP(ip) + assert.NotNil(t, parsedIP) } - - _, found = metadataMap[KeyKernelVersion] - if !found { - t.Fatalf("no kernel version") + if procVersion, found := metadataMap[KeyKernelProcVersion]; assert.True(t, found) { + expectedProcVersion, err := os.ReadFile("/proc/version") + if assert.NoError(t, err) { + assert.Equal(t, sanitizeString(expectedProcVersion), procVersion) + } } + _, found := metadataMap[KeyKernelVersion] + assert.True(t, found) // The below test for bpf_jit_enable may not be reproducible in all environments, as we may not // be able to read the value depending on the capabilities/privileges/network namespace of the // test process. jitEnabled, found := metadataMap["host:sysctl/net.core.bpf_jit_enable"] - if found { switch jitEnabled { case "0", "1", "2": default: - t.Fatalf("unexpected value for sysctl: %v", jitEnabled) + assert.Fail(t, "unexpected value for sysctl: %v", jitEnabled) } } @@ -125,7 +96,7 @@ func TestAddMetadata(t *testing.T) { cacheSockets, ok := metadataMap[keySocketID(cacheKey)] assert.True(t, ok) assert.NotEmpty(t, cacheSockets) - assert.True(t, cacheSockets[0] == '0', + assert.Equal(t, "0", cacheSockets[0:1], "expected '0' at start of '%v'", cacheSockets) sids := strings.Split(cacheSockets, ",") socketIDs := libpf.MapSlice(sids, func(a string) int { diff --git a/hostmetadata/hostmetadata.json b/hostmetadata/hostmetadata.json deleted file mode 100644 index eb6fca21..00000000 --- a/hostmetadata/hostmetadata.json +++ /dev/null @@ -1,619 +0,0 @@ -[ - { - "name": "agent:version", - "field": "profiling.agent.version", - "type": "string" - }, - { - "name": "agent:revision", - "field": "profiling.agent.revision", - "type": "string" - }, - { - "name": "agent:build_timestamp", - "field": "profiling.agent.build_timestamp", - "type": "uint32" - }, - { - "name": "agent:start_time", - "obsolete": true - }, - { - "name": "agent:start_time_milli", - "field": "profiling.agent.start_time", - "type": "int" - }, - { - "name": "agent:config_bpf_log_level", - "field": "profiling.agent.config.bpf_log_level", - "type": "uint32" - }, - { - "name": "agent:config_bpf_log_size", - "field": "profiling.agent.config.bpf_log_size", - "type": "int" - }, - { - "name": "agent:config_cache_directory", - "field": "profiling.agent.config.cache_directory", - "type": "string" - }, - { - "name": "agent:config_ca_address", - "field": "profiling.agent.config.ca_address", - "type": "string" - }, - { - "name": "agent:config_file", - "field": "profiling.agent.config.file", - "type": "string" - }, - { - "name": "agent:config_tags", - "field": "profiling.agent.config.tags", - "type": "string" - }, - { - "name": "agent:config_disable_tls", - "field": "profiling.agent.config.disable_tls", - "type": "bool" - }, - { - "name": "agent:config_no_kernel_version_check", - "field": "profiling.agent.config.no_kernel_version_check", - "type": "bool" - }, - { - "name": "agent:config_upload_symbols", - "field": "profiling.agent.config.upload_symbols", - "type": "bool" - }, - { - "name": "agent:config_tracers", - "field": "profiling.agent.config.tracers", - "type": "string" - }, - { - "name": "agent:config_known_traces_entries", - "field": "profiling.agent.config.known_traces_entries", - "type": "uint32" - }, - { - "name": "agent:config_map_scale_factor", - "field": "profiling.agent.config.map_scale_factor", - "type": "uint8" - }, - { - "name": "agent:config_max_elements_per_interval", - "field": "profiling.agent.config.max_elements_per_interval", - "type": "uint32" - }, - { - "name": "agent:config_verbose", - "field": "profiling.agent.config.verbose", - "type": "bool" - }, - { - "name": "agent:config_probabilistic_interval", - "field": "profiling.agent.config.probabilistic_interval", - "type": "duration" - }, - { - "name": "agent:config_probabilistic_threshold", - "field": "profiling.agent.config.probabilistic_threshold", - "type": "uint8" - }, - { - "name": "agent:config_present_cpu_cores", - "field": "profiling.agent.config.present_cpu_cores", - "type": "uint16" - }, - { - "name": "host:ip", - "field": "profiling.host.ip", - "type": "string" - }, - { - "name": "host:tags", - "field": "profiling.host.tags", - "type": "array", - "separator": ";" - }, - { - "name": "host:hostname", - "field": "profiling.host.name", - "type": "string" - }, - { - "name": "host:machine", - "field": "profiling.host.machine", - "type": "string" - }, - { - "name": "host:kernel_version", - "field": "profiling.host.kernel_version", - "type": "string" - }, - { - "name": "host:kernel_proc_version", - "field": "profiling.host.kernel_proc_version", - "type": "string" - }, - { - "name": "host:sysctl/kernel.bpf_stats_enabled", - "field": "profiling.host.sysctl.kernel.bpf_stats_enabled", - "type": "uint32" - }, - { - "name": "host:sysctl/kernel.unprivileged_bpf_disabled", - "field": "profiling.host.sysctl.kernel.unprivileged_bpf_disabled", - "type": "uint32" - }, - { - "name": "host:sysctl/net.core.bpf_jit_enable", - "field": "profiling.host.sysctl.net.core.bpf_jit_enable", - "type": "uint32" - }, - { - "name": "host:sysctl/net.core.bpf_jit_enable", - "field": "profiling.host.sysctl.net.core.bpf_jit_enable", - "type": "uint32" - }, - { - "name": "instance:public-ipv4s", - "field": "profiling.instance.public_ipv4s", - "type": "array", - "separator": "," - }, - { - "name": "instance:private-ipv4s", - "field": "profiling.instance.private_ipv4s", - "type": "array", - "separator": "," - }, - { - "name": "instance:public-ipv6s", - "field": "profiling.instance.public_ipv6s", - "type": "array", - "separator": "," - }, - { - "name": "instance:private-ipv6s", - "field": "profiling.instance.private_ipv6s", - "type": "array", - "separator": "," - }, - { - "name": "host:cpu/cpus", - "field": "profiling.host.cpu.cpus.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/cpus/socketIDs", - "field": "profiling.host.cpu.cpus.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/threads-per-core", - "field": "profiling.host.cpu.threads_per_core.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/threads-per-core/socketIDs", - "field": "profiling.host.cpu.threads_per_core.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/cores-per-socket", - "field": "profiling.host.cpu.cores_per_socket.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/cores-per-socket/socketIDs", - "field": "profiling.host.cpu.cores_per_socket.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/vendor", - "field": "profiling.host.cpu.vendor.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/vendor/socketIDs", - "field": "profiling.host.cpu.vendor.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/model", - "field": "profiling.host.cpu.model.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/model/socketIDs", - "field": "profiling.host.cpu.model.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/model-name", - "field": "profiling.host.cpu.model_name.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/model-name/socketIDs", - "field": "profiling.host.cpu.model_name.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/online", - "field": "profiling.host.cpu.online.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/online/socketIDs", - "field": "profiling.host.cpu.online.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/stepping", - "field": "profiling.host.cpu.stepping.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/stepping/socketIDs", - "field": "profiling.host.cpu.stepping.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/flags", - "field": "profiling.host.cpu.flags.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/flags/socketIDs", - "field": "profiling.host.cpu.flags.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/bugs", - "field": "profiling.host.cpu.bugs.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/bugs/socketIDs", - "field": "profiling.host.cpu.bugs.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/cache/L1i-kbytes", - "field": "profiling.host.cpu.cache.L1i_kbytes.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/cache/L1i-kbytes/socketIDs", - "field": "profiling.host.cpu.cache.L1i_kbytes.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/cache/L1d-kbytes", - "field": "profiling.host.cpu.cache.L1d_kbytes.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/cache/L1d-kbytes/socketIDs", - "field": "profiling.host.cpu.cache.L1d_kbytes.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/cache/L2-kbytes", - "field": "profiling.host.cpu.cache.L2_kbytes.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/cache/L2-kbytes/socketIDs", - "field": "profiling.host.cpu.cache.L2_kbytes.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/cache/L3-kbytes", - "field": "profiling.host.cpu.cache.L3_kbytes.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/cache/L3-kbytes/socketIDs", - "field": "profiling.host.cpu.cache.L3_kbytes.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/clock/max-mhz", - "field": "profiling.host.cpu.clock.max_mhz.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/clock/max-mhz/socketIDs", - "field": "profiling.host.cpu.clock.max_mhz.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/clock/min-mhz", - "field": "profiling.host.cpu.clock.min_mhz.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/clock/min-mhz/socketIDs", - "field": "profiling.host.cpu.clock.min_mhz.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/clock/scaling-cur-freq-mhz", - "field": "profiling.host.cpu.clock.scaling_cur_freq_mhz.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/clock/scaling-cur-freq-mhz/socketIDs", - "field": "profiling.host.cpu.clock.scaling_cur_freq_mhz.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/clock/scaling-driver", - "field": "profiling.host.cpu.clock.scaling_driver.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/clock/scaling-driver/socketIDs", - "field": "profiling.host.cpu.clock.scaling_driver.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/clock/scaling-governor", - "field": "profiling.host.cpu.clock.scaling_governor.value", - "type": "array", - "separator": ";" - }, - { - "name": "host:cpu/clock/scaling-governor/socketIDs", - "field": "profiling.host.cpu.clock.scaling_governor.sockets", - "type": "array", - "separator": ";" - }, - { - "name": "ec2:ami-id", - "field": "ec2.ami_id", - "type": "string" - }, - { - "name": "ec2:ami-manifest-path", - "field": "ec2.ami_manifest_path", - "type": "string" - }, - { - "name": "ec2:ancestor-ami-ids", - "field": "ec2.ancestor_ami_ids", - "type": "string" - }, - { - "name": "ec2:hostname", - "field": "ec2.hostname", - "type": "string" - }, - { - "name": "ec2:instance-id", - "field": "ec2.instance_id", - "type": "string" - }, - { - "name": "ec2:instance-type", - "field": "ec2.instance_type", - "type": "string" - }, - { - "name": "ec2:instance-life-cycle", - "field": "ec2.instance_life_cycle", - "type": "string" - }, - { - "name": "ec2:local-hostname", - "field": "ec2.local_hostname", - "type": "string" - }, - { - "name": "ec2:local-ipv4", - "field": "ec2.local_ipv4", - "type": "string" - }, - { - "name": "ec2:kernel-id", - "field": "ec2.kernel_id", - "type": "string" - }, - { - "name": "ec2:mac", - "field": "ec2.mac", - "type": "string" - }, - { - "name": "ec2:profile", - "field": "ec2.profile", - "type": "string" - }, - { - "name": "ec2:public-hostname", - "field": "ec2.public_hostname", - "type": "string" - }, - { - "name": "ec2:public-ipv4", - "field": "ec2.public_ipv4", - "type": "string" - }, - { - "name": "ec2:product-codes", - "field": "ec2.product_codes", - "type": "string" - }, - { - "name": "ec2:security-groups", - "field": "ec2.security_groups", - "type": "string" - }, - { - "name": "ec2:placement/availability-zone", - "field": "ec2.placement.availability_zone", - "type": "string" - }, - { - "name": "ec2:placement/availability-zone-id", - "field": "ec2.placement.availability_zone_id", - "type": "string" - }, - { - "name": "ec2:placement/region", - "field": "ec2.placement.region", - "type": "string" - }, - { - "name": "azure:compute/sku", - "field": "azure.compute.sku", - "type": "string" - }, - { - "name": "azure:compute/name", - "field": "azure.compute.name", - "type": "string" - }, - { - "name": "azure:compute/zone", - "field": "azure.compute.zone", - "type": "string" - }, - { - "name": "azure:compute/vmid", - "field": "azure.compute.vmid", - "type": "string" - }, - { - "name": "azure:compute/tags", - "field": "azure.compute.tags", - "type": "string" - }, - { - "name": "azure:compute/offer", - "field": "azure.compute.offer", - "type": "string" - }, - { - "name": "azure:compute/vmsize", - "field": "azure.compute.vmsize", - "type": "string" - }, - { - "name": "azure:compute/ostype", - "field": "azure.compute.ostype", - "type": "string" - }, - { - "name": "azure:compute/version", - "field": "azure.compute.version", - "type": "string" - }, - { - "name": "azure:compute/location", - "field": "azure.compute.location", - "type": "string" - }, - { - "name": "azure:compute/publisher", - "field": "azure.compute.publisher", - "type": "string" - }, - { - "name": "azure:compute/environment", - "field": "azure.compute.environment", - "type": "string" - }, - { - "name": "azure:compute/subscriptionid", - "field": "azure.compute.subscriptionid", - "type": "string" - }, - { - "name": "gce:instance/id", - "field": "gce.instance.id", - "type": "string" - }, - { - "name": "gce:instance/cpu-platform", - "field": "gce.instance.cpu_platform", - "type": "string" - }, - { - "name": "gce:instance/description", - "field": "gce.instance.description", - "type": "string" - }, - { - "name": "gce:instance/hostname", - "field": "gce.instance.hostname", - "type": "string" - }, - { - "name": "gce:instance/image", - "field": "gce.instance.image", - "type": "string" - }, - { - "name": "gce:instance/machine-type", - "field": "gce.instance.machine_type", - "type": "string" - }, - { - "name": "gce:instance/name", - "field": "gce.instance.name", - "type": "string" - }, - { - "name": "gce:instance/tags", - "field": "gce.instance.tags", - "type": "string" - }, - { - "name": "gce:instance/zone", - "field": "gce.instance.zone", - "type": "string" - } -] diff --git a/hostmetadata/instance/common_test.go b/hostmetadata/instance/common_test.go index 7245ebfe..21ccaf72 100644 --- a/hostmetadata/instance/common_test.go +++ b/hostmetadata/instance/common_test.go @@ -7,68 +7,45 @@ package instance import ( - "reflect" "testing" + + "github.com/stretchr/testify/assert" ) func TestEnumerate(t *testing.T) { r := Enumerate("") - if !reflect.DeepEqual([]string{}, r) { - t.Fatalf("unexpected result: %#v", r) - } + assert.Equal(t, []string{}, r) r = Enumerate("\n") - if !reflect.DeepEqual([]string{}, r) { - t.Fatalf("unexpected result: %#v", r) - } + assert.Equal(t, []string{}, r) r = Enumerate("\n\n") - if !reflect.DeepEqual([]string{}, r) { - t.Fatalf("unexpected result: %#v", r) - } + assert.Equal(t, []string{}, r) r = Enumerate("\nhello\n") - if !reflect.DeepEqual([]string{"hello"}, r) { - t.Fatalf("unexpected result: %#v", r) - } + assert.Equal(t, []string{"hello"}, r) r = Enumerate("\nhello/\n") - if !reflect.DeepEqual([]string{"hello"}, r) { - t.Fatalf("unexpected result: %#v", r) - } + assert.Equal(t, []string{"hello"}, r) r = Enumerate("hi\nhello/\n") - if !reflect.DeepEqual([]string{"hi", "hello"}, r) { - t.Fatalf("unexpected result: %#v", r) - } + assert.Equal(t, []string{"hi", "hello"}, r) r = Enumerate("hi\nhello/\n\nbye") - if !reflect.DeepEqual([]string{"hi", "hello", "bye"}, r) { - t.Fatalf("unexpected result: %#v", r) - } + assert.Equal(t, []string{"hi", "hello", "bye"}, r) r = Enumerate("\nbye") - if !reflect.DeepEqual([]string{"bye"}, r) { - t.Fatalf("unexpected result: %#v", r) - } + assert.Equal(t, []string{"bye"}, r) r = Enumerate("hello/\n") - if !reflect.DeepEqual([]string{"hello"}, r) { - t.Fatalf("unexpected result: %#v", r) - } + assert.Equal(t, []string{"hello"}, r) r = Enumerate("hello/") - if !reflect.DeepEqual([]string{"hello"}, r) { - t.Fatalf("unexpected result: %#v", r) - } + assert.Equal(t, []string{"hello"}, r) r = Enumerate("\nhello/ \n") - if !reflect.DeepEqual([]string{"hello"}, r) { - t.Fatalf("unexpected result: %#v", r) - } + assert.Equal(t, []string{"hello"}, r) r = Enumerate("hi\n \nbye") - if !reflect.DeepEqual([]string{"hi", "bye"}, r) { - t.Fatalf("unexpected result: %#v", r) - } + assert.Equal(t, []string{"hi", "bye"}, r) } diff --git a/hostmetadata/instance/instance.go b/hostmetadata/instance/instance.go index 21fd4743..1a0d3c76 100644 --- a/hostmetadata/instance/instance.go +++ b/hostmetadata/instance/instance.go @@ -21,6 +21,10 @@ const ( KeyPublicIPV6s = "public-ipv6s" KeyPrivateIPV6s = "private-ipv6s" + + KeyCloudProvider = "cloud:provider" + KeyCloudRegion = "cloud:region" + KeyHostType = "host:type" ) func AddToResult(metadata map[string][]string, result map[string]string) { diff --git a/interpreter/apmint/apmint.go b/interpreter/apmint/apmint.go new file mode 100644 index 00000000..a1067bdb --- /dev/null +++ b/interpreter/apmint/apmint.go @@ -0,0 +1,250 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +// Package apmint implements a pseudo interpreter handler that detects APM agent +// libraries, establishes socket connections with them and notifies them about +// the stack traces that we collected for their process. This allows the APM +// agent to associate stack traces with APM traces / transactions / spans. +package apmint + +import ( + "encoding/hex" + "errors" + "fmt" + "regexp" + "unsafe" + + "github.com/elastic/otel-profiling-agent/host" + "github.com/elastic/otel-profiling-agent/interpreter" + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/libpf/pfelf" + "github.com/elastic/otel-profiling-agent/remotememory" + "github.com/elastic/otel-profiling-agent/util" + + log "github.com/sirupsen/logrus" +) + +// #include +// #include "../../support/ebpf/types.h" +import "C" + +const ( + // serviceNameMaxLength defines the maximum allowed length of service names. + serviceNameMaxLength = 128 + // serviceEnvMaxLength defines the maximum allowed length of service environments. + serviceEnvMaxLength = 128 + // socketPathMaxLength defines the maximum length of the APM agent socket path. + socketPathMaxLength = 1024 + + // procStorageExport defines the name of the process storage ELF export. + procStorageExport = "elastic_apm_profiling_correlation_process_storage_v1" + // tlsExport defines the name of the thread info TLS export. + tlsExport = "elastic_apm_profiling_correlation_tls_v1" +) + +var dsoRegex = regexp.MustCompile(`.*/elastic-jvmti-linux-([\w-]*)\.so`) + +// apmProcessStorage represents a subset of the information present in the +// APM process storage. +// +// nolint:lll +// https://github.com/elastic/apm/blob/bd5fa9c1/specs/agents/universal-profiling-integration.md#process-storage-layout +type apmProcessStorage struct { + ServiceName string + TraceSocketPath string +} + +// Loader implements interpreter.Loader. +func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { + if !isPotentialAgentLib(info.FileName()) { + return nil, nil + } + + ef, err := info.GetELF() + if err != nil { + return nil, err + } + + // Resolve process storage symbol. + procStorageSym, err := ef.LookupSymbol(procStorageExport) + if err != nil { + if errors.Is(err, pfelf.ErrSymbolNotFound) { + // APM<->profiling integration not supported by agent. + return nil, nil + } + + return nil, err + } + if procStorageSym.Size != 8 { + return nil, fmt.Errorf("process storage export has wrong size %d", procStorageSym.Size) + } + + // Resolve thread info TLS export. + tlsDescs, err := ef.TLSDescriptors() + if err != nil { + return nil, errors.New("failed to extract TLS descriptors") + } + tlsDescElfAddr, ok := tlsDescs[tlsExport] + if !ok { + return nil, errors.New("failed to locate TLS descriptor") + } + + log.Debugf("APM integration TLS descriptor offset: 0x%08X", tlsDescElfAddr) + + return &data{ + tlsDescElfAddr: tlsDescElfAddr, + procStorageElfVA: libpf.Address(procStorageSym.Address), + }, nil +} + +type data struct { + tlsDescElfAddr libpf.Address + procStorageElfVA libpf.Address +} + +var _ interpreter.Data = &data{} + +func (d data) Attach(ebpf interpreter.EbpfHandler, pid util.PID, + bias libpf.Address, rm remotememory.RemoteMemory) (interpreter.Instance, error) { + procStorage, err := readProcStorage(rm, bias+d.procStorageElfVA) + if err != nil { + return nil, fmt.Errorf("failed to read APM correlation process storage: %s", err) + } + + // Read TLS offset from the TLS descriptor. + tlsOffset := rm.Uint64(bias + d.tlsDescElfAddr + 8) + procInfo := C.ApmIntProcInfo{tls_offset: C.u64(tlsOffset)} + if err = ebpf.UpdateProcData(libpf.APMInt, pid, unsafe.Pointer(&procInfo)); err != nil { + return nil, err + } + + // Establish socket connection with the agent. + socket, err := openAPMAgentSocket(pid, procStorage.TraceSocketPath) + if err != nil { + log.Warnf("Failed to open APM agent socket for PID %d", pid) + } + + log.Debugf("PID %d apm.service.name: %s, trace socket: %s", + pid, procStorage.ServiceName, procStorage.TraceSocketPath) + + return &Instance{ + serviceName: procStorage.ServiceName, + socket: socket, + }, nil +} + +type Instance struct { + serviceName string + socket *apmAgentSocket + interpreter.InstanceStubs +} + +var _ interpreter.Instance = &Instance{} + +// Detach implements the interpreter.Instance interface. +func (i *Instance) Detach(ebpf interpreter.EbpfHandler, pid util.PID) error { + return ebpf.DeleteProcData(libpf.APMInt, pid) +} + +// NotifyAPMAgent sends out collected traces to the connected APM agent. +func (i *Instance) NotifyAPMAgent( + pid util.PID, rawTrace *host.Trace, umTraceHash libpf.TraceHash, count uint16) { + if rawTrace.APMTransactionID == libpf.InvalidAPMSpanID || i.socket == nil { + return + } + + log.Debugf("Reporting %dx trace hash %s -> TX %s for PID %d", + count, umTraceHash.StringNoQuotes(), + hex.EncodeToString(rawTrace.APMTransactionID[:]), pid) + + msg := traceCorrMsg{ + MessageType: 1, + MinorVersion: 1, + APMTraceID: rawTrace.APMTraceID, + APMTransactionID: rawTrace.APMTransactionID, + StackTraceID: umTraceHash, + Count: count, + } + + if err := i.socket.SendMessage(msg.Serialize()); err != nil { + log.Debugf("Failed to send trace mappings to APM agent: %v", err) + } +} + +// APMServiceName returns the service name advertised by the APM agent. +func (i *Instance) APMServiceName() string { + return i.serviceName +} + +// isPotentialAgentLib checks whether the given path looks like a Java APM agent library. +func isPotentialAgentLib(path string) bool { + return dsoRegex.MatchString(path) +} + +// nextString reads the next `utf8-str` from memory and updates addr accordingly. +// +// nolint:lll +// https://github.com/elastic/apm/blob/bd5fa9c1/specs/agents/universal-profiling-integration.md#general-memory-layout +func nextString(rm remotememory.RemoteMemory, addr *libpf.Address, maxLen int) (string, error) { + length := int(rm.Uint32(*addr)) + *addr += 4 + + if length == 0 { + return "", nil + } + + if length > maxLen { + return "", fmt.Errorf("APM string length %d exceeds maximum length of %d", length, maxLen) + } + + raw := make([]byte, length) + if _, err := rm.ReadAt(raw, int64(*addr)); err != nil { + return "", errors.New("failed to read memory") + } + + *addr += libpf.Address(length) + return string(raw), nil +} + +// readProcStorage reads the APM process storage from memory. +// +// nolint:lll +// https://github.com/elastic/apm/blob/bd5fa9c1/specs/agents/universal-profiling-integration.md#process-storage-layout +func readProcStorage( + rm remotememory.RemoteMemory, + procStorageAddr libpf.Address, +) (*apmProcessStorage, error) { + readPtr := rm.Ptr(procStorageAddr) + if readPtr == 0 { + return nil, errors.New("failed to read Java agent process state pointer") + } + + // Skip `layout-minor-version` field: not relevant until values != 1 exist. + // The specification guarantees that the struct can only be extended by adding + // new fields after the old ones. + readPtr += 2 + + serviceName, err := nextString(rm, &readPtr, serviceNameMaxLength) + if err != nil { + return nil, err + } + + // Currently not used by us. + _, err = nextString(rm, &readPtr, serviceEnvMaxLength) + if err != nil { + return nil, err + } + + socketPath, err := nextString(rm, &readPtr, socketPathMaxLength) + if err != nil { + return nil, err + } + + return &apmProcessStorage{ + ServiceName: serviceName, + TraceSocketPath: socketPath, + }, nil +} diff --git a/interpreter/apmint/socket.go b/interpreter/apmint/socket.go new file mode 100644 index 00000000..914f7bfc --- /dev/null +++ b/interpreter/apmint/socket.go @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package apmint + +import ( + "bufio" + "bytes" + "encoding/binary" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" + + "golang.org/x/sys/unix" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/libpf/xsync" + "github.com/elastic/otel-profiling-agent/stringutil" + "github.com/elastic/otel-profiling-agent/util" +) + +// sendSocket is the shared, unbound socket that we use for communication with +// all agents. It is initialized once when the first APM agent shows up and then +// never closed until HA exit. +var sendSocket xsync.Once[int] + +// apmAgentSocket represents a unix socket connection to an APM agent. +type apmAgentSocket struct { + addr unix.SockaddrUnix +} + +// openAPMAgentSocket opens the APM unix socket in the given PID's root filesystem. +// +// This method never blocks. +func openAPMAgentSocket(pid util.PID, socketPath string) (*apmAgentSocket, error) { + // Ensure that the socket path can't escape our root. + socketPath = filepath.Clean(socketPath) + for _, segment := range strings.Split(socketPath, "/") { + if segment == ".." { + return nil, errors.New("socket path escapes root") + } + } + + // Prepend root system to ensure that this also works with containerized apps. + socketPath = fmt.Sprintf("/proc/%d/root/%s", pid, socketPath) + + // Read effective UID/GID of the APM agent process. + euid, egid, err := readProcessOwner(pid) + if err != nil { + return nil, fmt.Errorf("failed to determine owner of APM process: %v", err) + } + + // Stat socket and check whether the APM agent process is allowed to access it. + stat, err := os.Stat(socketPath) + if err != nil { + return nil, errors.New("failed to stat file") + } + unixStat, ok := stat.Sys().(*syscall.Stat_t) + if !ok || unixStat == nil { + return nil, errors.New("failed to get unix stat object from stat") + } + if stat.Mode().Type() != fs.ModeSocket { + return nil, errors.New("file is not a socket") + } + + userMayAccess := stat.Mode()&unix.S_IWUSR == unix.S_IWUSR && unixStat.Uid == euid + groupMayAccess := stat.Mode()&unix.S_IWGRP == unix.S_IWGRP && unixStat.Gid == egid + anyoneMayAccess := stat.Mode()&unix.S_IWOTH == unix.S_IWOTH + if euid != 0 && !anyoneMayAccess && !userMayAccess && !groupMayAccess { + return nil, errors.New("APM process does not have perms to open socket") + } + + return &apmAgentSocket{addr: unix.SockaddrUnix{Name: socketPath}}, nil +} + +// SendMessage tries sending the given datagram to the APM agent. +// +// This function intentionally never blocks. If the agent's receive buffer is +// full or the socket was closed, an error is returned and the message is +// discarded. +func (s *apmAgentSocket) SendMessage(msg []byte) error { + fd, err := sendSocket.GetOrInit(func() (int, error) { + return unix.Socket(unix.AF_UNIX, unix.SOCK_DGRAM, 0) + }) + if err != nil { + return fmt.Errorf("failed to create global send socket: %v", err) + } + + return unix.Sendto(*fd, msg, unix.MSG_DONTWAIT, &s.addr) +} + +// traceCorrMsg represents a trace correlation socket message. +// +// nolint:lll +// https://github.com/elastic/apm/blob/bd5fa9c1/specs/agents/universal-profiling-integration.md#cpu-profiler-trace-correlation-message +type traceCorrMsg struct { + MessageType uint16 + MinorVersion uint16 + APMTraceID libpf.APMTraceID + APMTransactionID libpf.APMTransactionID + StackTraceID libpf.TraceHash + Count uint16 +} + +func (m *traceCorrMsg) Serialize() []byte { + var buf bytes.Buffer + _ = binary.Write(&buf, binary.LittleEndian, m.MessageType) + _ = binary.Write(&buf, binary.LittleEndian, m.MinorVersion) + _, _ = buf.Write(m.APMTraceID[:]) + _, _ = buf.Write(m.APMTransactionID[:]) + _, _ = buf.Write(m.StackTraceID.Bytes()) + _ = binary.Write(&buf, binary.LittleEndian, m.Count) + return buf.Bytes() +} + +// readProcessOwner reads the effective UID and GID of the target process. +func readProcessOwner(pid util.PID) (euid, egid uint32, err error) { + statusFd, err := os.Open(fmt.Sprintf("/proc/%d/status", pid)) + if err != nil { + return 0, 0, fmt.Errorf("failed to open process status: %v", err) + } + defer statusFd.Close() + + scanner := bufio.NewScanner(statusFd) + found := 0 + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "Uid:") { + euid, err = parseUIDGIDLine(line) + if err != nil { + return 0, 0, err + } + found++ + } else if strings.HasPrefix(line, "Gid:") { + egid, err = parseUIDGIDLine(line) + if err != nil { + return 0, 0, err + } + found++ + } + } + if scanner.Err() != nil { + return 0, 0, fmt.Errorf("failed to read process status: %v", err) + } + + if found != 2 { + return 0, 0, errors.New("either euid or egid are missing") + } + + return euid, egid, nil +} + +// parseUIDGIDLine parses the "Uid:" and "Gid:" lines in /proc/$/status. +func parseUIDGIDLine(line string) (uint32, error) { + var fields [5]string + if stringutil.FieldsN(line, fields[:]) != 5 { + return 0, fmt.Errorf("unexpedted `Uid` line layout: %s", line) + } + + // Fields: real, effective, saved, FS UID + eid, err := strconv.Atoi(fields[2]) + if err != nil { + return 0, fmt.Errorf("failed to parse uid/gid int: %v", err) + } + + return uint32(eid), nil +} diff --git a/interpreter/dotnet/cachingreader.go b/interpreter/dotnet/cachingreader.go new file mode 100644 index 00000000..4a940a2b --- /dev/null +++ b/interpreter/dotnet/cachingreader.go @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package dotnet + +import ( + "io" +) + +// CachingReader allows reading data from the remote process using io.ReadByte and +// io.Reader interfaces. It provides also simple cache. +type cachingReader struct { + // r is the ReaderAt from which we are reading the data from + r io.ReaderAt + // buf contains all data read from the target process + buf []byte + // addr is the target offset to continue reading from + addr int64 + // i is the index to the buf[] byte which is to be returned next in ReadByte() + i int +} + +// ReadByte implements io.ByteReader interface to do cached single byte reads. +func (cr *cachingReader) ReadByte() (byte, error) { + // Readahead to buffer if needed + if cr.i >= len(cr.buf) { + cr.i = 0 + _, err := cr.r.ReadAt(cr.buf, cr.addr) + if err != nil { + return 0, err + } + cr.addr += int64(len(cr.buf)) + } + // Return byte from buffer + b := cr.buf[cr.i] + cr.i++ + return b, nil +} + +// Skip consumes numBytes without copying them +func (cr *cachingReader) Skip(numBytes int) { + if cr.i+numBytes < len(cr.buf) { + cr.i += numBytes + return + } + numBytes -= len(cr.buf) - cr.i + cr.i = len(cr.buf) + cr.addr += int64(numBytes) +} + +// Read implements io.Reader interface to read from the target. +func (cr *cachingReader) Read(buf []byte) (int, error) { + offs := 0 + if cr.i < len(cr.buf) { + // Read from the cache + cache := cr.buf[cr.i:] + if len(cache) > len(buf) { + cache = cache[:len(buf)] + } + copy(buf, cache) + offs = len(cache) + cr.i += len(cache) + if len(buf) == len(cache) { + return offs, nil + } + } + + // Satisfy rest of the read directly + n, err := cr.r.ReadAt(buf[offs:], cr.addr) + return offs + n, err +} + +// Reader returns a cachingReader to read and record data from given start. +func newCachingReader(r io.ReaderAt, addr int64, cacheSize int) *cachingReader { + return &cachingReader{ + r: r, + addr: addr, + i: cacheSize, + buf: make([]byte, cacheSize), + } +} diff --git a/interpreter/dotnet/data.go b/interpreter/dotnet/data.go new file mode 100644 index 00000000..487d0e93 --- /dev/null +++ b/interpreter/dotnet/data.go @@ -0,0 +1,271 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package dotnet + +import ( + "fmt" + "unsafe" + + log "github.com/sirupsen/logrus" + + "github.com/elastic/go-freelru" + "github.com/elastic/otel-profiling-agent/interpreter" + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/remotememory" + "github.com/elastic/otel-profiling-agent/util" +) + +// #include "../../support/ebpf/types.h" +import "C" + +type dotnetData struct { + // version contains the version + version uint32 + + // dacTableAddr contains the ELF symbol 'g_dacTable' value + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/debug/ee/dactable.cpp#L80-L81 + dacTableAddr libpf.SymbolValue + + // method to walk range sections + walkRangeSectionsMethod func(i *dotnetInstance, ebpf interpreter.EbpfHandler, + pid util.PID) error + + vmStructs struct { + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/debug/ee/dactable.cpp#L81 + DacTable struct { + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/inc/dacvars.h#L78 + ExecutionManagerCodeRangeList uint + PrecodeStubManager uint + StubLinkStubManager uint + ThunkHeapStubManager uint + DelegateInvokeStubManager uint + VirtualCallStubManagerManager uint + } + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/lockedrangelist.h#L12 + LockedRangeList struct { + SizeOf uint + } + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/codeman.h#L612 + RangeSection struct { + LowAddress uint + HighAddress uint + Next uint + Flags uint + HeapList uint + Module uint + RangeList uint + SizeOf uint + } + // https://github.com/dotnet/runtime/blob/v8.0.4/src/coreclr/vm/loaderallocator.hpp#L44 + CodeRangeMapRangeList struct { + // https://github.com/dotnet/runtime/blob/v8.0.4/src/coreclr/vm/loaderallocator.hpp#L180 + RangeListType uint + } + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/codeman.h#L466 + HeapList struct { + Next uint + StartAddress uint + EndAddress uint + MapBase uint + HdrMap uint + SizeOf uint + } + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/codeman.h#L131-L135 + // NOTE: USE_INDIRECT_CODEHEADER is defined on architectures we care about, and this + // really reflects the struct _hpRealCodeHdr. + CodeHeader struct { + DebugInfo uint + MethodDesc uint + SizeOf uint + } + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/method.hpp#L193 + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/method.hpp#L1670 + MethodDesc struct { + TokenRemainderMask uint16 + TokenRemainderBits uint + Alignment uint + + Flags3AndTokenRemainder uint + ChunkIndex uint + Flags uint + SizeOf uint + } + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/method.hpp#L2163 + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/method.hpp#L2344 + MethodDescChunk struct { + TokenRangeMask uint16 + MethodTable uint + TokenRange uint + SizeOf uint + } + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/methodtable.h#L518 + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/methodtable.h#L3548 + MethodTable struct { + LoaderModule uint + } + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/ceeload.h#L601 + Module struct { + SimpleName uint + } + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/inc/patchpointinfo.h#L176-L190 + PatchpointInfo struct { + SizeOf uint + NumberOfLocals uint + } + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/stubmgr.h#L204 + StubManager struct { + SizeOf uint + } + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/stubmgr.h#L402 + PrecodeStubManager struct { + StubPrecodeRangeList uint + FixupPrecodeRangeList uint + } + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/virtualcallstub.h#L721-L768 + VirtualCallStubManager struct { + Next uint + } + } +} + +func (d *dotnetData) String() string { + ver := d.version + return fmt.Sprintf("dotnet %d.%d.%d", (ver>>24)&0xff, (ver>>16)&0xff, ver&0xffff) +} + +func (d *dotnetData) Attach(ebpf interpreter.EbpfHandler, pid util.PID, bias libpf.Address, + rm remotememory.RemoteMemory) (interpreter.Instance, error) { + log.Debugf("Attach PID %d, bias %x", pid, bias) + + addrToMethod, err := freelru.New[libpf.Address, *dotnetMethod](interpreter.LruFunctionCacheSize, + libpf.Address.Hash32) + if err != nil { + return nil, err + } + + symbolizedLRU, err := freelru.New[symbolizedKey, libpf.Void](1024, symbolizedKey.Hash32) + if err != nil { + return nil, err + } + + procInfo := C.DotnetProcInfo{ + version: C.uint(d.version), + } + if err = ebpf.UpdateProcData(libpf.Dotnet, pid, unsafe.Pointer(&procInfo)); err != nil { + return nil, err + } + + return &dotnetInstance{ + d: d, + rm: rm, + bias: bias, + ranges: make(map[libpf.Address]dotnetRangeSection), + moduleToPEInfo: make(map[libpf.Address]*peInfo), + addrToMethod: addrToMethod, + symbolizedLRU: symbolizedLRU, + }, nil +} + +func (d *dotnetData) loadIntrospectionData() { + vms := &d.vmStructs + + // Slot numbers + vms.DacTable.ExecutionManagerCodeRangeList = 0x0 + vms.DacTable.StubLinkStubManager = 0xa + vms.DacTable.ThunkHeapStubManager = 0xb + + // Addresses + vms.LockedRangeList.SizeOf = 0x120 + vms.RangeSection.LowAddress = 0x0 + vms.RangeSection.HighAddress = 0x8 + + vms.HeapList.Next = 0x0 + vms.HeapList.StartAddress = 0x10 + vms.HeapList.EndAddress = 0x18 + vms.HeapList.MapBase = 0x20 + vms.HeapList.HdrMap = 0x28 + vms.HeapList.SizeOf = 0x30 + + vms.CodeHeader.DebugInfo = 0x0 + vms.CodeHeader.MethodDesc = 0x18 // NOTE: 0x20 if FEATURE_GDBJIT + vms.CodeHeader.SizeOf = 0x20 + + // NOTE: MethodDesc layout is quite different if _DEBUG build + vms.MethodDesc.Alignment = 0x8 + vms.MethodDesc.Flags3AndTokenRemainder = 0x0 + vms.MethodDesc.ChunkIndex = 0x2 + vms.MethodDesc.Flags = 0x6 + vms.MethodDesc.SizeOf = 0x8 + + vms.MethodDescChunk.MethodTable = 0 + vms.MethodDescChunk.TokenRange = 0x12 + vms.MethodDescChunk.SizeOf = 0x18 + + vms.MethodTable.LoaderModule = 0x18 // NOTE: 0x20 if _DEBUG build + + vms.StubManager.SizeOf = 0x10 + + // Version specific introspection data + switch d.version >> 24 { + case 6: + vms.DacTable.DelegateInvokeStubManager = 0xe + vms.DacTable.VirtualCallStubManagerManager = 0xf + vms.RangeSection.Next = 0x18 + vms.RangeSection.Flags = 0x28 + vms.RangeSection.HeapList = 0x30 + vms.RangeSection.Module = 0x30 + vms.RangeSection.SizeOf = 0x38 + vms.MethodDesc.TokenRemainderBits = 14 + vms.Module.SimpleName = 0x8 + vms.PatchpointInfo.SizeOf = 20 + vms.PatchpointInfo.NumberOfLocals = 0 + d.walkRangeSectionsMethod = (*dotnetInstance).walkRangeSectionList + case 7: + vms.DacTable.DelegateInvokeStubManager = 0xe + vms.DacTable.VirtualCallStubManagerManager = 0xf + vms.RangeSection.Next = 0x18 + vms.RangeSection.Flags = 0x28 + vms.RangeSection.HeapList = 0x30 + vms.RangeSection.Module = 0x30 + vms.RangeSection.SizeOf = 0x38 + vms.MethodDesc.TokenRemainderBits = 13 + // Module inherits from ModuleBase with quite a bit of data + // see: https://github.com/dotnet/runtime/pull/71271 + vms.Module.SimpleName = 0x100 + // PatchpointInfo was adjusted in: + // https://github.com/dotnet/runtime/pull/65196 + // https://github.com/dotnet/runtime/pull/61712 + vms.PatchpointInfo.SizeOf = 32 + vms.PatchpointInfo.NumberOfLocals = 8 + // Contains useful information in dotnet7 only + vms.DacTable.PrecodeStubManager = 0x9 + // Only present in dotnet7 + vms.PrecodeStubManager.StubPrecodeRangeList = vms.StubManager.SizeOf + vms.PrecodeStubManager.FixupPrecodeRangeList = vms.StubManager.SizeOf + + vms.LockedRangeList.SizeOf + vms.VirtualCallStubManager.Next = 0x6e8 + d.walkRangeSectionsMethod = (*dotnetInstance).walkRangeSectionList + case 8: + vms.DacTable.VirtualCallStubManagerManager = 0xe + vms.RangeSection.Flags = 0x10 + vms.RangeSection.Module = 0x20 + vms.RangeSection.HeapList = 0x28 + vms.RangeSection.RangeList = 0x30 + vms.RangeSection.SizeOf = 0x38 + vms.CodeRangeMapRangeList.RangeListType = 0x120 + vms.MethodDesc.TokenRemainderBits = 12 + vms.Module.SimpleName = 0x108 + vms.PatchpointInfo.SizeOf = 32 + vms.PatchpointInfo.NumberOfLocals = 8 + vms.VirtualCallStubManager.Next = 0x268 + d.walkRangeSectionsMethod = (*dotnetInstance).walkRangeSectionMap + } + + // Calculated masks + vms.MethodDesc.TokenRemainderMask = (1 << vms.MethodDesc.TokenRemainderBits) - 1 + vms.MethodDescChunk.TokenRangeMask = (1 << (24 - vms.MethodDesc.TokenRemainderBits)) - 1 +} diff --git a/interpreter/dotnet/dotnet.go b/interpreter/dotnet/dotnet.go new file mode 100644 index 00000000..d28dd659 --- /dev/null +++ b/interpreter/dotnet/dotnet.go @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package dotnet + +// Microsoft .Net Unwinder support code + +// The Microsoft dotnet is formally specified in ECMA-335. For the main references see: +// nolint:lll +// sources https://github.com/dotnet/runtime/ +// ECMA-335 https://www.ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf +// R2RFMT https://github.com/dotnet/runtime/blob/v8.0.0/docs/design/coreclr/botr/readytorun-format.md + +// The dotnet runtime sources uses specific termionology. And there is also documentation +// about the internals. Find below useful resources to get started with the runtime: +// https://github.com/dotnet/runtime/blob/v8.0.0/docs/project/glossary.md +// https://github.com/dotnet/runtime/blob/v8.0.0/docs/design/coreclr/botr/README.md +// https://github.com/dotnet/runtime/blob/v8.0.0/docs/design/coreclr/botr/clr-abi.md +// https://github.com/dotnet/runtime/tree/v8.0.0/docs/design/specs + +// To understand the ECMA specification better, the following blog series was useful: +// https://www.red-gate.com/simple-talk/blogs/anatomy-of-a-net-assembly-pe-headers/ +// https://www.red-gate.com/simple-talk/blogs/anatomy-of-a-net-assembly-clr-metadata-1/ +// https://www.red-gate.com/simple-talk/blogs/anatomy-of-a-net-assembly-clr-metadata-2/ +// https://www.red-gate.com/simple-talk/blogs/anatomy-of-a-net-assembly-clr-metadata-3/ +// https://www.red-gate.com/simple-talk/blogs/anatomy-of-a-net-assembly-methods/ + +// On Windows x64, the standard Windows error handling is used. Originally, this was also +// used on Linux. The complicated details are at: +// https://docs.microsoft.com/en-us/cpp/build/exception-handling-x64?view=msvc-160 +// Fortunately, since dotnet5 this was adjusted that frame pointer frame chains are always +// preserved on Linux. See: https://github.com/dotnet/runtime/issues/4651 + +// Dotnet core itself implements stack unwinding of remote dotnet process by something +// called "Data Access Component" or DAC. It is build of the dotnet vm in special mode +// (macro DACCESS_COMPILE is defined) where it supports directly reading the classes/structures +// from remote process. The DAC DSO needs to always match the dotnet VM of the target process. +// So e.g. on Windows it can automatically detect the version on attach, and even download +// the DAC from Microsoft. Though, typically on every dotnet framework install the DAC is +// also installed along with the dotnet VM. +// Due to this approach, there is no real introspection data available. There has been some +// talk to have an "Universal DAC" where one .DLL could support any target VM. If this gets +// implemented, the dotnet VM will likely start shipping introspection data we can use also. + +// Our strategy currently is as follows: +// 1. inspect the dotnet to find the mmapped PE DLL executable code and the JIT areas along +// with its metadata maps. insert this into ebpf pid_page mappings. +// 2. just use standard frame pointer unwinding in ebpf, and also locate the JIT function +// code header to get access to debug data and method descriptors +// 3. in the host agent, the debug data and method descriptors are resolved and mappped to +// PE (.dll) FileID, Method index, and the IL code offset (JIT code), or the PE FileID +// and Relative Virtual Address (RVA) (Ready to Run code) +// 4. symbolizer can then map the above data to source code file and line +// 5. we explicitly do not support debug features such as Edit-and-Continue, or other +// external manipulation to the files (e.g. via ICorProfile API) + +// The dotnet runtime has had various features come and go, and this explains some of them. +// NGEN Native Image Generator. Conceptually this was intended to be run on the server +// executing the code to pre-compile everything (not as part of build, but as part +// of deployment). It is now removed from the code base since dotnet7. +// R2R Ready to Run. This feature embeds precompiled native code along with the IL +// to the PE .dll files during build. It is intended to reduce startup overhead. +// R2R compiled methods often will still be replaced by JIT optimized methods. +// NativeAOT Native AOT will build statically linked native executable. It includes all +// dotnet code, coreclr and the runtime. Everything is statically compiled and +// there is no JIT nor IL code included in the executable. Also dotnet framework +// is not needed to run this binary. There are various limitations on what kind +// of code is supported. Currently not supported by this interpreter code. + +// The dotnet runtime has numerous compile-time FEATURE_* macros to enable or disable +// some specific functionality. Fortunately, practically all of them are fixed in the build +// based on CPU architecture, OS and dotnet version. This code assumes official build with +// the default build-time featureset, but comments are added where such assumptions are made. + +// We currently make the following build feature assumptions: +// _DEBUG Is assumed off. High overhead. Not enabled on production builds. +// FEATURE_GDBJIT Is assumed off. It has high overhead. And is enabled on some +// specific custom builds only. +// FEATURE_PREJIT Is assumed off. It was specific to NGEN support, and was not +// enabled on builds we support. +// FEATURE_ON_STACK_REPLACEMENT Assume on. Enabled always on x64 and arm64. + +// Additional references: +// https://mattwarren.org/2019/01/21/Stackwalking-in-the-.NET-Runtime/ +// https://github.com/dotnet/diagnostics/tree/v8.0.505301/documentation +// https://github.com/dotnet/runtime/blob/v8.0.0/docs/design/specs/PortablePdb-Metadata.md +// https://github.com/dotnet/runtime/blob/v8.0.0/docs/design/coreclr/botr/readytorun-format.md + +// Known issues (due to dotnet runtime limitations): +// - Large methods are not handled correctly (and fail to symbolize) +// see: https://github.com/dotnet/runtime/issues/93550 +// - Inlining information is not available in default configuration +// see: https://github.com/dotnet/runtime/issues/96473 +// - Line numbers for Release built modules are inaccurate +// see: https://github.com/dotnet/runtime/issues/96473#issuecomment-1890383639 + +// TODO: +// - more coredump testcases +// - support On-Stack-Replacement (OSR): if OSR happened, currently a duplicate frame is shown +// - support for loaded IL code without backing PE DLL: Reflection.Emit, Assembly.Load(byte[]) + +import ( + "fmt" + "regexp" + "strconv" + + log "github.com/sirupsen/logrus" + + "github.com/elastic/otel-profiling-agent/interpreter" + "github.com/elastic/otel-profiling-agent/libpf" +) + +const ( + // dumpDebugInfo enables extra analysis for debug info. + // Useful for development and bug finding. But the amount of logs + // is so much that this is to be enabled on developer build if needed. + dumpDebugInfo = false + + // maxBoundsSize is the maximum size of boundary debug info (for a method) + // that we accept as valid. This is to prevent OOM situation. + maxBoundsSize = 16 * 1024 +) + +var ( + // regex for the core language runtime + dotnetRegex = regexp.MustCompile(`/(\d+)\.(\d+).(\d+)/libcoreclr.so$`) + + // The FileID used for Dotnet stub frames. Same FileID as in other interpeters. + stubsFileID = libpf.NewFileID(0x578b, 0x1d) + + // compiler check to make sure the needed interfaces are satisfied + _ interpreter.Data = &dotnetData{} + _ interpreter.Instance = &dotnetInstance{} +) + +// dotnetVer encodes the x.y.z version to single uint32 +func dotnetVer(x, y, z uint32) uint32 { + return (x << 24) + (y << 16) + z +} + +func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { + // The dotnet DSOs are in a directory with the version such as: + // /usr/lib/dotnet/shared/Microsoft.NETCore.App/6.0.25/libcoreclr.so + // It is possible to find the version also from the .so itself, but + // it requires loading the RODATA and doing a string search. + matches := dotnetRegex.FindStringSubmatch(info.FileName()) + if matches == nil { + return nil, nil + } + major, _ := strconv.Atoi(matches[1]) + minor, _ := strconv.Atoi(matches[2]) + release, _ := strconv.Atoi(matches[3]) + version := dotnetVer(uint32(major), uint32(minor), uint32(release)) + + // dotnet8 requires additional support for RangeSectionMap and MethodDesc updates + if version < dotnetVer(6, 0, 0) || version >= dotnetVer(9, 0, 0) { + return nil, fmt.Errorf("dotnet version %d.%d.%d not supported", + major, minor, release) + } + + ef, err := info.GetELF() + if err != nil { + return nil, err + } + + addr, err := ef.LookupSymbolAddress("g_dacTable") + if err != nil { + return nil, err + } + + log.Debugf("Dotnet DAC table at %x", addr) + + d := &dotnetData{ + version: version, + dacTableAddr: addr, + } + d.loadIntrospectionData() + + return d, nil +} diff --git a/interpreter/dotnet/instance.go b/interpreter/dotnet/instance.go new file mode 100644 index 00000000..aa5e09c3 --- /dev/null +++ b/interpreter/dotnet/instance.go @@ -0,0 +1,808 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package dotnet + +import ( + "context" + "fmt" + "hash/fnv" + "path" + "slices" + "strings" + "sync/atomic" + + log "github.com/sirupsen/logrus" + + "github.com/elastic/go-freelru" + "github.com/elastic/otel-profiling-agent/host" + "github.com/elastic/otel-profiling-agent/interpreter" + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/lpm" + "github.com/elastic/otel-profiling-agent/metrics" + npsr "github.com/elastic/otel-profiling-agent/nopanicslicereader" + "github.com/elastic/otel-profiling-agent/process" + "github.com/elastic/otel-profiling-agent/remotememory" + "github.com/elastic/otel-profiling-agent/reporter" + "github.com/elastic/otel-profiling-agent/successfailurecounter" + "github.com/elastic/otel-profiling-agent/support" + "github.com/elastic/otel-profiling-agent/util" +) + +// dotnet internal constants which have not changed through the current +// git repository life time, and are unlikely to change. +const ( + // MethodDesc's method classification as defined in: + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/method.hpp#L93 + mcIL = iota + mcFCall + mcNDirect + mcEEImpl + mcArray + mcInstantiated + mcComInterop + mcDynamic + mdcClassificationMask = 7 + + // enum RangeSectionFlags + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/codeman.h#L628 + rangeSectionCodeHeap = 2 + // https://github.com/dotnet/runtime/blob/v8.0.4/src/coreclr/vm/codeman.h#L664 + rangeSectionRangelist = 4 +) + +var methodClassficationName = []string{ + mcIL: "method", + mcFCall: "fcall", + mcNDirect: "ndirect", + mcEEImpl: "eeimpl", + mcArray: "array", + mcInstantiated: "instantiated", + mcComInterop: "cominterop", + mcDynamic: "dynamic", +} + +// non-JIT code / stub types +const ( + // according to dotnet8 enum StubCodeBlockKind + // https://github.com/dotnet/runtime/blob/v8.0.4/src/coreclr/vm/codeman.h#L97 + codeStubUnknown = iota + codeStubJump + codeStubPrecode + codeStubDynamicHelper + codeStubStubPrecode + codeStubFixupPrecode + codeStubVirtualCallDispatch + codeStubVirtualCallResolve + codeStubVirtualCallLookup + codeStubVirtualCallVtable + + // additional entries from dotnet6 and dotnet7 + codeStubLink + codeStubThunkHeap + codeStubDelegateInvoke + codeStubVirtualCallCacheEntry + + // synthetic entries + codeDynamic + codeReadyToRun + + // keep these in sync with the dotnet_tracer.c + codeJIT = 0x1f + codeFlagLeaf = 0x80 +) + +var codeName = []string{ + codeStubJump: "jump", + codeStubPrecode: "precode", + codeStubDynamicHelper: "dynamic helper", + codeStubStubPrecode: "stub precode", + codeStubFixupPrecode: "fixup precode", + codeStubVirtualCallDispatch: "VC/dispatch", + codeStubVirtualCallResolve: "VC/resolve", + codeStubVirtualCallLookup: "VC/lookup", + codeStubVirtualCallVtable: "VC/vtable", + + codeStubLink: "link", + codeStubThunkHeap: "thunk", + codeStubDelegateInvoke: "delegate", + codeStubVirtualCallCacheEntry: "VC/cache", + codeDynamic: "dynamic", +} + +// dotnetMapping reflects mapping of PE file to process. +type dotnetMapping struct { + start, end uint64 + info *peInfo +} + +type dotnetRangeSection struct { + prefixes []lpm.Prefix +} + +type symbolizedKey struct { + fileID libpf.FileID + lineID libpf.AddressOrLineno +} + +func (key symbolizedKey) Hash32() uint32 { + return key.fileID.Hash32() + uint32(key.lineID) +} + +type dotnetInstance struct { + interpreter.InstanceStubs + + // Symbolization metrics + successCount atomic.Uint64 + failCount atomic.Uint64 + + // d is the interpreter data from dotnet (shared between processes) + d *dotnetData + // rm is used to access the remote process memory + rm remotememory.RemoteMemory + // bias is the ELF DSO load bias + bias libpf.Address + + codeTypeMethodIDs [codeReadyToRun]libpf.AddressOrLineno + + // Internal class instance pointers + codeRangeListPtr libpf.Address + precodeStubManagerPtr libpf.Address + stubLinkStubManagerPtr libpf.Address + thunkHeapStubManagerPtr libpf.Address + delegateInvokeStubManagerPtr libpf.Address + virtualCallStubManagerManagerPtr libpf.Address + + // mappings contains the PE mappings to process memory space. Multiple individual + // consecutive process.Mappings may be merged to one mapping per PE file. + mappings []dotnetMapping + + ranges map[libpf.Address]dotnetRangeSection + + rangeSectionSeen map[libpf.Address]libpf.Void + + // moduleToPEInfo maps Module* to it's peInfo. Since a dotnet instance will have + // limited number of PE files mapped in, this is a map instead of a LRU. + moduleToPEInfo map[libpf.Address]*peInfo + + addrToMethod *freelru.LRU[libpf.Address, *dotnetMethod] + + symbolizedLRU *freelru.LRU[symbolizedKey, libpf.Void] +} + +// calculateAndSymbolizeStubID calculates a stub LineID, and symbolizes it if needed +func (i *dotnetInstance) calculateAndSymbolizeStubID(symbolReporter reporter.SymbolReporter, + codeType uint) { + if i.codeTypeMethodIDs[codeType] != 0 { + return + } + name := "[stub: " + codeName[codeType] + "]" + h := fnv.New128a() + _, _ = h.Write([]byte(name)) + nameHash := h.Sum(nil) + lineID := libpf.AddressOrLineno(npsr.Uint64(nameHash, 0)) + i.codeTypeMethodIDs[codeType] = lineID + symbolReporter.FrameMetadata(stubsFileID, lineID, 0, 0, name, "") +} + +// addRange inserts a known memory mapping along with the needed data of it to ebpf maps +func (i *dotnetInstance) addRange(ebpf interpreter.EbpfHandler, pid util.PID, + lowAddress, highAddress, mapBase libpf.Address, stubTypeOrHdrMap uint64) { + // Inform the unwinder about this range + prefixes, err := lpm.CalculatePrefixList(uint64(lowAddress), uint64(highAddress)) + if err != nil { + log.Debugf("Failed to calculate lpm: %v", err) + return + } + + // Known stub types that have no stack frame + switch stubTypeOrHdrMap { + case codeStubPrecode, codeStubFixupPrecode, codeStubLink, codeStubThunkHeap, + codeStubDelegateInvoke, codeStubVirtualCallVtable: + stubTypeOrHdrMap |= codeFlagLeaf + } + + rs := dotnetRangeSection{ + prefixes: prefixes, + } + i.ranges[lowAddress] = rs + + for _, prefix := range rs.prefixes { + err := ebpf.UpdatePidInterpreterMapping(pid, prefix, support.ProgUnwindDotnet, + host.FileID(stubTypeOrHdrMap), uint64(mapBase)) + if err != nil { + log.Debugf("Failed to update interpreter mapping: %v", err) + } + } +} + +// walkRangeList processes stub ranges from a RangeList +func (i *dotnetInstance) walkRangeList(ebpf interpreter.EbpfHandler, pid util.PID, + headPtr libpf.Address, codeType uint) { + // This hardcodes the layout of RangeList, Range and RangeListBlock from + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/inc/utilcode.h#L3556-L3579 + const numRangesInBlock = 10 + const rangeSize = 3 * 8 + blockSize := uint(numRangesInBlock*rangeSize + 8) + block := make([]byte, blockSize) + + flagLeaf := uint(0) + stubName := codeName[codeType] + log.Debugf("Found %s stub range list head at %x", stubName, headPtr) + blockNum := 0 + for blockPtr := headPtr + 0x8; blockPtr != 0; blockNum++ { + if err := i.rm.Read(blockPtr, block); err != nil { + log.Debugf("Failed to read %s stub range block %d: %v", + stubName, blockNum, err) + return + } + for index := uint(0); index < numRangesInBlock; index++ { + startAddr := npsr.Ptr(block, index*rangeSize) + endAddr := npsr.Ptr(block, index*rangeSize+8) + id := npsr.Ptr(block, index*rangeSize+16) + if startAddr == 0 || endAddr == 0 || id == 0 { + return + } + if _, ok := i.ranges[startAddr]; ok { + continue + } + log.Debugf("pid %d: %s: %d/%d: rangeList %x-%x id %x", + pid, stubName, blockNum, index, startAddr, endAddr, id) + i.addRange(ebpf, pid, startAddr, endAddr, startAddr, uint64(codeType|flagLeaf)) + } + blockPtr = npsr.Ptr(block, numRangesInBlock*rangeSize) + } +} + +// addRangeSection processes a RangeSection structure and calls addRange as needed +func (i *dotnetInstance) addRangeSection(ebpf interpreter.EbpfHandler, pid util.PID, + rangeSection []byte) error { + // Extract interesting fields + vms := &i.d.vmStructs + lowAddress := npsr.Ptr(rangeSection, vms.RangeSection.LowAddress) + highAddress := npsr.Ptr(rangeSection, vms.RangeSection.HighAddress) + flags := npsr.Uint32(rangeSection, vms.RangeSection.Flags) + if _, ok := i.ranges[lowAddress]; ok { + return nil + } + + // Check for stub RangeList (dotnet8+) + if vms.RangeSection.RangeList != 0 && flags&rangeSectionRangelist != 0 { + rangeListPtr := npsr.Ptr(rangeSection, vms.RangeSection.RangeList) + stubKind := i.rm.Uint32(rangeListPtr + + libpf.Address(vms.CodeRangeMapRangeList.RangeListType)) + log.Debugf("%x-%x flags:%x rangeListPtr %#x, type %d", + lowAddress, highAddress, flags, + rangeListPtr, stubKind) + i.addRange(ebpf, pid, lowAddress, highAddress, lowAddress, uint64(stubKind)) + } + + // Determine and parse the heapListOrZapModule content + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/codeman.h#L640-L645 + if flags&rangeSectionCodeHeap != 0 { + // heapListOrZapModule points to a heap list + heapList := make([]byte, vms.HeapList.SizeOf) + heapListPtr := npsr.Ptr(rangeSection, vms.RangeSection.HeapList) + + if err := i.rm.Read(heapListPtr, heapList); err != nil { + log.Debugf("Failed to read heapList at %#x", heapListPtr) + return err + } + mapBase := npsr.Ptr(heapList, vms.HeapList.MapBase) + hdrMap := npsr.Ptr(heapList, vms.HeapList.HdrMap) + heapListPtr = npsr.Ptr(heapList, vms.HeapList.Next) + heapStart := npsr.Ptr(heapList, vms.HeapList.StartAddress) + heapEnd := npsr.Ptr(heapList, vms.HeapList.EndAddress) + + log.Debugf("%x-%x flags:%x heap: next:%x %x-%x mapBase: %x headerMap: %x", + lowAddress, highAddress, flags, + heapListPtr, heapStart, heapEnd, mapBase, hdrMap) + + i.addRange(ebpf, pid, lowAddress, highAddress, mapBase, uint64(hdrMap)) + } else { + // heapListOrZapModule points to a Module. + modulePtr := npsr.Ptr(rangeSection, vms.RangeSection.Module) + // Find the memory mapping area for this module, and establish mapping from + // Module* to the PE. This precaches the mapping for R2R modules and avoids + // some remote memory reads. + info, err := i.getPEInfoByAddress(uint64(lowAddress)) + if err != nil { + return nil + } + i.moduleToPEInfo[modulePtr] = info + log.Debugf("%x-%x flags:%x module: %x -> %s", + lowAddress, highAddress, flags, + modulePtr, info.simpleName) + i.addRange(ebpf, pid, lowAddress, highAddress, lowAddress, codeReadyToRun) + } + + return nil +} + +// walkRangeSectionList adds all RangeSections in a list (dotnet6 and dotnet7) +func (i *dotnetInstance) walkRangeSectionList(ebpf interpreter.EbpfHandler, pid util.PID) error { + vms := &i.d.vmStructs + rangeSection := make([]byte, vms.RangeSection.SizeOf) + // walk the RangeSection list + ptr := i.rm.Ptr(i.codeRangeListPtr) + for ptr != 0 { + if err := i.rm.Read(ptr, rangeSection); err != nil { + return err + } + if err := i.addRangeSection(ebpf, pid, rangeSection); err != nil { + return err + } + ptr = npsr.Ptr(rangeSection, vms.RangeSection.Next) + } + return nil +} + +// walkRangeSectionMapFragments walks a RangeSectionMap::RangeSectionFragment list and processes +// the RangeSections from it. +func (i *dotnetInstance) walkRangeSectionMapFragments(ebpf interpreter.EbpfHandler, pid util.PID, + fragmentPtr libpf.Address) error { + // https://github.com/dotnet/runtime/blob/v8.0.4/src/coreclr/vm/codeman.h#L974 + vms := &i.d.vmStructs + fragment := make([]byte, 4*8) + rangeSection := make([]byte, vms.RangeSection.SizeOf) + for fragmentPtr != 0 { + if err := i.rm.Read(fragmentPtr, fragment); err != nil { + return fmt.Errorf("failed to read fragment: %v", err) + } + // Remove collectible bit + fragmentPtr = npsr.Ptr(fragment, 0) &^ 1 + rangeSectionPtr := npsr.Ptr(fragment, 24) + if _, ok := i.rangeSectionSeen[rangeSectionPtr]; ok { + continue + } + i.rangeSectionSeen[rangeSectionPtr] = libpf.Void{} + + if err := i.rm.Read(rangeSectionPtr, rangeSection); err != nil { + return fmt.Errorf("failed to read range section: %v", err) + } + if err := i.addRangeSection(ebpf, pid, rangeSection); err != nil { + return err + } + } + return nil +} + +// walkRangeSectionMapLevel walks recursively a level index of a RangeSectionMap. +func (i *dotnetInstance) walkRangeSectionMapLevel(ebpf interpreter.EbpfHandler, pid util.PID, + levelMapPtr libpf.Address, level uint) error { + // https://github.com/dotnet/runtime/blob/v8.0.4/src/coreclr/vm/codeman.h#L999-L1002 + const maxLevel = 5 + const entriesInLevel = 256 + levelPointers := make([]byte, entriesInLevel*8) + + if err := i.rm.Read(levelMapPtr, levelPointers); err != nil { + return fmt.Errorf("failed to read section level: %v", err) + } + for index := uint(0); index < uint(len(levelPointers)); index += 8 { + // mask out collectible bit + ptr := npsr.Ptr(levelPointers, index) &^ 1 + if ptr == 0 { + continue + } + if level < maxLevel { + if err := i.walkRangeSectionMapLevel(ebpf, pid, ptr, level+1); err != nil { + return err + } + } else { + if err := i.walkRangeSectionMapFragments(ebpf, pid, ptr); err != nil { + return err + } + } + } + return nil +} + +// walkRangeSectionMap processes a dotnet8 RangeSectionMap to enumerate all RangeSections +func (i *dotnetInstance) walkRangeSectionMap(ebpf interpreter.EbpfHandler, pid util.PID) error { + i.rangeSectionSeen = make(map[libpf.Address]libpf.Void) + err := i.walkRangeSectionMapLevel(ebpf, pid, i.codeRangeListPtr, 1) + i.rangeSectionSeen = nil + return err +} + +func (i *dotnetInstance) getPEInfoByAddress(addressInModule uint64) (*peInfo, error) { + idx, ok := slices.BinarySearchFunc(i.mappings, addressInModule, + func(m dotnetMapping, addr uint64) int { + if addr < m.start { + return 1 + } + if addr >= m.end { + return -1 + } + return 0 + }) + if !ok { + return nil, fmt.Errorf("failed to find mapping for address %x", addressInModule) + } + + mapping := &i.mappings[idx] + return mapping.info, nil +} + +func (i *dotnetInstance) getPEInfoByModulePtr(modulePtr libpf.Address) (*peInfo, error) { + if info, ok := i.moduleToPEInfo[modulePtr]; ok { + return info, nil + } + + // If the Module does not have R2R executable code and we have not seen it yet, + // we fallback to finding the PE info. The strategy is to read the SimpleName + // member which is a pointer inside the memory mapped location of the PE .dll. + // Read that and locate the memory mapping to get the PE info. + vms := &i.d.vmStructs + simpleNamePtr := i.rm.Ptr(modulePtr + libpf.Address(vms.Module.SimpleName)) + if simpleNamePtr == 0 { + return nil, fmt.Errorf("module at %x, does not have name", modulePtr) + } + + info, err := i.getPEInfoByAddress(uint64(simpleNamePtr)) + if err != nil { + return nil, err + } + i.moduleToPEInfo[modulePtr] = info + return info, nil +} + +func (i *dotnetInstance) readMethod(methodDescPtr libpf.Address, + debugInfoPtr libpf.Address) (*dotnetMethod, error) { + vms := &i.d.vmStructs + + // Extract MethodDesc data + methodDesc := make([]byte, vms.MethodDesc.SizeOf) + if err := i.rm.Read(methodDescPtr, methodDesc); err != nil { + return nil, err + } + + tokenRemainder := npsr.Uint16(methodDesc, vms.MethodDesc.Flags3AndTokenRemainder) + tokenRemainder &= vms.MethodDesc.TokenRemainderMask + chunkIndex := npsr.Uint8(methodDesc, vms.MethodDesc.ChunkIndex) + classification := npsr.Uint16(methodDesc, vms.MethodDesc.Flags) & mdcClassificationMask + + // Calculate the offset to the owning MethodDescChunk structure + // https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/method.hpp#L2321-L2328 + methodDescChunkPtr := methodDescPtr - + libpf.Address(chunkIndex)*libpf.Address(vms.MethodDesc.Alignment) - + libpf.Address(vms.MethodDescChunk.SizeOf) + + log.Debugf("method @%x: classification '%v', tokenRemainder %x, chunkIndex %x -> chunkPtr %x", + methodClassficationName[classification], methodDescPtr, + tokenRemainder, chunkIndex, methodDescChunkPtr) + + // Read the MethodDescChunk + methodDescChunk := make([]byte, vms.MethodDescChunk.SizeOf) + if err := i.rm.Read(methodDescChunkPtr, methodDescChunk); err != nil { + return nil, err + } + methodTablePtr := npsr.Ptr(methodDescChunk, vms.MethodDescChunk.MethodTable) + tokenRange := npsr.Uint16(methodDescChunk, vms.MethodDescChunk.TokenRange) + tokenRange &= vms.MethodDescChunk.TokenRangeMask + + // Merge the MethodDesc and MethodDescChunk bits of Token value + // https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/method.hpp#L76-L80 + index := uint32(tokenRange)< index %d", + methodDescChunkPtr, methodTablePtr, tokenRange, tokenRemainder, index) + + // Extract the loader module from the associated MethodTable + // https://github.com/dotnet/runtime/blob/release/8.0/src/coreclr/vm/methodtable.cpp#L369-L383 + // FIXME: The dotnet runtime handles generic and array method differently. + // Investigate if this needs adjustments to create correct method indexes. + loaderModulePtr := i.rm.Ptr(methodTablePtr + libpf.Address(vms.MethodTable.LoaderModule)) + module, err := i.getPEInfoByModulePtr(loaderModulePtr) + if err != nil { + return nil, err + } + + method := &dotnetMethod{ + classification: classification, + index: index, + module: module, + } + if debugInfoPtr != 0 { + if err := method.readDebugInfo(newCachingReader(i.rm, int64(debugInfoPtr), + 1024), i.d); err != nil { + log.Debugf("debug info reading failed: %v", err) + } + } + return method, nil +} + +func (i *dotnetInstance) getMethod(codeHeaderPtr libpf.Address) (*dotnetMethod, error) { + if method, ok := i.addrToMethod.Get(codeHeaderPtr); ok { + return method, nil + } + + vms := &i.d.vmStructs + codeHeader := make([]byte, vms.CodeHeader.SizeOf) + if err := i.rm.Read(codeHeaderPtr, codeHeader); err != nil { + return nil, err + } + + debugInfoPtr := npsr.Ptr(codeHeader, vms.CodeHeader.DebugInfo) + methodDescPtr := npsr.Ptr(codeHeader, vms.CodeHeader.MethodDesc) + method, err := i.readMethod(methodDescPtr, debugInfoPtr) + if err != nil { + return nil, err + } + + i.addrToMethod.Add(codeHeaderPtr, method) + return method, nil +} + +func (i *dotnetInstance) Detach(ebpf interpreter.EbpfHandler, pid util.PID) error { + return ebpf.DeleteProcData(libpf.Dotnet, pid) +} + +func (i *dotnetInstance) getDacSlot(slot uint) libpf.Address { + // dotnet6 records everything as RVAs and starting dotnet7 they are pointers + // see: https://github.com/dotnet/runtime/pull/68065 + dacTable := i.bias + libpf.Address(i.d.dacTableAddr) + if i.d.version>>24 == 6 { + slotPtr := dacTable + libpf.Address(slot*4) + if rva := i.rm.Uint32(slotPtr); rva != 0 { + return i.bias + libpf.Address(rva) + } + } else { + slotPtr := dacTable + libpf.Address(slot*8) + if ptr := i.rm.Ptr(slotPtr); ptr != 0 { + return ptr + } + } + return 0 +} + +func (i *dotnetInstance) getDacSlotPtr(slot uint) libpf.Address { + ptr := i.getDacSlot(slot) + if ptr == 0 { + return 0 + } + return i.rm.Ptr(ptr) +} + +func (i *dotnetInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler, + symbolReporter reporter.SymbolReporter, pr process.Process, + mappings []process.Mapping) error { + // find pointer to codeRangeList if needed + vms := &i.d.vmStructs + if i.codeRangeListPtr == 0 { + i.codeRangeListPtr = i.getDacSlot(vms.DacTable.ExecutionManagerCodeRangeList) + if i.codeRangeListPtr == 0 { + // This is normal state if we attached to the process before + // the dotnet runtime has initialized itself fully. + log.Debugf("Dotnet DAC table is not yet initialized at %x", i.d.dacTableAddr) + return nil + } + log.Debugf("Found code range list head at %x", i.codeRangeListPtr) + } + if i.precodeStubManagerPtr == 0 && vms.DacTable.PrecodeStubManager != 0 { + i.precodeStubManagerPtr = i.getDacSlotPtr(vms.DacTable.PrecodeStubManager) + } + if i.stubLinkStubManagerPtr == 0 && vms.DacTable.StubLinkStubManager != 0 { + i.stubLinkStubManagerPtr = i.getDacSlotPtr(vms.DacTable.StubLinkStubManager) + } + if i.thunkHeapStubManagerPtr == 0 && vms.DacTable.ThunkHeapStubManager != 0 { + i.thunkHeapStubManagerPtr = i.getDacSlotPtr(vms.DacTable.ThunkHeapStubManager) + } + if i.delegateInvokeStubManagerPtr == 0 && vms.DacTable.DelegateInvokeStubManager != 0 { + i.delegateInvokeStubManagerPtr = i.getDacSlotPtr(vms.DacTable.DelegateInvokeStubManager) + } + if i.virtualCallStubManagerManagerPtr == 0 && vms.DacTable.VirtualCallStubManagerManager != 0 { + i.virtualCallStubManagerManagerPtr = i.getDacSlotPtr( + vms.DacTable.VirtualCallStubManagerManager) + } + + // Collect PE files + dotnetMappings := []dotnetMapping{} + var prevKey util.OnDiskFileIdentifier + var prevMaxVA uint64 + for idx := range mappings { + m := &mappings[idx] + // Some dotnet .dll files do not get executable mappings at all + if m.IsAnonymous() { + continue + } + if !strings.HasSuffix(m.Path, ".dll") { + continue + } + + // Does this extend the previous mapping + if prevKey == m.GetOnDiskFileIdentifier() && m.Vaddr < prevMaxVA { + dotnetMappings[len(dotnetMappings)-1].end = m.Vaddr + m.Length + continue + } + prevKey = m.GetOnDiskFileIdentifier() + + // Inspect the PE + info := globalPeCache.Get(pr, m) + if info.err != nil { + return info.err + } + + log.Debugf("%x = %v -> %v guid %v", + info.fileID, m.Path, + info.simpleName, info.guid) + + if !info.reported { + symbolReporter.ExecutableMetadata(context.TODO(), + info.fileID, path.Base(m.Path), info.guid) + info.reported = true + } + + dotnetMappings = append(dotnetMappings, dotnetMapping{ + start: m.Vaddr, + end: m.Vaddr + m.Length, + info: info, + }) + prevMaxVA = m.Vaddr + uint64(info.sizeOfImage) + } + + // mappings are in sorted order + i.mappings = dotnetMappings + + for _, m := range dotnetMappings { + log.Debugf("mapped %x-%x %s", m.start, m.end, m.info.simpleName) + } + + if err := i.d.walkRangeSectionsMethod(i, ebpf, pr.PID()); err != nil { + log.Infof("Failed to walk code ranges: %v", err) + } + if i.precodeStubManagerPtr != 0 { + if vms.PrecodeStubManager.StubPrecodeRangeList != 0 { + rangeListPtr := i.precodeStubManagerPtr + + libpf.Address(vms.PrecodeStubManager.StubPrecodeRangeList) + i.walkRangeList(ebpf, pr.PID(), rangeListPtr, codeStubPrecode) + } + if vms.PrecodeStubManager.FixupPrecodeRangeList != 0 { + rangeListPtr := i.precodeStubManagerPtr + + libpf.Address(vms.PrecodeStubManager.FixupPrecodeRangeList) + i.walkRangeList(ebpf, pr.PID(), rangeListPtr, codeStubFixupPrecode) + } + } + if i.stubLinkStubManagerPtr != 0 { + rangeListPtr := i.stubLinkStubManagerPtr + libpf.Address(vms.StubManager.SizeOf) + i.walkRangeList(ebpf, pr.PID(), rangeListPtr, codeStubLink) + } + if i.thunkHeapStubManagerPtr != 0 { + rangeListPtr := i.thunkHeapStubManagerPtr + libpf.Address(vms.StubManager.SizeOf) + i.walkRangeList(ebpf, pr.PID(), rangeListPtr, codeStubThunkHeap) + } + if i.delegateInvokeStubManagerPtr != 0 { + rangeListPtr := i.delegateInvokeStubManagerPtr + libpf.Address(vms.StubManager.SizeOf) + i.walkRangeList(ebpf, pr.PID(), rangeListPtr, codeStubDelegateInvoke) + } + if i.virtualCallStubManagerManagerPtr != 0 && vms.VirtualCallStubManager.Next != 0 { + managerPtr := i.virtualCallStubManagerManagerPtr + libpf.Address(vms.StubManager.SizeOf) + managerPtr = i.rm.Ptr(managerPtr) + for num := 0; managerPtr != 0 && num < 10; num++ { + rangeListPtr := managerPtr + libpf.Address(vms.StubManager.SizeOf) + + // This hard codes the virtual call range list member order at: + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/virtualcallstub.h#L437 + // https://github.com/dotnet/runtime/blob/v8.0.4/src/coreclr/vm/virtualcallstub.h#L338 + switch i.d.version >> 24 { + case 7: + i.walkRangeList(ebpf, pr.PID(), rangeListPtr, codeStubVirtualCallLookup) + + rangeListPtr += libpf.Address(vms.LockedRangeList.SizeOf) + i.walkRangeList(ebpf, pr.PID(), rangeListPtr, codeStubVirtualCallResolve) + + rangeListPtr += libpf.Address(vms.LockedRangeList.SizeOf) + i.walkRangeList(ebpf, pr.PID(), rangeListPtr, codeStubVirtualCallDispatch) + + rangeListPtr += libpf.Address(vms.LockedRangeList.SizeOf) + i.walkRangeList(ebpf, pr.PID(), rangeListPtr, codeStubVirtualCallCacheEntry) + + rangeListPtr += libpf.Address(vms.LockedRangeList.SizeOf) + i.walkRangeList(ebpf, pr.PID(), rangeListPtr, codeStubVirtualCallVtable) + case 8: + i.walkRangeList(ebpf, pr.PID(), rangeListPtr, codeStubVirtualCallCacheEntry) + } + + managerPtr = i.rm.Ptr(managerPtr + libpf.Address(vms.VirtualCallStubManager.Next)) + } + } + return nil +} + +func (i *dotnetInstance) GetAndResetMetrics() ([]metrics.Metric, error) { + addrToMethodStats := i.addrToMethod.ResetMetrics() + + return []metrics.Metric{ + { + ID: metrics.IDDotnetSymbolizationSuccesses, + Value: metrics.MetricValue(i.successCount.Swap(0)), + }, + { + ID: metrics.IDDotnetSymbolizationFailures, + Value: metrics.MetricValue(i.failCount.Swap(0)), + }, + { + ID: metrics.IDDotnetAddrToMethodHit, + Value: metrics.MetricValue(addrToMethodStats.Hits), + }, + { + ID: metrics.IDDotnetAddrToMethodMiss, + Value: metrics.MetricValue(addrToMethodStats.Misses), + }, + }, nil +} + +func (i *dotnetInstance) Symbolize(symbolReporter reporter.SymbolReporter, + frame *host.Frame, trace *libpf.Trace) error { + if !frame.Type.IsInterpType(libpf.Dotnet) { + return interpreter.ErrMismatchInterpreterType + } + + sfCounter := successfailurecounter.New(&i.successCount, &i.failCount) + defer sfCounter.DefaultToFailure() + + codeHeaderAndType := frame.File + frameType := uint(codeHeaderAndType & 0x1f) + codeHeaderPtr := libpf.Address(codeHeaderAndType >> 5) + pcOffset := uint32(frame.Lineno) + + switch frameType { + case codeReadyToRun: + // Ready to Run (Non-JIT) frame running directly code from a PE file + module, err := i.getPEInfoByAddress(uint64(codeHeaderPtr)) + if err != nil { + return err + } + fileID := module.fileID + lineID := libpf.AddressOrLineno(pcOffset) + + if _, ok := i.symbolizedLRU.Get(symbolizedKey{fileID, lineID}); !ok { + methodName := module.resolveR2RMethodName(pcOffset) + symbolReporter.FrameMetadata(fileID, lineID, 0, 0, + methodName, module.simpleName) + i.symbolizedLRU.Add(symbolizedKey{fileID, lineID}, libpf.Void{}) + } + // The Line ID is the Relative Virtual Address (RVA) within into the PE file + // where PC is executing. On non-leaf frames it points to the return address. + // The instructionafter the CALL machine opcode. + trace.AppendFrame(libpf.DotnetFrame, fileID, lineID) + case codeJIT: + // JITted frame in anonymous mapping + method, err := i.getMethod(codeHeaderPtr) + if err != nil { + return err + } + ilOffset := method.mapPCOffsetToILOffset(pcOffset, frame.ReturnAddress) + fileID := method.module.fileID + + // The Line ID format is: + // 4 bits Set to 0xf to indicate JIT frame. + // 28 bits Method index + // 32 bits IL offset within that method. On non-leaf frames, it is + // pointing to CALL instruction if the debug info was accurate. + lineID := libpf.AddressOrLineno(0xf0000000+method.index)<<32 + + libpf.AddressOrLineno(ilOffset) + + if method.index == 0 || method.classification == mcDynamic { + i.calculateAndSymbolizeStubID(symbolReporter, codeDynamic) + trace.AppendFrame(libpf.DotnetFrame, stubsFileID, i.codeTypeMethodIDs[codeDynamic]) + } else { + if _, ok := i.symbolizedLRU.Get(symbolizedKey{fileID, lineID}); !ok { + methodName := method.module.resolveMethodName(method.index) + symbolReporter.FrameMetadata(fileID, lineID, 0, ilOffset, + methodName, method.module.simpleName) + i.symbolizedLRU.Add(symbolizedKey{fileID, lineID}, libpf.Void{}) + } + trace.AppendFrame(libpf.DotnetFrame, fileID, lineID) + } + default: + // Stub code + i.calculateAndSymbolizeStubID(symbolReporter, frameType) + trace.AppendFrame(libpf.DotnetFrame, stubsFileID, i.codeTypeMethodIDs[frameType]) + } + + sfCounter.ReportSuccess() + return nil +} diff --git a/interpreter/dotnet/method.go b/interpreter/dotnet/method.go new file mode 100644 index 00000000..f5f81cdd --- /dev/null +++ b/interpreter/dotnet/method.go @@ -0,0 +1,237 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package dotnet + +import ( + "bytes" + "encoding/binary" + "fmt" + + log "github.com/sirupsen/logrus" + + npsr "github.com/elastic/otel-profiling-agent/nopanicslicereader" +) + +type dotnetMethod struct { + // module is the PE DLL defining this method + module *peInfo + // boundInfo is the extracted boundary debug information from coreclr vm. + boundsInfo []byte + // methodIndex is the index to MethodDef metadata table defining this method. + index uint32 + // classification is the coreclr vm categorization of the method type. + classification uint16 +} + +// dotnet internal constants which have not changed through the current +// git repository life time, and are unlikely to change. +const ( + // Debug Info boundary info mapping types, as defined in: + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/inc/cordebuginfo.h#L16 + mappingTypeNoMapping = -1 + mappingTypeProlog = -2 + mappingTypeEpilog = -3 + mappingTypeMaxValue = mappingTypeEpilog + + // Debug Info Boundary info's Source Type valid mask + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/inc/cordebuginfo.h#L41 + sourceTypeCallInstruction = 0x10 + + // CLR internal debug info flags + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/debuginfostore.cpp#L458 + extraDebugInfoPathcPoint = 0x01 + extraDebugInfoRich = 0x02 +) + +func (m *dotnetMethod) mapPCOffsetToILOffset(pcOffset uint32, findCall bool) uint32 { + // FIXME: should there be a small LRU for these? + + // NOTE: The dotnet coreclr optimizing JIT (used when the module is built in Release + // configuration) does not currently generate reliably DebugInfo. Most importantly + // it is missing CALL_INSTRUCTION mapping. It seems to generate only best effort + // STACK_EMPTY mappings which gives only very coarse PC-to-IL mappings. The "wrong + // line numbers" issue affects also dotnet coreclr itself. See also: + // https://github.com/dotnet/runtime/issues/96473#issuecomment-1890383639 + // https://dotnetdocs.ir/Post/47/wrong-exception-line-number-in-stack-trace-in-release-mode + r := bytes.NewReader(m.boundsInfo) + nr := nibbleReader{ByteReader: r} + numEntries := nr.Uint32() + + log.Debugf("finding method index=%d, pcOffset=%d, callCall=%v, numEntries=%v", + m.index, pcOffset, findCall, numEntries) + + // Decode Bounds Info portion of DebugInfo + // https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/debuginfostore.cpp#L289-L310 + nativeOffset := uint32(0) + ilOffset := uint32(0) + lastCallILOffset := uint32(0) + for i := uint32(0); i < numEntries; i++ { + nativeOffset += nr.Uint32() + if findCall && nativeOffset >= pcOffset { + // If finding call site, always return lastCallILOffset. + // This will be zero if there are no CALL_INSTRUCTION boundary info. + log.Debugf(" returning %#x as last call site (next entry's native offset %d)", + lastCallILOffset, nativeOffset) + return lastCallILOffset + } else if nativeOffset > pcOffset { + log.Debugf(" returning %#x (next entry's native offset %d)", + ilOffset, nativeOffset) + return ilOffset + } + + // Ignore the special value for IL offset. The prolog and epilog values + // are often emitted for same native offset together with the IL offset. + // This allows epilog to point to the actual IL ret instruction. + encodedILOffset := int32(nr.Uint32()) + mappingTypeMaxValue + if encodedILOffset >= 0 { + ilOffset = uint32(encodedILOffset) + } + sourceFlags := nr.Uint32() + if sourceFlags&sourceTypeCallInstruction != 0 { + lastCallILOffset = ilOffset + } + + // NOTE: _DEBUG builds could have a 0xA nibble to indentify row change. + log.Debugf(" %3d, native %3d -> IL %#03x, sourceFlags %#x", + i, nativeOffset, ilOffset, sourceFlags) + } + return uint32(0) +} + +func (m *dotnetMethod) dumpBounds() { + r := bytes.NewReader(m.boundsInfo) + nr := nibbleReader{ByteReader: r} + numEntries := nr.Uint32() + + log.Debugf("dumping method index=%d, numEntries=%v", m.index, numEntries) + + // Decode Bounds Info portion of DebugInfo + // https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/debuginfostore.cpp#L289-L310 + nativeOffset := uint32(0) + for i := uint32(0); i < numEntries; i++ { + nativeOffset += nr.Uint32() + ilOffset := uint32(int32(nr.Uint32()) + mappingTypeMaxValue) + sourceFlags := nr.Uint32() + // NOTE: _DEBUG builds could have a 0xA nibble to indentify row change. + + log.Debugf(" %3d, native %3d -> IL %#03x, sourceFlags %#x", + i, nativeOffset, ilOffset, sourceFlags) + } +} + +func dumpRichDebugInfo(richInfo []byte) { + nr := nibbleReader{ByteReader: bytes.NewReader(richInfo)} + numInlineTree := nr.Uint32() + numRichOffsets := nr.Uint32() + log.Debugf("debug info: rich debug %d bytes, %d inlines, %d offsets", + len(richInfo), numInlineTree, numRichOffsets) + + // Decode Rich Debug info's Inline Tree Nodes + // https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/debuginfostore.cpp#L404-L429 + var ilOffset, child, sibling int32 + for i := uint32(0); i < numInlineTree; i++ { + ptr := nr.Ptr() + ilOffset += nr.Int32() + child += nr.Int32() + sibling += nr.Int32() + log.Debugf(" il %03d child %d sibling %x handle %x", + ilOffset, child, sibling, ptr) + } + + // Decode Rich Debug info's Offset Mappings + // https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/debuginfostore.cpp#L431-L456 + nativeOffset := uint32(0) + ilOffset = 0 + inlinee := int32(0) + for i := uint32(0); i < numRichOffsets; i++ { + nativeOffset += nr.Uint32() + inlinee += nr.Int32() + ilOffset += nr.Int32() + sourceFlags := nr.Uint32() + log.Debugf(" native %d IL %x inlinee %d flags %x", + nativeOffset, ilOffset, inlinee, sourceFlags) + } +} + +// Read and parse the dotnet coreclr DebugInfo structure +// https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/debuginfostore.cpp#L711 +func (m *dotnetMethod) readDebugInfo(r *cachingReader, d *dotnetData) error { + // The Flags byte is optional depending on build options. Namely FEATURE_ON_STACK_REPLACEMENT + // enables it always, which is always enabled for x86 and arm64. + // https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/codeman.cpp#L3786-L3804 + flags, err := r.ReadByte() + if err != nil { + return fmt.Errorf("failed to read flags: %w", err) + } + if flags&^(extraDebugInfoPathcPoint|extraDebugInfoRich) != 0 { + return fmt.Errorf("flags (%#x) not supported", flags) + } + if flags&extraDebugInfoPathcPoint != 0 { + // skip PatchpointInfo + // https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/debuginfostore.cpp#L741-L746 + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/inc/patchpointinfo.h#L29-L35 + vms := &d.vmStructs + patchpointInfo := make([]byte, vms.PatchpointInfo.SizeOf) + if _, err = r.Read(patchpointInfo); err != nil { + return fmt.Errorf("failed to read patchpoint info: %w", err) + } + numLocals := npsr.Uint32(patchpointInfo, vms.PatchpointInfo.NumberOfLocals) + r.Skip(int(numLocals * 4)) + log.Debugf("debug info: skipped patchpoint info with %d locals", numLocals) + } + if flags&extraDebugInfoRich != 0 { + // https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/debuginfostore.cpp#L748-L754 + // export COMPlus_RichDebugInfo=1 (to enable generation of this information) + lengthBytes := make([]byte, 4) + if _, err = r.Read(lengthBytes); err != nil { + return fmt.Errorf("failed to read rich debug: %w", err) + } + + length := binary.LittleEndian.Uint32(lengthBytes) + if dumpDebugInfo { + richInfo := make([]byte, length) + _, err = r.Read(richInfo) + if err != nil { + return fmt.Errorf("failed to read rich debug: %w", err) + } + dumpRichDebugInfo(richInfo) + } else { + r.Skip(int(length)) + } + // FIXME: implement support for RichDebugInfo to decode inlining info + // In dotnet7, the RichDebugInfo was added back as experimental opt-in + // feature, but mentioning the format may change. + // https://github.com/dotnet/runtime/pull/71263 + // We have open issue to make the RichDebugInfo usable for profilers: + // https://github.com/dotnet/runtime/issues/96473 + } + + // Decode the DebugInfo header + // https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/debuginfostore.cpp#L759-L765 + nr := nibbleReader{ByteReader: r} + numBytesBounds := nr.Uint32() + numBytesVars := nr.Uint32() + nr.AlignToBytes() + if err := nr.Error(); err != nil { + return fmt.Errorf("failed to read bounds header: %w", err) + } + log.Debugf("debug info: bounds size %d, vars size %d", numBytesBounds, numBytesVars) + if numBytesBounds > maxBoundsSize { + return fmt.Errorf("boundary debug info size %d is too large", numBytesBounds) + } + + // Extract the boundary information blob + m.boundsInfo = make([]byte, numBytesBounds) + if _, err := r.Read(m.boundsInfo); err != nil { + m.boundsInfo = nil + return fmt.Errorf("failed to read bounds: %w", err) + } + if dumpDebugInfo { + m.dumpBounds() + } + return nil +} diff --git a/interpreter/dotnet/nativereader.go b/interpreter/dotnet/nativereader.go new file mode 100644 index 00000000..a19c23c0 --- /dev/null +++ b/interpreter/dotnet/nativereader.go @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package dotnet + +import ( + "encoding/binary" + "fmt" + "io" +) + +const ( + nativeTableBlockSize = uint32(16) + nativeTableBlockMask = nativeTableBlockSize - 1 +) + +// Ready-to-Run Native Format Reader +// https://github.com/dotnet/runtime/blob/v8.0.0/src/coreclr/vm/nativeformatreader.h +type nativeReader struct { + io.ReaderAt +} + +// UintFixed reads a fixed size integer of 'n' bytes at position offs. +func (nr *nativeReader) UintFixed(offs int64, n int) (value uint32, newOffs int64, err error) { + var d [4]byte + _, err = nr.ReadAt(d[0:n], offs) + return binary.LittleEndian.Uint32(d[:]), offs + int64(n), err +} + +// Uint decodes one Native encoded Unsigned integer at position offs. +// https://github.com/dotnet/runtime/blob/v8.0.0/src/coreclr/vm/nativeformatreader.h#L104 +func (nr *nativeReader) Uint(offs int64) (value uint32, newOffs int64, err error) { + var data [4]byte + if _, err = nr.ReadAt(data[0:1], offs); err != nil { + return 0, offs, err + } + var more uint8 + switch { + case data[0]&0x01 == 0: + return uint32(data[0] >> 1), offs + 1, nil + case data[0]&0x02 == 0: + more = 1 + case data[0]&0x04 == 0: + more = 2 + case data[0]&0x08 == 0: + more = 3 + case data[0]&0x10 == 0: + return nr.UintFixed(offs+1, 4) + default: + return 0, offs, fmt.Errorf("invalid native uint format byte %#02x", data[0]) + } + _, err = nr.ReadAt(data[1:more+1], offs+1) + return binary.LittleEndian.Uint32(data[:]) >> (more + 1), offs + int64(more) + 1, err +} + +// decodeBlock handles one bit of native sparse array block walking. +func (nr *nativeReader) decodeBlock(offset int64, index, bitMask uint32, + cb func(uint32, int64) error) error { + if bitMask == 0 { + return cb(index, offset) + } + + val, nextOffset, err := nr.Uint(offset) + if err != nil { + return err + } + if val&1 != 0 { + // Left entry valid, recurse with bit not set. + err = nr.decodeBlock(nextOffset, index, bitMask>>1, cb) + if err != nil { + return err + } + } + if val&2 != 0 { + // Right entry valid, recurse with bit set. + err = nr.decodeBlock(offset+int64(val>>2), index|bitMask, bitMask>>1, cb) + if err != nil { + return err + } + } + if val&3 == 0 && val < 0x40 { + // Special entry: only the entry encoded in "val" entry is present. + // And a special tombstone meaning no entry in this block if the "val" + // is larger than block. + err = nr.decodeBlock(nextOffset, (index&^nativeTableBlockMask)|(val>>2), 0, cb) + if err != nil { + return err + } + } + return nil +} + +// WalkTable enumerates all entries in a sparse "Native Table". The R2RFMT does not document the +// format, there is just a TODO placeholder item. The code to lookup an item in a NativeTable +// is at: https://github.com/dotnet/runtime/blob/v8.0.0/src/coreclr/vm/nativeformatreader.h#L370 +func (nr *nativeReader) WalkTable(cb func(uint32, int64) error) error { + header, baseOffset, err := nr.Uint(0) + if err != nil { + return err + } + entrySize := int(0) + switch header & 3 { + case 0: + entrySize = 1 + case 1: + entrySize = 2 + default: + entrySize = 4 + } + numElems := header >> 2 + + indexOffset := baseOffset + for i := uint32(0); i < numElems; i += nativeTableBlockSize { + var blockOffset uint32 + blockOffset, indexOffset, err = nr.UintFixed(indexOffset, entrySize) + if err != nil { + return err + } + err = nr.decodeBlock(baseOffset+int64(blockOffset), i, nativeTableBlockSize>>1, cb) + if err != nil { + return err + } + } + return nil +} diff --git a/interpreter/dotnet/nativereader_test.go b/interpreter/dotnet/nativereader_test.go new file mode 100644 index 00000000..4b48ae25 --- /dev/null +++ b/interpreter/dotnet/nativereader_test.go @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package dotnet + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNativeReader(t *testing.T) { + testCases := []struct { + data string + expected uint32 + }{ + {"18", 12}, + {"a10f", 1000}, + } + + for _, test := range testCases { + t.Run(test.data, func(t *testing.T) { + data, err := hex.DecodeString(test.data) + require.NoError(t, err, "Hex decoding failed") + + decoder := nativeReader{ + ReaderAt: bytes.NewReader(data), + } + value, _, err := decoder.Uint(0) + require.NoError(t, err, "Error") + assert.Equal(t, test.expected, value, "Wrong native decoding") + }) + } +} diff --git a/interpreter/dotnet/nibblereader.go b/interpreter/dotnet/nibblereader.go new file mode 100644 index 00000000..d02e6295 --- /dev/null +++ b/interpreter/dotnet/nibblereader.go @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package dotnet + +import ( + "errors" + "io" +) + +// nibbleReader provides the interface to read nibble encoded data as implemented in +// https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/inc/nibblestream.h#L188 +type nibbleReader struct { + io.ByteReader + + cachedNibble uint8 + + err error +} + +func (nr *nibbleReader) Error() error { + return nr.err +} + +func (nr *nibbleReader) AlignToBytes() { + nr.cachedNibble = 0 +} + +// ReadNibble reads one nibble from the stream. +// https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/inc/nibblestream.h#L213 +func (nr *nibbleReader) ReadNibble() uint8 { + if nr.err != nil { + return 0 + } + if nr.cachedNibble != 0 { + nibble := nr.cachedNibble & 0xf + nr.cachedNibble = 0 + return nibble + } + + b, err := nr.ReadByte() + if err != nil { + nr.err = err + return 0 + } + + // Lower nibble first + nibble := b & 0xf + nr.cachedNibble = 0xf0 | (b >> 4) + return nibble +} + +// Uint32 reads one nibble encoded 32-bit unsigned integer. +// https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/inc/nibblestream.h#L250 +func (nr *nibbleReader) Uint32() uint32 { + val := uint32(0) + for i := 0; i < 11; i++ { + n := nr.ReadNibble() + val = (val << 3) + uint32(n&0x7) + if n&0x8 == 0 { + return val + } + } + nr.err = errors.New("corrupt nibble data") + return 0 +} + +// Int32 reads one nibble encoded 32-bit signed integer. +// https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/inc/nibblestream.h#L292 +func (nr *nibbleReader) Int32() int32 { + raw := nr.Uint32() + val := int32(raw >> 1) + if raw&1 != 0 { + val = -val + } + return val +} + +// Ptr reads one raw pointer value. +// https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/inc/nibblestream.h#L307 +// https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/debuginfostore.cpp#L224 +func (nr *nibbleReader) Ptr() uint64 { + val := uint64(0) + for i := 0; i < 64; i += 4 { + n := nr.ReadNibble() + val |= uint64(n) << i + } + return val +} diff --git a/interpreter/dotnet/nibblereader_test.go b/interpreter/dotnet/nibblereader_test.go new file mode 100644 index 00000000..903f192c --- /dev/null +++ b/interpreter/dotnet/nibblereader_test.go @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package dotnet + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNibbleReader(t *testing.T) { + testCases := []struct { + data string + expected uint32 + }{ + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/inc/nibblestream.h#L32-L46 + {"00", 0}, + {"71", 1}, + {"07", 7}, + {"09", 8}, + {"19", 9}, + {"7f", 63}, + {"8900", 64}, // incorrect example in dotnet code + {"8901", 65}, // incorrect example in dotnet code + {"ff07", 511}, + {"8908", 512}, + {"8918", 513}, + } + + for _, test := range testCases { + t.Run(test.data, func(t *testing.T) { + data, err := hex.DecodeString(test.data) + require.NoError(t, err, "Hex decoding failed") + + decoder := nibbleReader{ + ByteReader: bytes.NewReader(data), + } + value := decoder.Uint32() + require.NoError(t, decoder.Error(), "Error") + assert.Equal(t, test.expected, value, "Wrong nibble decoding") + }) + } +} diff --git a/interpreter/dotnet/pe.go b/interpreter/dotnet/pe.go new file mode 100644 index 00000000..1d43309a --- /dev/null +++ b/interpreter/dotnet/pe.go @@ -0,0 +1,1265 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package dotnet + +import ( + "bytes" + "debug/pe" + "encoding/binary" + "errors" + "fmt" + "io" + "os" + "slices" + "sync/atomic" + "time" + + "github.com/elastic/go-freelru" + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/libpf/readatbuf" + "github.com/elastic/otel-profiling-agent/process" + "github.com/elastic/otel-profiling-agent/util" +) + +const ( + // Maximum size of the LRU cache holding the executables' PE information. + peInfoCacheSize = 16384 + + // TTL of entries in the LRU cache holding the executables' PE information. + peInfoCacheTTL = 6 * time.Hour +) + +// OptionalHeader32 is the IMAGE_OPTIONAL_HEADER32 without its Magic or DataDirectory +// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header32 +type OptionalHeader32 struct { + MajorLinkerVersion uint8 + MinorLinkerVersion uint8 + SizeOfCode uint32 + SizeOfInitializedData uint32 + SizeOfUninitializedData uint32 + AddressOfEntryPoint uint32 + BaseOfCode uint32 + BaseOfData uint32 + ImageBase uint32 + SectionAlignment uint32 + FileAlignment uint32 + MajorOperatingSystemVersion uint16 + MinorOperatingSystemVersion uint16 + MajorImageVersion uint16 + MinorImageVersion uint16 + MajorSubsystemVersion uint16 + MinorSubsystemVersion uint16 + Win32VersionValue uint32 + SizeOfImage uint32 + SizeOfHeaders uint32 + CheckSum uint32 + Subsystem uint16 + DllCharacteristics uint16 + SizeOfStackReserve uint32 + SizeOfStackCommit uint32 + SizeOfHeapReserve uint32 + SizeOfHeapCommit uint32 + LoaderFlags uint32 + NumberOfRvaAndSizes uint32 +} + +// OptionalHeader64 is the IMAGE_OPTIONAL_HEADER64 without its Magic or DataDirectory +// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header64 +type OptionalHeader64 struct { + MajorLinkerVersion uint8 + MinorLinkerVersion uint8 + SizeOfCode uint32 + SizeOfInitializedData uint32 + SizeOfUninitializedData uint32 + AddressOfEntryPoint uint32 + BaseOfCode uint32 + ImageBase uint64 + SectionAlignment uint32 + FileAlignment uint32 + MajorOperatingSystemVersion uint16 + MinorOperatingSystemVersion uint16 + MajorImageVersion uint16 + MinorImageVersion uint16 + MajorSubsystemVersion uint16 + MinorSubsystemVersion uint16 + Win32VersionValue uint32 + SizeOfImage uint32 + SizeOfHeaders uint32 + CheckSum uint32 + Subsystem uint16 + DllCharacteristics uint16 + SizeOfStackReserve uint64 + SizeOfStackCommit uint64 + SizeOfHeapReserve uint64 + SizeOfHeapCommit uint64 + LoaderFlags uint32 + NumberOfRvaAndSizes uint32 +} + +// CLIHeader is the ECMA-335 II.25.3.3 CLI header +type CLIHeader struct { + SizeOfHeader uint32 + MajorRuntimeVersion uint16 + MinorRuntimeVersion uint16 + MetaData pe.DataDirectory + Flags uint32 + EntryPointToken uint32 + Resources pe.DataDirectory + StrongNameSignature pe.DataDirectory + CodeManagerTable pe.DataDirectory + VTableFixups pe.DataDirectory + ExportAddressTableJumps pe.DataDirectory + ManagedNativeHeader pe.DataDirectory +} + +const ( + // The image contains native code. + // ECMA-335 II.25.3.3.1 Runtime flags + // R2RFMT "PE Headers and CLI Headers" + comimageFlagsILLibrary = 0x04 + + // R2R RuntimeFunctions section identifier + r2rSectionRuntimeFunctions = 102 + // R2R MethodDefEntryPoints section identifier + r2rSectionMethodDefEntryPoints = 103 +) + +// ReadyToRunHeader is the R2RFMT READYTORUN_HEADER + READYTORUN_CORE_HEADER +type ReadyToRunHeader struct { + Signature uint32 + MajorVersion uint16 + MinorVersion uint16 + Flags uint32 + NumSections uint32 +} + +// ReadyToRunSection is the R2RFMT READYTORUN_SECTION +type ReadyToRunSection struct { + Type uint32 + Section pe.DataDirectory +} + +// ReadyToRunRuntimeFunction is the R2RFMT RUNTIME_FUNCTION for x86_64 +type ReadyToRunRuntimeFunction struct { + StartRVA uint32 + EndRVA uint32 + GCInfo uint32 +} + +// MetadataRoot is the ECMA-335 II.24.2.1 Metadata root (non-variable length header) +type MetadataRoot struct { + Signature uint32 + MajorVersion uint16 + MinorVersion uint16 + Reserved uint32 + Length uint32 +} + +// StreamHeader is the ECMA-335 II.24.2.2 Stream header (non-variable length header) +type StreamHeader struct { + Offset uint32 + Size uint32 +} + +// table* variables are ECMA-335 II.22 defined Metdata table numbers +const ( + tableModule = 0x00 + tableTypeRef = 0x01 + tableTypeDef = 0x02 + tableFieldPtr = 0x03 + tableField = 0x04 + tableMethodPtr = 0x05 + tableMethodDef = 0x06 + tableParam = 0x08 + tableInterfaceImpl = 0x09 + tableMemberRef = 0x0a + tableConstant = 0x0b + tableCustomAttribute = 0x0c + tableFieldMarshal = 0x0d + tableDeclSecurity = 0x0e + tableClassLayout = 0x0f + tableFieldLayout = 0x10 + tableStandAloneSig = 0x11 + tableEventMap = 0x12 + tableEvent = 0x14 + tablePropertyMap = 0x15 + tableProperty = 0x17 + tableMethodSemantics = 0x18 + tableMethodImpl = 0x19 + tableModuleRef = 0x1a + tableTypeSpec = 0x1b + tableImplMap = 0x1c + tableFieldRVA = 0x1d + tableAssembly = 0x20 + tableAssemblyProcessor = 0x21 + tableAssemblyOS = 0x22 + tableAssemblyRef = 0x23 + tableAssemblyRefProcessor = 0x24 + tableAssemblyRefOS = 0x25 + tableFile = 0x26 + tableExportedType = 0x27 + tableManifestResource = 0x28 + tableNestedClass = 0x29 + tableGenericParam = 0x2a + tableMethodSpec = 0x2b + tableGenericParamConstraint = 0x2c +) + +// peTypeSpec is the information we need to store from a TypeDef entry for symbolization +type peTypeSpec struct { + namespaceIdx uint32 + typeNameIdx uint32 + methodIdx uint32 + enclosingClass uint32 +} + +// peMethodSpec is the information we need to store from a MethodDef entry for symbolization +type peMethodSpec struct { + methodNameIdx uint32 + startRVA uint32 +} + +// index* variables are the index key types used as metadata table column values. +// These are internal to our code. +const ( + // Indexes to heap as defined in ECMA-335 II.24.2.[345] + indexString = iota + indexGUID + indexBlob + // Coded indexes as defined in ECMA-335 II.24.2.6 + indexResolutionScope + indexTypeDefOrRef + indexMethodDefOrRef + indexMemberRefParent + indexHasConstant + indexHasCustomAttribute + indexCustomAttributeType + indexHasFieldMarshal + indexHasDeclSecurity + indexHasSemantics + indexMemberForwarded + indexImplementation + // Indexes to ECMA-335 II.22 defined tables + indexTypeDef + indexField + indexMethodDef + indexParam + indexEvent + indexProperty + indexModuleRef + indexCount +) + +// peInfo is the information we need to cache from a Dotnet PE file for symbolization +type peInfo struct { + err error + lastModified int64 + fileID libpf.FileID + simpleName string + guid string + typeSpecs []peTypeSpec + methodSpecs []peMethodSpec + sizeOfImage uint32 + reported bool + + // strings contains the preloaded strings from dotnet string heap. + // If this consumes too much memory, this could be converted to LRU and on-demand + // populated by reading the strings from attached process memory. + strings map[uint32]string +} + +// peParser contains the needed data when reading and parsing the dotnet data from a PE file. +type peParser struct { + info *peInfo + headers []byte + + io.ReaderAt + io.ReadSeeker + + peBase int64 + + err error + + nt pe.FileHeader + cli pe.DataDirectory + sections []pe.SectionHeader32 + + indexSizes [indexCount]int + tableRows [64]uint32 + + dotnetTables io.ReadSeeker + dotnetStrings io.ReaderAt + dotnetGUID io.ReaderAt + + r2rFunctions io.ReadSeeker +} + +func (pp *peParser) parseMZ() error { + // ECMA-335 II.25.2.1 "MS-DOS header" has additional requirements for this header. + + // The first 96 contains the MZ header + if pp.headers[0] != 'M' || pp.headers[1] != 'Z' { + return fmt.Errorf("invalid MZ header: %x", pp.headers[0:2]) + } + + // PE signature offset + signoff := int64(binary.LittleEndian.Uint32(pp.headers[0x3c:])) + if signoff >= int64(len(pp.headers)-4) { + return fmt.Errorf("invalid PE offset: %x", signoff) + } + + if !bytes.Equal(pp.headers[signoff:signoff+4], []byte{'P', 'E', 0, 0}) { + return fmt.Errorf("invalid PE magic: %x", pp.headers[signoff:signoff+4]) + } + pp.peBase = signoff + 4 + return nil +} + +func (pp *peParser) parsePE() error { + // ECMA-335 II.25.2.2 "PE File header" defines this + _, _ = pp.Seek(pp.peBase, io.SeekStart) + if err := binary.Read(pp, binary.LittleEndian, &pp.nt); err != nil { + return err + } + + // According to ECMA-335 the Machine should be always IMAGE_FILE_MACHINE_I386. + // R2RFMT "PE Headers and CLI Headers", Machine is set to platform for which + // Ready to Run code has been generated. + switch pp.nt.Machine { + case pe.IMAGE_FILE_MACHINE_AMD64, + pe.IMAGE_FILE_MACHINE_I386, // According to ECMA spec always this + 0xfd1d: // Seen on dotnet internal .dlls + // ok + default: + return fmt.Errorf("unrecognized PE machine: %#x", pp.nt.Machine) + } + return nil +} + +func (pp *peParser) parseOptionalHeader() error { + // ECMA-335 II.25.2.3 "PE optional header" defines requirements for this header + if _, err := pp.Seek(pp.peBase+int64(binary.Size(pp.nt)), io.SeekStart); err != nil { + return err + } + + var magic uint16 + if err := binary.Read(pp, binary.LittleEndian, &magic); err != nil { + return err + } + + // ECMA-335 II.25.2.3.1 requires always a PE32 (0x10b) header, but the dotnet clr + // internal PE files have actually a PE32+ header. + var numDirectories, sizeHeaders uint32 + switch magic { + case 0x10b: // PE32 + var opt32 OptionalHeader32 + if err := binary.Read(pp, binary.LittleEndian, &opt32); err != nil { + return err + } + sizeHeaders = opt32.SizeOfHeaders + numDirectories = opt32.NumberOfRvaAndSizes + pp.info.sizeOfImage = opt32.SizeOfImage + case 0x20b: // PE32+ (PE64) + var opt64 OptionalHeader64 + if err := binary.Read(pp, binary.LittleEndian, &opt64); err != nil { + return err + } + sizeHeaders = opt64.SizeOfHeaders + numDirectories = opt64.NumberOfRvaAndSizes + pp.info.sizeOfImage = opt64.SizeOfImage + default: + return fmt.Errorf("invalid optional header magic: %x", magic) + } + if sizeHeaders > uint32(len(pp.headers)) { + return fmt.Errorf("invalid header size: %d", sizeHeaders) + } + if numDirectories < 0x10 { + return fmt.Errorf("invalid unmber of data directories: %d", numDirectories) + } + + // ECMA-335 II.25.2.3.3 "PE header data directories" defines the data directory + // indexes. Slot 14 is the "CLI Header" data directory entry. + if _, err := pp.Seek(14*int64(binary.Size(pe.DataDirectory{})), io.SeekCurrent); err != nil { + return err + } + if err := binary.Read(pp, binary.LittleEndian, &pp.cli); err != nil { + return err + } + + pp.sections = make([]pe.SectionHeader32, pp.nt.NumberOfSections) + if _, err := pp.Seek(int64(numDirectories-15)*int64(binary.Size(pe.DataDirectory{})), + io.SeekCurrent); err != nil { + return err + } + + if err := binary.Read(pp, binary.LittleEndian, pp.sections); err != nil { + return err + } + + // Check sections headers that they look sane to the extent we care + for index, section := range pp.sections { + if section.VirtualSize >= 0x10000000 { + return fmt.Errorf("section %d, virtual size is huge (%#x)", + index, section.VirtualSize) + } + if section.VirtualAddress >= 0x10000000 { + return fmt.Errorf("section %d, relative virtual address (RVA) is huge (%#x)", + index, section.VirtualAddress) + } + } + + return nil +} + +// getRVASectionReader() find the PE Section containing the requested DataDirectory and +// creates a SectionReader for the range. This is done by searching for the matching +// PE Section mapping and converting the Relative Virtual Address (RVA) to file offset. +func (pp *peParser) getRVASectionReader(dd pe.DataDirectory) (*io.SectionReader, error) { + for _, s := range pp.sections { + if dd.VirtualAddress >= s.VirtualAddress && + dd.VirtualAddress+dd.Size <= s.VirtualAddress+s.VirtualSize { + return io.NewSectionReader(pp.ReaderAt, + int64(dd.VirtualAddress)-int64(s.VirtualAddress)+int64(s.PointerToRawData), + int64(dd.Size)), nil + } + } + return nil, fmt.Errorf("unable to find section for data at %#x-%#x", + dd.VirtualAddress, dd.VirtualAddress+dd.Size) +} + +func roundUp(value, alignment uint32) uint32 { + return (value + alignment - 1) &^ (alignment - 1) +} + +func (pp *peParser) parseR2RMethodDefs(table pe.DataDirectory) error { + r, err := pp.getRVASectionReader(table) + if err != nil { + return err + } + nr := nativeReader{ReaderAt: r} + prevIndex := uint32(0) + prevRVA := uint32(0) + + // The ready-to-run MethodDefs table is a lookup table indexed with MethodDef index, + // and the data contains R2R RuntimeFunction table index (among other things). + // The callback will get monotonic MethodDef index, and monotonic startRVA. + return nr.WalkTable(func(index uint32, offset int64) error { + id, _, err := nr.Uint(offset) + if err != nil { + return err + } + // The entry decoding is from: + // https://github.com/dotnet/runtime/blob/v8.0.0/src/coreclr/vm/readytoruninfo.cpp#L1181 + if id&1 != 0 { + id >>= 2 + } else { + id >>= 1 + } + // id is index to the RuntimeFunctions table. + // Read the Function start address. + var f ReadyToRunRuntimeFunction + _, err = pp.r2rFunctions.Seek(int64(id*uint32(binary.Size(f))), io.SeekStart) + if err != nil { + return err + } + if err := binary.Read(pp.r2rFunctions, binary.LittleEndian, &f); err != nil { + return err + } + // Shift by one, so that the methods without r2r implementation can + // be inserted in-between valid RVAs + startRVA := f.StartRVA << 1 + if startRVA < prevRVA { + return fmt.Errorf("non-monotonic R2R code RVA: %x < %x", + startRVA, prevRVA) + } + prevRVA = startRVA + for i := prevIndex + 1; i < index; i++ { + pp.info.methodSpecs[i].startRVA = startRVA - 1 + } + // Record the Start RVA + pp.info.methodSpecs[index].startRVA = startRVA + prevIndex = index + return nil + }) +} + +// parseR2R reads the Read-To-Run data directory needed for symbolization +func (pp *peParser) parseR2R(hdr pe.DataDirectory) error { + var r2r ReadyToRunHeader + + r, err := pp.getRVASectionReader(hdr) + if err != nil { + return err + } + if err = binary.Read(r, binary.LittleEndian, &r2r); err != nil { + return err + } + if r2r.Signature != 0x00525452 { + return nil + } + // Walk the Sections. See R2RFMT READYTORUN_SECTION. The array is + // sorted by section Type to allow binary search. + for i := uint32(0); i < r2r.NumSections; i++ { + var s ReadyToRunSection + if err = binary.Read(r, binary.LittleEndian, &s); err != nil { + return err + } + switch s.Type { + case r2rSectionRuntimeFunctions: + pp.r2rFunctions, err = pp.getRVASectionReader(s.Section) + if err != nil { + return err + } + case r2rSectionMethodDefEntryPoints: + return pp.parseR2RMethodDefs(s.Section) + } + } + return nil +} + +func (pp *peParser) parseCLI() error { + r, err := pp.getRVASectionReader(pp.cli) + if err != nil { + return err + } + + // Read the data from ECMA-335 II.25.3.3 CLI header + var cliHeader CLIHeader + if err = binary.Read(r, binary.LittleEndian, &cliHeader); err != nil { + return err + } + + // Read and parse the data from ECMA-335 II.24.2.1 Metadata root + var metadataRoot MetadataRoot + r, err = pp.getRVASectionReader(cliHeader.MetaData) + if err != nil { + return err + } + if err = binary.Read(r, binary.LittleEndian, &metadataRoot); err != nil { + return err + } + if metadataRoot.Signature != 0x424A5342 { + return fmt.Errorf("invalid metadata signature %#x", metadataRoot.Signature) + } + if _, err = r.Seek(int64(roundUp(metadataRoot.Length, 4)+2), io.SeekCurrent); err != nil { + return err + } + + var numStreams uint16 + if err = binary.Read(r, binary.LittleEndian, &numStreams); err != nil { + return err + } + for i := uint16(0); i < numStreams; i++ { + // Read and parse the ECMA-335 II.24.2.2 Stream header + var hdr StreamHeader + var nameBuf [32]byte + if err = binary.Read(r, binary.LittleEndian, &hdr); err != nil { + return err + } + name := nameBuf[:] + for j := 0; j < len(name); j += 4 { + block := nameBuf[j : j+4] + if _, err = r.Read(block); err != nil { + return err + } + if n := bytes.IndexByte(block, 0); n >= 0 { + name = nameBuf[:j+n] + break + } + } + switch string(name) { + case "#Strings": + // ECMA-335 II.24.2.3 #Strings heap + pp.dotnetStrings = io.NewSectionReader(r, int64(hdr.Offset), int64(hdr.Size)) + case "#GUID": + // ECMA-335 II.24.2.5 #GUID heap + pp.dotnetGUID = io.NewSectionReader(r, int64(hdr.Offset), int64(hdr.Size)) + case "#~": + // ECMA-335 II.24.2.6 #~ stream + pp.dotnetTables = io.NewSectionReader(r, int64(hdr.Offset), int64(hdr.Size)) + } + } + + if err = pp.parseTables(); err != nil { + return err + } + + // Check for R2R header + if cliHeader.Flags&comimageFlagsILLibrary != 0 { + if err = pp.parseR2R(cliHeader.ManagedNativeHeader); err != nil { + return err + } + } + + return nil +} + +func (pp *peParser) readDotnetString(offs uint32) string { + // Read a string from the ECMA-335 II.24.2.3 #Strings heap + if offs == 0 { + return "" + } + + // Zero terminated string. Assume maximum length of 1024 bytes. + // But read it in small chunks to make good use of the readatbuf. + var str [1024]byte + chunkSize := 128 + for i := 0; i < len(str); i += chunkSize { + chunk := str[i : i+chunkSize] + n, err := pp.dotnetStrings.ReadAt(chunk, int64(offs)+int64(i)) + if n == 0 && err != nil { + return "" + } + + zeroIdx := bytes.IndexByte(chunk[:n], 0) + if zeroIdx >= 0 { + return string(str[:i+zeroIdx]) + } + } + + // Likely broken string. + return "" +} + +func (pp *peParser) readDotnetGUID(offs uint32) string { + // Read a GUID from the ECMA-335 II.24.2.5 #GUID heap + if offs == 0 { + return "" + } + + var guid [16]byte + if _, err := pp.dotnetGUID.ReadAt(guid[:], int64(offs-1)*16); err != nil { + return "" + } + + // Format as a GUID string + return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", + binary.LittleEndian.Uint32(guid[:4]), + binary.LittleEndian.Uint16(guid[4:6]), + binary.LittleEndian.Uint16(guid[6:8]), + guid[8:10], + guid[10:]) +} + +func (pp *peParser) preloadString(heapIndex uint32) { + // String index is well known empty string + if heapIndex == 0 { + return + } + // Check if already loaded + if _, ok := pp.info.strings[heapIndex]; ok { + return + } + pp.info.strings[heapIndex] = pp.readDotnetString(heapIndex) +} + +func (pp *peParser) skipDotnetBytes(n int) { + if n == 0 || pp.err != nil { + return + } + _, pp.err = pp.dotnetTables.Seek(int64(n), io.SeekCurrent) +} + +func (pp *peParser) readDotnetIndex(kind int) uint32 { + if pp.err != nil { + return 0 + } + switch pp.indexSizes[kind] { + case 2: + var value uint16 + if pp.err = binary.Read(pp.dotnetTables, binary.LittleEndian, &value); pp.err != nil { + return 0 + } + return uint32(value) + case 4: + var value uint32 + if pp.err = binary.Read(pp.dotnetTables, binary.LittleEndian, &value); pp.err != nil { + return 0 + } + return value + } + pp.err = fmt.Errorf("tried to read index (%d) value with invalid size (%d)", + kind, pp.indexSizes[kind]) + return 0 +} + +// parseModuleTable parses an ECMA-335 II.22.30 Module table +func (pp *peParser) parseModuleTable() { + // Generation a 2-byte value, reserved, shall be zero + // Name an index into the String heap + // Mvid an index into the Guid heap; differs between two versions of the same module + // EncID an index into the Guid heap; reserved, shall be zero + // EncBaseID an index into the Guid heap; reserved, shall be zero + for i := uint32(0); i < pp.tableRows[tableModule]; i++ { + pp.skipDotnetBytes(2) + nameIdx := pp.readDotnetIndex(indexString) + guidIdx := pp.readDotnetIndex(indexGUID) + pp.skipDotnetBytes(2 * pp.indexSizes[indexGUID]) + + pp.info.simpleName = pp.readDotnetString(nameIdx) + pp.info.guid = pp.readDotnetGUID(guidIdx) + } +} + +// preloadTypeSpecStrings preload the strings for given TypeDef entry +func (pp *peParser) preloadTypeSpecStrings(spec *peTypeSpec) { + if spec.methodIdx < pp.tableRows[tableMethodDef] { + pp.preloadString(spec.namespaceIdx) + pp.preloadString(spec.typeNameIdx) + } +} + +// parseTypeDef parses an ECMA-335 II.22.37 TypeDef table +func (pp *peParser) parseTypeDef() { + // Flags a 4-byte bitmask of type TypeAttributes, §II.23.1.15 + // TypeName an index into the String heap + // TypeNamespace an index into the String heap + // Extends a TypeDefOrRef (§II.24.2.6) coded index + // FieldList an index into the Field table; first Fields owned by this Type + // MethodList an index into the MethodDef table; first Method owned by this Type + + specs := make([]peTypeSpec, 0, pp.tableRows[tableTypeDef]) + + // NOTE: We could probably not load the rows where MethodList is same as the next + // entry as it is a type without Methods. We also do lookups from symbolization + // via binary search using the methodIdx field. However, the NestedClass table + // will contain direct indexes to this table, so we would need to record the index + // or do the elimination later during the load - so perhaps its not worth while. + + prevEntry := peTypeSpec{} + for i := uint32(0); i < pp.tableRows[tableTypeDef]; i++ { + pp.skipDotnetBytes(4) + typeNameIdx := pp.readDotnetIndex(indexString) + namespaceIdx := pp.readDotnetIndex(indexString) + pp.skipDotnetBytes(pp.indexSizes[indexTypeDefOrRef] + pp.indexSizes[indexField]) + methodIdx := pp.readDotnetIndex(indexMethodDef) + + if prevEntry.methodIdx != methodIdx { + pp.preloadTypeSpecStrings(&prevEntry) + } + + prevEntry = peTypeSpec{ + namespaceIdx: namespaceIdx, + typeNameIdx: typeNameIdx, + methodIdx: methodIdx, + } + specs = append(specs, prevEntry) + } + pp.preloadTypeSpecStrings(&specs[len(specs)-1]) + + pp.info.typeSpecs = specs +} + +// parseMethodDef parses the ECMA-335 II.22.26 MethodDef table +func (pp *peParser) parseMethodDef() { + // RVA a 4-byte constant + // ImplFlags a 2-byte bitmask of type MethodImplAttributes, §II.23.1.10 + // Flags a 2-byte bitmask of type MethodAttributes, §II.23.1.10 + // Name an index into the String heap + // Signature an index into the Blob heap + // ParamList an index into the Param table + + specs := make([]peMethodSpec, 0, pp.tableRows[tableMethodDef]) + + for i := uint32(0); i < pp.tableRows[tableMethodDef]; i++ { + pp.skipDotnetBytes(4 + 2 + 2) + nameIdx := pp.readDotnetIndex(indexString) + pp.skipDotnetBytes(pp.indexSizes[indexBlob] + pp.indexSizes[indexParam]) + + pp.preloadString(nameIdx) + specs = append(specs, peMethodSpec{methodNameIdx: nameIdx}) + } + pp.info.methodSpecs = specs +} + +// parseNestedClass parses the ECMA-335 II.22.32 NestedClass table +func (pp *peParser) parseNestedClass() { + // NestedClass an index into the TypeDef table + // EnclosingClass an index into the TypeDef table + + numTypeDefs := uint32(len(pp.info.typeSpecs)) + + for i := uint32(0); i < pp.tableRows[tableNestedClass]; i++ { + nestedClass := pp.readDotnetIndex(indexTypeDef) + enclosingClass := pp.readDotnetIndex(indexTypeDef) + if nestedClass <= 0 || nestedClass > numTypeDefs || + enclosingClass <= 0 || enclosingClass > numTypeDefs { + // Invalid indexes + pp.err = fmt.Errorf("invalid NestedClass row %d: indexes (%d/%d) vs. %d typedefs", + i, nestedClass, enclosingClass, numTypeDefs) + return + } + pp.info.typeSpecs[nestedClass-1].enclosingClass = enclosingClass + } +} + +// getHeapSize returns the heap size depending if its large or not +func getHeapSize(isLarge bool) int { + if isLarge { + return 4 + } + return 2 +} + +// getIndexSize calculates the encoded index size given its tag bit size and indexes +// refer to ECMA-335 II.24.2.6 portion about "coded index" on the details. +func (pp *peParser) getIndexSize(tagBits int, indexes []uint) int { + maxRows := uint32(0) + for _, index := range indexes { + if pp.tableRows[index] > maxRows { + maxRows = pp.tableRows[index] + } + } + if maxRows >= uint32(1<<(16-tagBits)) { + return 4 + } + return 2 +} + +func (pp *peParser) parseTables() error { + // Parse the ECMA-335 II.24.2.6 #~ stream + + var tablesHeader struct { + Reserved0 uint32 + MajorVersion uint8 + MinorVersion uint8 + HeapSizes uint8 + Reserved1 uint8 + Valid uint64 + Sorted uint64 + // Rows[] entry for each Valid bit + // Tables + } + r := pp.dotnetTables + if err := binary.Read(r, binary.LittleEndian, &tablesHeader); err != nil { + return err + } + for i := 0; i < 64; i++ { + if tablesHeader.Valid&(1< tableNestedClass { + break + } + return fmt.Errorf("metadata table %x not implemented", tableIndex) + } + + if rowSize != 0 { + pp.skipDotnetBytes(rowSize * int(rowCount)) + } + + if pp.err != nil { + return fmt.Errorf("metadata table parsing failed: %w", pp.err) + } + } + + return nil +} + +func (pp *peParser) parse() error { + var err error + + // Rest of the code reads the file using RVAs and section mappings + // Use caching file reader + if pp.ReaderAt, err = readatbuf.New(pp.ReaderAt, 4096, 4); err != nil { + return err + } + // Dotnet requires currently all headers to fit into 4K + pp.headers = make([]byte, 4096) + if _, err = pp.ReadAt(pp.headers, 0); err != nil { + return fmt.Errorf("failed to read PE header: %v", err) + } + pp.ReadSeeker = bytes.NewReader(pp.headers) + if err = pp.parseMZ(); err != nil { + return err + } + if err = pp.parsePE(); err != nil { + return err + } + if err = pp.parseOptionalHeader(); err != nil { + return err + } + return pp.parseCLI() +} + +func (pi *peInfo) resolveMethodName(methodIdx uint32) string { + if methodIdx == 0 || methodIdx > uint32(len(pi.methodSpecs)) { + return fmt.Sprintf("", methodIdx, len(pi.methodSpecs)) + } + + idx, ok := slices.BinarySearchFunc(pi.typeSpecs, methodIdx, + func(typespec peTypeSpec, methodIdx uint32) int { + if methodIdx < typespec.methodIdx { + return 1 + } + if methodIdx > typespec.methodIdx { + return -1 + } + return 0 + }) + if !ok { + idx-- + } + + typeSpec := &pi.typeSpecs[idx] + typeName := pi.strings[typeSpec.typeNameIdx] + for typeSpec.enclosingClass != 0 { + enclosingSpec := &pi.typeSpecs[typeSpec.enclosingClass-1] + typeName = fmt.Sprintf("%s/%s", pi.strings[enclosingSpec.typeNameIdx], typeName) + typeSpec = enclosingSpec + } + methodName := pi.strings[pi.methodSpecs[methodIdx-1].methodNameIdx] + if typeSpec.namespaceIdx != 0 { + return fmt.Sprintf("%s.%s.%s", + pi.strings[typeSpec.namespaceIdx], + typeName, methodName) + } + return fmt.Sprintf("%s.%s", typeName, methodName) +} + +func (pi *peInfo) resolveR2RMethodName(pcRVA uint32) string { + idx, ok := slices.BinarySearchFunc(pi.methodSpecs, pcRVA<<1, + func(methodspec peMethodSpec, pcRVA uint32) int { + if pcRVA < methodspec.startRVA { + return 1 + } + if pcRVA > methodspec.startRVA { + return -1 + } + return 0 + }) + if !ok { + idx-- + } + str := pi.resolveMethodName(uint32(idx + 1)) + return str +} + +func (pi *peInfo) parse(r io.ReaderAt) error { + pp := peParser{ + ReaderAt: r, + info: pi, + } + err := pp.parse() + if err != nil { + return err + } + return nil +} + +type peCache struct { + // peInfoCacheHit + peInfoCacheHit atomic.Uint64 + peInfoCacheMiss atomic.Uint64 + + // elfInfoCache provides a cache to quickly retrieve the PE info and fileID for a particular + // executable. It caches results based on iNode number and device ID. Locked LRU. + peInfoCache *freelru.LRU[util.OnDiskFileIdentifier, *peInfo] +} + +func (pc *peCache) init() { + peInfoCache, err := freelru.New[util.OnDiskFileIdentifier, *peInfo](peInfoCacheSize, + util.OnDiskFileIdentifier.Hash32) + if err != nil { + panic(fmt.Errorf("unable to create peInfoCache: %v", err)) + } + peInfoCache.SetLifetime(peInfoCacheTTL) + pc.peInfoCache = peInfoCache +} + +func (pc *peCache) Get(pr process.Process, mapping *process.Mapping) *peInfo { + key := mapping.GetOnDiskFileIdentifier() + lastModified := pr.GetMappingFileLastModified(mapping) + if info, ok := pc.peInfoCache.Get(key); ok && info.lastModified == lastModified { + // Cached data ok + pc.peInfoCacheHit.Add(1) + return info + } + + // Slow path, calculate all the data and update cache + pc.peInfoCacheMiss.Add(1) + + file, err := pr.OpenMappingFile(mapping) + if err != nil { + info := &peInfo{err: err} + if !errors.Is(err, os.ErrNotExist) { + pc.peInfoCache.Add(key, info) + } + return info + } + defer file.Close() + + info := &peInfo{ + err: err, + lastModified: lastModified, + } + err = info.parse(file) + if err == nil { + info.fileID, err = pr.CalculateMappingFileID(mapping) + } + if err != nil { + info.err = err + } + + pc.peInfoCache.Add(key, info) + return info +} + +var globalPeCache peCache + +func init() { + globalPeCache.init() +} diff --git a/interpreter/hotspot/data.go b/interpreter/hotspot/data.go new file mode 100644 index 00000000..a8923f56 --- /dev/null +++ b/interpreter/hotspot/data.go @@ -0,0 +1,680 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package hotspot + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "reflect" + "strings" + + log "github.com/sirupsen/logrus" + + "github.com/elastic/go-freelru" + + "github.com/elastic/otel-profiling-agent/interpreter" + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/libpf/pfelf" + "github.com/elastic/otel-profiling-agent/libpf/xsync" + "github.com/elastic/otel-profiling-agent/lpm" + npsr "github.com/elastic/otel-profiling-agent/nopanicslicereader" + "github.com/elastic/otel-profiling-agent/remotememory" + "github.com/elastic/otel-profiling-agent/util" +) + +// hotspotIntrospectionTable contains the resolved ELF symbols for an introspection table +type hotspotIntrospectionTable struct { + skipBaseDref bool + base, stride libpf.Address + typeOffset, fieldOffset libpf.Address + valueOffset, addressOffset libpf.Address +} + +// resolveSymbols resolves the ELF symbols of the introspection table +func (it *hotspotIntrospectionTable) resolveSymbols(ef *pfelf.File, symNames []string) error { + symVals := make([]libpf.Address, len(symNames)) + for i, s := range symNames { + if s == "" { + continue + } + addr, err := ef.LookupSymbolAddress(libpf.SymbolName(s)) + if err != nil { + return fmt.Errorf("symbol '%v' not found: %w", s, err) + } + symVals[i] = libpf.Address(addr) + } + + it.base, it.stride = symVals[0], symVals[1] + it.typeOffset, it.fieldOffset = symVals[2], symVals[3] + it.valueOffset, it.addressOffset = symVals[4], symVals[5] + return nil +} + +// hotspotVMData contains static information from one HotSpot build (libjvm.so). +// It mostly is limited to the introspection data (class sizes and field offsets) and +// the version. +type hotspotVMData struct { + // err is the permanent error if introspection data is not supported + err error + + // version is the JDK numeric version. Used in some places to make version specific + // adjustments to the unwinding process. + version uint32 + + // versionStr is the Hotspot build version string, and can contain additional + // details such as the distribution name and patch level. + versionStr string + + // unsigned5X is the number of exclusion bytes used in UNSIGNED5 encoding + unsigned5X uint8 + + // vmStructs reflects the HotSpot introspection data we want to extract + // from the runtime. It is filled using golang reflection (the struct and + // field names are used to find the data from the JVM). Thus the structs + // here are following the JVM naming convention. + // + // The comments of .Sizeof like ">xxx" are to signify the size range of the JVM + // C++ class and thus the expected value of .Sizeof member. This is mainly to + // indicate the classes for which uint8 is not enough to hold the offset values + // for the eBPF code. + vmStructs struct { + AbstractVMVersion struct { + Release libpf.Address `name:"_s_vm_release"` + MajorVersion libpf.Address `name:"_vm_major_version"` + MinorVersion libpf.Address `name:"_vm_minor_version"` + SecurityVersion libpf.Address `name:"_vm_security_version"` + BuildNumber libpf.Address `name:"_vm_build_number"` + } `name:"Abstract_VM_Version"` + JdkVersion struct { + Current libpf.Address `name:"_current"` + } `name:"JDK_Version"` + CodeBlob struct { + Sizeof uint + Name uint `name:"_name"` + FrameCompleteOffset uint `name:"_frame_complete_offset"` + FrameSize uint `name:"_frame_size"` + // JDK -8: offset, JDK 9+: pointers + CodeBegin uint `name:"_code_begin,_code_offset"` + CodeEnd uint `name:"_code_end,_data_offset"` + } + CodeCache struct { + Heap libpf.Address `name:"_heap"` + Heaps libpf.Address `name:"_heaps"` + HighBound libpf.Address `name:"_high_bound"` + LowBound libpf.Address `name:"_low_bound"` + } + CodeHeap struct { + Sizeof uint + Log2SegmentSize uint `name:"_log2_segment_size"` + Memory uint `name:"_memory"` + Segmap uint `name:"_segmap"` + } + CompiledMethod struct { // .Sizeof >200 + Sizeof uint + DeoptHandlerBegin uint `name:"_deopt_handler_begin"` + Method uint `name:"_method"` + ScopesDataBegin uint `name:"_scopes_data_begin"` + } + ConstantPool struct { + Sizeof uint + PoolHolder uint `name:"_pool_holder"` + SourceFileNameIndex uint `name:"_source_file_name_index"` + } `name:"ConstantPool,constantPoolOopDesc"` + ConstMethod struct { + Sizeof uint + Constants uint `name:"_constants"` + CodeSize uint `name:"_code_size"` + // JDK21+: ConstMethod._flags is now a struct with another _flags field + // https://github.com/openjdk/jdk/commit/316d303c1da550c9589c9be56b65650964e3886b + Flags uint `name:"_flags,_flags._flags"` + NameIndex uint `name:"_name_index"` + SignatureIndex uint `name:"_signature_index"` + } `name:"ConstMethod,constMethodOopDesc"` + // JDK9-15 structure + GenericGrowableArray struct { + Len uint `name:"_len"` + } + // JDK16 structure + GrowableArrayBase struct { + Len uint `name:"_len"` + } + GrowableArrayInt struct { + Sizeof uint + Data uint `name:"_data"` + } `name:"GrowableArray"` + HeapBlock struct { + Sizeof uint + } + InstanceKlass struct { // .Sizeof >400 + Sizeof uint + SourceFileNameIndex uint `name:"_source_file_name_index"` + SourceFileName uint `name:"_source_file_name"` // JDK -7 only + } `name:"InstanceKlass,instanceKlass"` + Klass struct { // .Sizeof >200 + Sizeof uint + Name uint `name:"_name"` + } + Method struct { + ConstMethod uint `name:"_constMethod"` + } `name:"Method,methodOopDesc"` + Nmethod struct { // .Sizeof >256 + Sizeof uint + CompileID uint `name:"_compile_id"` + MetadataOffset uint `name:"_metadata_offset,_oops_offset"` + ScopesPcsOffset uint `name:"_scopes_pcs_offset"` + DependenciesOffset uint `name:"_dependencies_offset"` + OrigPcOffset uint `name:"_orig_pc_offset"` + DeoptimizeOffset uint `name:"_deoptimize_offset"` + Method uint `name:"_method"` + ScopesDataOffset uint `name:"_scopes_data_offset"` // JDK -8 only + } `name:"nmethod"` + OopDesc struct { + Sizeof uint + } `name:"oopDesc"` + PcDesc struct { + Sizeof uint + PcOffset uint `name:"_pc_offset"` + ScopeDecodeOffset uint `name:"_scope_decode_offset"` + } + StubRoutines struct { + Sizeof uint // not needed, just keep this out of CatchAll + CatchAll map[string]libpf.Address `name:"*"` + } + Symbol struct { + Sizeof uint + Body uint `name:"_body"` + Length uint `name:"_length"` + LengthAndRefcount uint `name:"_length_and_refcount"` + } + VirtualSpace struct { + HighBoundary uint `name:"_high_boundary"` + LowBoundary uint `name:"_low_boundary"` + } + } +} + +// fieldByJavaName searches obj for a field by its JVM name using the struct tags. +func fieldByJavaName(obj reflect.Value, fieldName string) reflect.Value { + var catchAll reflect.Value + + objType := obj.Type() + for i := 0; i < obj.NumField(); i++ { + objField := objType.Field(i) + if nameTag, ok := objField.Tag.Lookup("name"); ok { + for _, javaName := range strings.Split(nameTag, ",") { + if fieldName == javaName { + return obj.Field(i) + } + if javaName == "*" { + catchAll = obj.Field(i) + } + } + } + if fieldName == objField.Name { + return obj.Field(i) + } + } + + return catchAll +} + +// parseIntrospection loads and parses HotSpot introspection tables. It will then fill in +// hotspotData.vmStructs using reflection to gather the offsets and sizes +// we are interested about. +func (vmd *hotspotVMData) parseIntrospection(it *hotspotIntrospectionTable, + rm remotememory.RemoteMemory, loadBias libpf.Address) error { + stride := libpf.Address(rm.Uint64(it.stride + loadBias)) + typeOffs := uint(rm.Uint64(it.typeOffset + loadBias)) + addrOffs := uint(rm.Uint64(it.addressOffset + loadBias)) + fieldOffs := uint(rm.Uint64(it.fieldOffset + loadBias)) + valOffs := uint(rm.Uint64(it.valueOffset + loadBias)) + base := it.base + loadBias + + if !it.skipBaseDref { + base = rm.Ptr(base) + } + + if base == 0 || stride == 0 { + return fmt.Errorf("bad introspection table data (%#x / %d)", base, stride) + } + + // Parse the introspection table + e := make([]byte, stride) + vm := reflect.ValueOf(&vmd.vmStructs).Elem() + for addr := base; true; addr += stride { + if err := rm.Read(addr, e); err != nil { + return err + } + + typeNamePtr := npsr.Ptr(e, typeOffs) + if typeNamePtr == 0 { + break + } + + typeName := rm.String(typeNamePtr) + f := fieldByJavaName(vm, typeName) + if !f.IsValid() { + continue + } + + // If parsing the Types table, we have sizes. Otherwise, we are + // parsing offsets for fields. + fieldName := "Sizeof" + if it.fieldOffset != 0 { + fieldNamePtr := npsr.Ptr(e, fieldOffs) + fieldName = rm.String(fieldNamePtr) + if fieldName == "" || fieldName[0] != '_' { + continue + } + } + + f = fieldByJavaName(f, fieldName) + if !f.IsValid() { + continue + } + + value := uint64(npsr.Ptr(e, addrOffs)) + if value != 0 { + // We just resolved a const pointer. Adjust it by loadBias + // to get a globally cacheable unrelocated virtual address. + value -= uint64(loadBias) + log.Debugf("JVM %v.%v = @ %x", typeName, fieldName, value) + } else { + // Literal value + value = npsr.Uint64(e, valOffs) + log.Debugf("JVM %v.%v = %v", typeName, fieldName, value) + } + + switch f.Kind() { + case reflect.Uint64, reflect.Uint, reflect.Uintptr: + f.SetUint(value) + case reflect.Map: + if f.IsNil() { + // maps need explicit init (nil is invalid) + f.Set(reflect.MakeMap(f.Type())) + } + + castedValue := reflect.ValueOf(value).Convert(f.Type().Elem()) + f.SetMapIndex(reflect.ValueOf(fieldName), castedValue) + default: + panic(fmt.Sprintf("bug: unexpected field type in vmStructs: %v", f.Kind())) + } + } + return nil +} + +type hotspotData struct { + // ELF symbols needed for the introspection data + typePtrs, structPtrs, jvmciStructPtrs hotspotIntrospectionTable + + // Once protected hotspotVMData + xsync.Once[hotspotVMData] +} + +func (d *hotspotData) newUnsigned5Decoder(r io.ByteReader) *unsigned5Decoder { + return &unsigned5Decoder{ + r: r, + x: d.Get().unsigned5X, + } +} + +func (d *hotspotData) String() string { + if vmd := d.Get(); vmd != nil { + return fmt.Sprintf("Java HotSpot VM %d.%d.%d+%d (%v)", + (vmd.version>>24)&0xff, (vmd.version>>16)&0xff, + (vmd.version>>8)&0xff, vmd.version&0xff, + vmd.versionStr) + } + return "" +} + +// Attach loads to the ebpf program the needed pointers and sizes to unwind given hotspot process. +// As the hotspot unwinder depends on the native unwinder, a part of the cleanup is done by the +// process manager and not the corresponding Detach() function of hotspot objects. +func (d *hotspotData) Attach(_ interpreter.EbpfHandler, _ util.PID, bias libpf.Address, + rm remotememory.RemoteMemory) (ii interpreter.Instance, err error) { + // Each function has four symbols: source filename, class name, + // method name and signature. However, most of them are shared across + // different methods, so assume about 2 unique symbols per function. + addrToSymbol, err := + freelru.New[libpf.Address, string](2*interpreter.LruFunctionCacheSize, + libpf.Address.Hash32) + if err != nil { + return nil, err + } + addrToMethod, err := + freelru.New[libpf.Address, *hotspotMethod](interpreter.LruFunctionCacheSize, + libpf.Address.Hash32) + if err != nil { + return nil, err + } + addrToJITInfo, err := + freelru.New[libpf.Address, *hotspotJITInfo](interpreter.LruFunctionCacheSize, + libpf.Address.Hash32) + if err != nil { + return nil, err + } + // In total there are about 100 to 200 intrinsics. We don't expect to encounter + // everyone single one. So we use a small cache size here than LruFunctionCacheSize. + addrToStubNameID, err := + freelru.New[libpf.Address, libpf.AddressOrLineno](128, + libpf.Address.Hash32) + if err != nil { + return nil, err + } + + return &hotspotInstance{ + d: d, + rm: rm, + bias: bias, + addrToSymbol: addrToSymbol, + addrToMethod: addrToMethod, + addrToJITInfo: addrToJITInfo, + addrToStubNameID: addrToStubNameID, + prefixes: libpf.Set[lpm.Prefix]{}, + stubs: map[libpf.Address]StubRoutine{}, + }, nil +} + +// locateJvmciVMStructs attempts to heuristically locate the JVMCI VM structs by +// searching for references to the string `Klass_vtable_start_offset`. In all JVM +// versions >= 9.0, this corresponds to the first entry in the VM structs: +// +// nolint:lll +// https://github.com/openjdk/jdk/blob/jdk-9%2B181/hotspot/src/share/vm/jvmci/vmStructs_jvmci.cpp#L48 +// https://github.com/openjdk/jdk/blob/jdk-22%2B10/src/hotspot/share/jvmci/vmStructs_jvmci.cpp#L49 +func locateJvmciVMStructs(ef *pfelf.File) (libpf.Address, error) { + const maxDataReadSize = 1 * 1024 * 1024 // seen in practice: 192 KiB + const maxRodataReadSize = 4 * 1024 * 1024 // seen in practice: 753 KiB + + rodataSec := ef.Section(".rodata") + if rodataSec == nil { + return 0, errors.New("unable to find `.rodata` section") + } + + rodata, err := rodataSec.Data(maxRodataReadSize) + if err != nil { + return 0, err + } + + offs := bytes.Index(rodata, []byte("Klass_vtable_start_offset")) + if offs == -1 { + return 0, errors.New("unable to find string for heuristic") + } + + ptr := rodataSec.Addr + uint64(offs) + ptrEncoded := make([]byte, 8) + binary.LittleEndian.PutUint64(ptrEncoded, ptr) + + dataSec := ef.Section(".data") + if dataSec == nil { + return 0, errors.New("unable to find `.data` section") + } + + data, err := dataSec.Data(maxDataReadSize) + if err != nil { + return 0, err + } + + offs = bytes.Index(data, ptrEncoded) + if offs == -1 { + return 0, errors.New("unable to find string pointer") + } + + // 8 in the expression below is what we'd usually read from + // gHotSpotVMStructEntryFieldNameOffset. This value unfortunately lives in + // BSS, so we have no choice but to hard-code it. Fortunately enough this + // offset hasn't changed since at least JDK 9. + return libpf.Address(dataSec.Addr + uint64(offs) - 8), nil +} + +// forEachItem walks the given struct reflection fields recursively, and calls the visitor +// function for each field item with it's value and name. This does not work with recursively +// linked structs, and is intended currently to be ran with the Hotspot's vmStructs struct only. +// Catch-all fields are ignored and skipped. +func forEachItem(prefix string, t reflect.Value, visitor func(reflect.Value, string) error) error { + if prefix != "" { + prefix += "." + } + for i := 0; i < t.NumField(); i++ { + val := t.Field(i) + fieldName := prefix + t.Type().Field(i).Name + switch val.Kind() { + case reflect.Struct: + if err := forEachItem(fieldName, val, visitor); err != nil { + return err + } + case reflect.Uint, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + if err := visitor(val, fieldName); err != nil { + return err + } + case reflect.Map: + continue + default: + panic("unsupported type") + } + } + return nil +} + +// newVMData will read introspection data from remote process and return hotspotVMData +func (d *hotspotData) newVMData(rm remotememory.RemoteMemory, bias libpf.Address) ( + hotspotVMData, error) { + // Initialize the data with non-zero values so it's easy to check that + // everything got loaded (some fields will get zero values) + vmd := hotspotVMData{} + _ = forEachItem("", reflect.ValueOf(&vmd.vmStructs).Elem(), + func(item reflect.Value, _ string) error { + item.SetUint(^uint64(0)) + return nil + }) + + // First load the sizes of the classes + if err := vmd.parseIntrospection(&d.typePtrs, rm, bias); err != nil { + return vmd, err + } + // And the field offsets and static values + if err := vmd.parseIntrospection(&d.structPtrs, rm, bias); err != nil { + return vmd, err + } + if d.jvmciStructPtrs.base != 0 { + if err := vmd.parseIntrospection(&d.jvmciStructPtrs, rm, bias); err != nil { + return vmd, err + } + } + + // Failures after this point are permanent + vms := &vmd.vmStructs + var major, minor, security uint32 + if vms.JdkVersion.Current != ^libpf.Address(0) { + // JDK8 and earlier do not export all Abstract_VM_Version fields + jdkVersion := rm.Uint32(vms.JdkVersion.Current + bias) + major = jdkVersion & 0xff + minor = (jdkVersion >> 8) & 0xff + security = (jdkVersion >> 16) & 0xff + vms.AbstractVMVersion.MajorVersion = 0 + vms.AbstractVMVersion.MinorVersion = 0 + vms.AbstractVMVersion.SecurityVersion = 0 + } else { + // JDK22+ no longer exports JDK_Version + major = rm.Uint32(vms.AbstractVMVersion.MajorVersion + bias) + minor = rm.Uint32(vms.AbstractVMVersion.MinorVersion + bias) + security = rm.Uint32(vms.AbstractVMVersion.SecurityVersion + bias) + vms.JdkVersion.Current = 0 + } + build := rm.Uint32(vms.AbstractVMVersion.BuildNumber + bias) + + vmd.version = major<<24 + minor<<16 + security<<8 + build + vmd.versionStr = rm.StringPtr(vms.AbstractVMVersion.Release + bias) + + // Check minimum supported version. JDK 7-22 supported. + // Assume newer JDK works if the needed symbols are found. + if major < 7 { + vmd.err = fmt.Errorf("JVM version %d.%d.%d+%d (minimum is 7)", + major, minor, security, build) + return vmd, nil + } + + if vms.ConstantPool.SourceFileNameIndex != ^uint(0) { //nolint: gocritic + // JDK15: Use ConstantPool.SourceFileNameIndex + vms.InstanceKlass.SourceFileNameIndex = 0 + vms.InstanceKlass.SourceFileName = 0 + } else if vms.InstanceKlass.SourceFileNameIndex != ^uint(0) { + // JDK8-14: Use InstanceKlass.SourceFileNameIndex + vms.ConstantPool.SourceFileNameIndex = 0 + vms.InstanceKlass.SourceFileName = 0 + } else { + // JDK7: File name is direct Symbol*, adjust offsets with OopDesc due + // to the base pointer type changes + vms.InstanceKlass.SourceFileName += vms.OopDesc.Sizeof + if vms.Klass.Name != ^uint(0) { + vms.Klass.Name += vms.OopDesc.Sizeof + } + vms.ConstantPool.SourceFileNameIndex = 0 + vms.InstanceKlass.SourceFileNameIndex = 0 + } + + // JDK-8: Only single CodeCache Heap, some CodeBlob and Nmethod changes + if vms.CodeCache.Heap != ^libpf.Address(0) { + // Validate values that can be missing, fixup CompiledMethod offsets + vms.CodeCache.Heaps = 0 + vms.CodeCache.HighBound = 0 + vms.CodeCache.LowBound = 0 + vms.CompiledMethod.Sizeof = vms.Nmethod.Sizeof + vms.CompiledMethod.DeoptHandlerBegin = vms.Nmethod.DeoptimizeOffset + vms.CompiledMethod.Method = vms.Nmethod.Method + vms.CompiledMethod.ScopesDataBegin = 0 + } else { + // Reset the compatibility symbols not needed + vms.CodeCache.Heap = 0 + vms.Nmethod.Method = 0 + vms.Nmethod.DeoptimizeOffset = 0 + vms.Nmethod.ScopesDataOffset = 0 + } + + // JDK12+: Use Symbol.Length_and_refcount for Symbol.Length + if vms.Symbol.LengthAndRefcount != ^uint(0) { + // The symbol _length was merged and renamed to _symbol_length_and_refcount. + // Calculate the _length offset from it. + vms.Symbol.Length = vms.Symbol.LengthAndRefcount + 2 + } else { + // Reset the non-used symbols so the check below does not fail + vms.Symbol.LengthAndRefcount = 0 + } + + // JDK16: use GenericGrowableArray as in JDK9-15 case + if vms.GrowableArrayBase.Len != ^uint(0) { + vms.GenericGrowableArray.Len = vms.GrowableArrayBase.Len + } else { + // Reset the non-used symbols so the check below does not fail + vms.GrowableArrayBase.Len = 0 + } + + // JDK20+: UNSIGNED5 encoding change (since 20.0.15) + // https://github.com/openjdk/jdk20u/commit/8d3399bf5f354931b0c62d2ed8095e554be71680 + if vmd.version >= 0x1400000f { + vmd.unsigned5X = 1 + } + + // Check that all symbols got loaded from JVM introspection data + err := forEachItem("", reflect.ValueOf(&vmd.vmStructs).Elem(), + func(item reflect.Value, name string) error { + switch item.Kind() { + case reflect.Uint, reflect.Uint64, reflect.Uintptr: + if item.Uint() != ^uint64(0) { + return nil + } + case reflect.Uint32: + if item.Uint() != uint64(^uint32(0)) { + return nil + } + } + return fmt.Errorf("JVM symbol '%v' not found", name) + }) + if err != nil { + vmd.err = err + return vmd, nil + } + + if vms.Symbol.Sizeof > 32 { + // Additional sanity for Symbol.Sizeof which normally is + // just 8 byte or so. The getSymbol() hard codes the first read + // as 128 bytes and it needs to be more than this. + vmd.err = fmt.Errorf("JVM Symbol.Sizeof value %d", vms.Symbol.Sizeof) + return vmd, nil + } + + // Verify that all struct fields are within limits + structs := reflect.ValueOf(&vmd.vmStructs).Elem() + for i := 0; i < structs.NumField(); i++ { + klass := structs.Field(i) + sizeOf := klass.FieldByName("Sizeof") + if !sizeOf.IsValid() { + continue + } + maxOffset := sizeOf.Uint() + for j := 0; j < klass.NumField(); j++ { + field := klass.Field(j) + if field.Kind() == reflect.Map { + continue + } + + if field.Uint() > maxOffset { + vmd.err = fmt.Errorf("%s.%s offset %v is larger than class size %v", + structs.Type().Field(i).Name, + klass.Type().Field(j).Name, + field.Uint(), maxOffset) + return vmd, nil + } + } + } + + return vmd, nil +} + +func newHotspotData(filename string, ef *pfelf.File) (interpreter.Data, error) { + d := &hotspotData{} + err := d.structPtrs.resolveSymbols(ef, + []string{ + "gHotSpotVMStructs", + "gHotSpotVMStructEntryArrayStride", + "gHotSpotVMStructEntryTypeNameOffset", + "gHotSpotVMStructEntryFieldNameOffset", + "gHotSpotVMStructEntryOffsetOffset", + "gHotSpotVMStructEntryAddressOffset", + }) + if err != nil { + return nil, err + } + + err = d.typePtrs.resolveSymbols(ef, + []string{ + "gHotSpotVMTypes", + "gHotSpotVMTypeEntryArrayStride", + "gHotSpotVMTypeEntryTypeNameOffset", + "", + "gHotSpotVMTypeEntrySizeOffset", + "", + }) + if err != nil { + return nil, err + } + + if ptr, err := locateJvmciVMStructs(ef); err == nil { + // Everything except for the base pointer is identical. + d.jvmciStructPtrs = d.structPtrs + d.jvmciStructPtrs.base = ptr + d.jvmciStructPtrs.skipBaseDref = true + } else { + log.Warnf("%s: unable to read JVMCI VM structs: %v", filename, err) + } + + return d, nil +} diff --git a/interpreter/hotspot/demangle.go b/interpreter/hotspot/demangle.go new file mode 100644 index 00000000..ea359a89 --- /dev/null +++ b/interpreter/hotspot/demangle.go @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package hotspot + +import ( + "io" + "strings" +) + +// javaBaseTypes maps a basic type signature character to the full type name +var javaBaseTypes = map[byte]string{ + 'B': "byte", + 'C': "char", + 'D': "double", + 'F': "float", + 'I': "int", + 'J': "long", + 'S': "short", + 'V': "void", + 'Z': "boolean", +} + +// demangleJavaTypeSignature demangles a JavaTypeSignature +func demangleJavaTypeSignature(signature string, sb io.StringWriter) string { + var i, numArr int + for i = 0; i < len(signature) && signature[i] == '['; i++ { + numArr++ + } + if i >= len(signature) { + return "" + } + + typeChar := signature[i] + i++ + + if typeChar == 'L' { + end := strings.IndexByte(signature, ';') + if end < 0 { + return "" + } + _, _ = sb.WriteString(strings.ReplaceAll(signature[i:end], "/", ".")) + i = end + 1 + } else if typeStr, ok := javaBaseTypes[typeChar]; ok { + _, _ = sb.WriteString(typeStr) + } + + for numArr > 0 { + _, _ = sb.WriteString("[]") + numArr-- + } + + if len(signature) > i { + return signature[i:] + } + return "" +} + +// demangleJavaSignature demangles a JavaTypeSignature +func demangleJavaMethod(klass, method, signature string) string { + var sb strings.Builder + + // Name format is specified in + // - Java Virtual Machine Specification (JVMS) + // https://docs.oracle.com/javase/specs/jvms/se14/jvms14.pdf + // - Java Language Specification (JLS) + // https://docs.oracle.com/javase/specs/jls/se13/jls13.pdf + // + // see: JVMS §4.2 (name encoding), §4.3 (signature descriptors) + // JLS §13.1 (name encoding) + // + // Scala has additional internal transformations which are not + // well defined, and have changed between Scala versions. + + // Signature looks like "(argumentsSignatures)returnValueSignature" + // Check for the parenthesis first. + end := strings.IndexByte(signature, ')') + if end < 0 || signature[0] != '(' { + return "" + } + + left := demangleJavaTypeSignature(signature[end+1:], &sb) + if left != "" { + return "" + } + sb.WriteRune(' ') + sb.WriteString(strings.ReplaceAll(klass, "/", ".")) + sb.WriteRune('.') + sb.WriteString(method) + sb.WriteRune('(') + left = signature[1:end] + for left != "" { + left = demangleJavaTypeSignature(left, &sb) + if left == "" { + break + } + sb.WriteString(", ") + } + sb.WriteRune(')') + + return sb.String() +} diff --git a/interpreter/hotspot/demangle_test.go b/interpreter/hotspot/demangle_test.go new file mode 100644 index 00000000..3ac3a5e4 --- /dev/null +++ b/interpreter/hotspot/demangle_test.go @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package hotspot + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestJavaDemangling(t *testing.T) { + cases := []struct { + klass, method, signature, demangled string + }{ + {"java/lang/Object", "", "()V", + "void java.lang.Object.()"}, + {"java/lang/StringLatin1", "equals", "([B[B)Z", + "boolean java.lang.StringLatin1.equals(byte[], byte[])"}, + {"java/util/zip/ZipUtils", "CENSIZ", "([BI)J", + "long java.util.zip.ZipUtils.CENSIZ(byte[], int)"}, + {"java/util/regex/Pattern$BmpCharProperty", "match", + "(Ljava/util/regex/Matcher;ILjava/lang/CharSequence;)Z", + "boolean java.util.regex.Pattern$BmpCharProperty.match" + + "(java.util.regex.Matcher, int, java.lang.CharSequence)"}, + {"java/lang/AbstractStringBuilder", "appendChars", "(Ljava/lang/String;II)V", + "void java.lang.AbstractStringBuilder.appendChars" + + "(java.lang.String, int, int)"}, + {"foo/test", "bar", "([)J", "long foo.test.bar()"}, + } + + for _, c := range cases { + demangled := demangleJavaMethod(c.klass, c.method, c.signature) + assert.Equal(t, c.demangled, demangled) + } +} diff --git a/interpreter/hotspot/hotspot.go b/interpreter/hotspot/hotspot.go index bbb55ff9..013180d8 100644 --- a/interpreter/hotspot/hotspot.go +++ b/interpreter/hotspot/hotspot.go @@ -54,6 +54,7 @@ package hotspot // - nmethod._method -> CompiledMethod._method // - nmethod._deoptimize_offset -> CompiledMethod._deopt_handler_begin // - Modules introduced (but no introspection data to access those) +// - Abstract_VM_Version._vm_security_version exported // JDK9 - Tested ok // JDK10 - Tested ok // JDK11 - Reference, works @@ -72,6 +73,9 @@ package hotspot // JDK19 - Tested ok // - Excluding zero byte from UNSIGNED5 encoding output // JDK20 - Tested ok +// JDK21 - Tested ok +// - JDK_Version removed from introspection data +// JDK22 - Tested ok // // NOTE: Ahead-Of-Time compilation (AOT) is NOT SUPPORTED. The main complication is that, the AOT // ELF files are mapped directly to the program virtual space, and contain the code to execute. @@ -105,1781 +109,28 @@ package hotspot // will be trace hash collision due to other factors where the same issue would happen. import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "hash/fnv" - "io" - "reflect" "regexp" - "runtime" - "strings" - "sync/atomic" - "unsafe" - "github.com/elastic/otel-profiling-agent/host" + log "github.com/sirupsen/logrus" + "github.com/elastic/otel-profiling-agent/interpreter" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/freelru" - npsr "github.com/elastic/otel-profiling-agent/libpf/nopanicslicereader" - "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/elastic/otel-profiling-agent/libpf/process" - "github.com/elastic/otel-profiling-agent/libpf/remotememory" - "github.com/elastic/otel-profiling-agent/libpf/successfailurecounter" - "github.com/elastic/otel-profiling-agent/libpf/xsync" - "github.com/elastic/otel-profiling-agent/lpm" - "github.com/elastic/otel-profiling-agent/metrics" - "github.com/elastic/otel-profiling-agent/reporter" - "github.com/elastic/otel-profiling-agent/support" - log "github.com/sirupsen/logrus" - "go.uber.org/multierr" ) -// #include "../../support/ebpf/types.h" -// #include "../../support/ebpf/frametypes.h" -import "C" - var ( - invalidSymbolCharacters = regexp.MustCompile(`[^A-Za-z0-9_]+`) // The following regex is intended to match the HotSpot libjvm.so libjvmRegex = regexp.MustCompile(`.*/libjvm\.so`) - _ interpreter.Data = &hotspotData{} - _ interpreter.Instance = &hotspotInstance{} -) + // Match Java Hidden Class identifier and the replacement string + hiddenClassRegex = regexp.MustCompile(`\+0x[0-9a-f]{16}`) + hiddenClassMask = "+" -var ( // The FileID used for intrinsic stub frames hotspotStubsFileID = libpf.NewFileID(0x578b, 0x1d) -) - -// Constants for the JVM internals that have never changed -// nolint:golint,stylecheck,revive -const ConstMethod_has_linenumber_table = 0x0001 - -// unsigned5Decoder is a decoder for UNSIGNED5 based byte streams. -type unsigned5Decoder struct { - // r is the byte reader interface to read from - r io.ByteReader - - // x is the number of exclusion bytes in encoding (JDK20+) - x uint8 -} - -// getUint decodes one "standard" J2SE Pack200 UNSIGNED5 number -func (d *unsigned5Decoder) getUint() (uint32, error) { - const L = uint8(192) - x := d.x - r := d.r - - ch, err := r.ReadByte() - if err != nil { - return 0, err - } - if ch < x { - return 0, fmt.Errorf("byte %#x is in excluded range", ch) - } - - sum := uint32(ch - x) - for shift := 6; ch >= L && shift < 30; shift += 6 { - ch, err = r.ReadByte() - if err != nil { - return 0, err - } - if ch < x { - return 0, fmt.Errorf("byte %#x is in excluded range", ch) - } - sum += uint32(ch-x) << shift - } - return sum, nil -} - -// getSigned decodes one signed number -func (d *unsigned5Decoder) getSigned() (int32, error) { - val, err := d.getUint() - if err != nil { - return 0, err - } - return int32(val>>1) ^ -int32(val&1), nil -} - -// decodeLineTableEntry incrementally parses one line-table entry consisting of the source -// line number and a byte code index (BCI) from the decoder. The delta encoded line -// table format is specific to HotSpot VM which compresses the unpacked class file line -// tables during class loading. -func (d *unsigned5Decoder) decodeLineTableEntry(bci, line *uint32) error { - b, err := d.r.ReadByte() - if err != nil { - return fmt.Errorf("failed to read line table: %v", err) - } - switch b { - case 0x00: // End-of-Stream - return io.EOF - case 0xff: // Escape for long deltas - val, err := d.getSigned() - if err != nil { - return fmt.Errorf("failed to read byte code index delta: %v", err) - } - *bci += uint32(val) - val, err = d.getSigned() - if err != nil { - return fmt.Errorf("failed to read line number delta: %v", err) - } - *line += uint32(val) - default: // Short encoded delta - *bci += uint32(b >> 3) - *line += uint32(b & 7) - } - return nil -} - -// mapByteCodeIndexToLine decodes a line table to map a given Byte Code Index (BCI) -// to a line number -func (d *unsigned5Decoder) mapByteCodeIndexToLine(bci int32) libpf.SourceLineno { - // The line numbers array is a short array of 2-tuples [start_pc, line_number]. - // Not necessarily sorted. Encoded as delta-encoded numbers. - var curBci, curLine, bestBci, bestLine uint32 - - for d.decodeLineTableEntry(&curBci, &curLine) == nil { - if curBci == uint32(bci) { - return libpf.SourceLineno(curLine) - } - if curBci >= bestBci && curBci < uint32(bci) { - bestBci = curBci - bestLine = curLine - } - } - return libpf.SourceLineno(bestLine) -} - -// javaBaseTypes maps a basic type signature character to the full type name -var javaBaseTypes = map[byte]string{ - 'B': "byte", - 'C': "char", - 'D': "double", - 'F': "float", - 'I': "int", - 'J': "long", - 'S': "short", - 'V': "void", - 'Z': "boolean", -} - -// demangleJavaTypeSignature demangles a JavaTypeSignature -func demangleJavaTypeSignature(signature string, sb io.StringWriter) string { - var i, numArr int - for i = 0; i < len(signature) && signature[i] == '['; i++ { - numArr++ - } - if i >= len(signature) { - return "" - } - - typeChar := signature[i] - i++ - - if typeChar == 'L' { - end := strings.IndexByte(signature, ';') - if end < 0 { - return "" - } - _, _ = sb.WriteString(strings.ReplaceAll(signature[i:end], "/", ".")) - i = end + 1 - } else if typeStr, ok := javaBaseTypes[typeChar]; ok { - _, _ = sb.WriteString(typeStr) - } - - for numArr > 0 { - _, _ = sb.WriteString("[]") - numArr-- - } - - if len(signature) > i { - return signature[i:] - } - return "" -} - -// demangleJavaSignature demangles a JavaTypeSignature -func demangleJavaMethod(klass, method, signature string) string { - var sb strings.Builder - - // Name format is specified in - // - Java Virtual Machine Specification (JVMS) - // https://docs.oracle.com/javase/specs/jvms/se14/jvms14.pdf - // - Java Language Specification (JLS) - // https://docs.oracle.com/javase/specs/jls/se13/jls13.pdf - // - // see: JVMS §4.2 (name encoding), §4.3 (signature descriptors) - // JLS §13.1 (name encoding) - // - // Scala has additional internal transformations which are not - // well defined, and have changed between Scala versions. - - // Signature looks like "(argumentsSignatures)returnValueSignature" - // Check for the parenthesis first. - end := strings.IndexByte(signature, ')') - if end < 0 || signature[0] != '(' { - return "" - } - - left := demangleJavaTypeSignature(signature[end+1:], &sb) - if left != "" { - return "" - } - sb.WriteRune(' ') - sb.WriteString(strings.ReplaceAll(klass, "/", ".")) - sb.WriteRune('.') - sb.WriteString(method) - sb.WriteRune('(') - left = signature[1:end] - for left != "" { - left = demangleJavaTypeSignature(left, &sb) - if left == "" { - break - } - sb.WriteString(", ") - } - sb.WriteRune(')') - - return sb.String() -} - -// hotspotIntrospectionTable contains the resolved ELF symbols for an introspection table -type hotspotIntrospectionTable struct { - skipBaseDref bool - base, stride libpf.Address - typeOffset, fieldOffset libpf.Address - valueOffset, addressOffset libpf.Address -} - -// resolveSymbols resolves the ELF symbols of the introspection table -func (it *hotspotIntrospectionTable) resolveSymbols(ef *pfelf.File, symNames []string) error { - symVals := make([]libpf.Address, len(symNames)) - for i, s := range symNames { - if s == "" { - continue - } - addr, err := ef.LookupSymbolAddress(libpf.SymbolName(s)) - if err != nil { - return fmt.Errorf("symbol '%v' not found: %w", s, err) - } - symVals[i] = libpf.Address(addr) - } - - it.base, it.stride = symVals[0], symVals[1] - it.typeOffset, it.fieldOffset = symVals[2], symVals[3] - it.valueOffset, it.addressOffset = symVals[4], symVals[5] - return nil -} - -// hotspotVMData contains static information from one HotSpot build (libjvm.so). -// It mostly is limited to the introspection data (class sizes and field offsets) and -// the version. -type hotspotVMData struct { - // err is the permanent error if introspection data is not supported - err error - - // version is the JDK numeric version. Used in some places to make version specific - // adjustments to the unwinding process. - version uint32 - - // versionStr is the Hotspot build version string, and can contain additional - // details such as the distribution name and patch level. - versionStr string - - // unsigned5X is the number of exclusion bytes used in UNSIGNED5 encoding - unsigned5X uint8 - - // vmStructs reflects the HotSpot introspection data we want to extract - // from the runtime. It is filled using golang reflection (the struct and - // field names are used to find the data from the JVM). Thus the structs - // here are following the JVM naming convention. - // - // The comments of .Sizeof like ">xxx" are to signify the size range of the JVM - // C++ class and thus the expected value of .Sizeof member. This is mainly to - // indicate the classes for which uint8 is not enough to hold the offset values - // for the eBPF code. - vmStructs struct { - AbstractVMVersion struct { - Release libpf.Address `name:"_s_vm_release"` - BuildNumber libpf.Address `name:"_vm_build_number"` - } `name:"Abstract_VM_Version"` - JdkVersion struct { - Current libpf.Address `name:"_current"` - } `name:"JDK_Version"` - CodeBlob struct { - Sizeof uint - Name uint `name:"_name"` - FrameCompleteOffset uint `name:"_frame_complete_offset"` - FrameSize uint `name:"_frame_size"` - // JDK -8: offset, JDK 9+: pointers - CodeBegin uint `name:"_code_begin,_code_offset"` - CodeEnd uint `name:"_code_end,_data_offset"` - } - CodeCache struct { - Heap libpf.Address `name:"_heap"` - Heaps libpf.Address `name:"_heaps"` - HighBound libpf.Address `name:"_high_bound"` - LowBound libpf.Address `name:"_low_bound"` - } - CodeHeap struct { - Sizeof uint - Log2SegmentSize uint `name:"_log2_segment_size"` - Memory uint `name:"_memory"` - Segmap uint `name:"_segmap"` - } - CompiledMethod struct { // .Sizeof >200 - Sizeof uint - DeoptHandlerBegin uint `name:"_deopt_handler_begin"` - Method uint `name:"_method"` - ScopesDataBegin uint `name:"_scopes_data_begin"` - } - ConstantPool struct { - Sizeof uint - PoolHolder uint `name:"_pool_holder"` - SourceFileNameIndex uint `name:"_source_file_name_index"` - } `name:"ConstantPool,constantPoolOopDesc"` - ConstMethod struct { - Sizeof uint - Constants uint `name:"_constants"` - CodeSize uint `name:"_code_size"` - // JDK21+: ConstMethod._flags is now a struct with another _flags field - // https://github.com/openjdk/jdk/commit/316d303c1da550c9589c9be56b65650964e3886b - Flags uint `name:"_flags,_flags._flags"` - NameIndex uint `name:"_name_index"` - SignatureIndex uint `name:"_signature_index"` - } `name:"ConstMethod,constMethodOopDesc"` - // JDK9-15 structure - GenericGrowableArray struct { - Len uint `name:"_len"` - } - // JDK16 structure - GrowableArrayBase struct { - Len uint `name:"_len"` - } - GrowableArrayInt struct { - Sizeof uint - Data uint `name:"_data"` - } `name:"GrowableArray"` - HeapBlock struct { - Sizeof uint - } - InstanceKlass struct { // .Sizeof >400 - Sizeof uint - SourceFileNameIndex uint `name:"_source_file_name_index"` - SourceFileName uint `name:"_source_file_name"` // JDK -7 only - } `name:"InstanceKlass,instanceKlass"` - Klass struct { // .Sizeof >200 - Sizeof uint - Name uint `name:"_name"` - } - Method struct { - ConstMethod uint `name:"_constMethod"` - } `name:"Method,methodOopDesc"` - Nmethod struct { // .Sizeof >256 - Sizeof uint - CompileID uint `name:"_compile_id"` - MetadataOffset uint `name:"_metadata_offset,_oops_offset"` - ScopesPcsOffset uint `name:"_scopes_pcs_offset"` - DependenciesOffset uint `name:"_dependencies_offset"` - OrigPcOffset uint `name:"_orig_pc_offset"` - DeoptimizeOffset uint `name:"_deoptimize_offset"` - Method uint `name:"_method"` - ScopesDataOffset uint `name:"_scopes_data_offset"` // JDK -8 only - } `name:"nmethod"` - OopDesc struct { - Sizeof uint - } `name:"oopDesc"` - PcDesc struct { - Sizeof uint - PcOffset uint `name:"_pc_offset"` - ScopeDecodeOffset uint `name:"_scope_decode_offset"` - } - StubRoutines struct { - Sizeof uint // not needed, just keep this out of CatchAll - CatchAll map[string]libpf.Address `name:"*"` - } - Symbol struct { - Sizeof uint - Body uint `name:"_body"` - Length uint `name:"_length"` - LengthAndRefcount uint `name:"_length_and_refcount"` - } - VirtualSpace struct { - HighBoundary uint `name:"_high_boundary"` - LowBoundary uint `name:"_low_boundary"` - } - } -} - -type hotspotData struct { - // ELF symbols needed for the introspection data - typePtrs, structPtrs, jvmciStructPtrs hotspotIntrospectionTable - - // Once protected hotspotVMData - xsync.Once[hotspotVMData] -} - -// hotspotMethod contains symbolization information for one Java method. It caches -// information from Hotspot class Method, the connected class ConstMethod, and -// chasing the pointers in the ConstantPool and other dynamic parts. -type hotspotMethod struct { - sourceFileName string - objectID libpf.FileID - methodName string - bytecodeSize uint16 - startLineNo uint16 - lineTable []byte - bciSeen libpf.Set[uint16] -} - -// hotspotJITInfo contains symbolization and debug information for one JIT compiled -// method or JVM internal stub/function. The main JVM class it extracts the data -// from is class nmethod, and it caches the connected class Method and inlining info. -type hotspotJITInfo struct { - // compileID is the global unique id (running number) for this code blob - compileID uint32 - // method contains the Java method data for this JITted instance of it - method *hotspotMethod - // scopesPcs contains PC (RIP) to inlining scope mapping information - scopesPcs []byte - // scopesData contains information about inlined scopes - scopesData []byte - // metadata is the object addresses for the scopes data - metadata []byte -} - -// hotspotInstance contains information about one running HotSpot instance (pid) -type hotspotInstance struct { - interpreter.InstanceStubs - - // Hotspot symbolization metrics - successCount atomic.Uint64 - failCount atomic.Uint64 - - // d is the interpreter data from jvm.so (shared between processes) - d *hotspotData - - // rm is used to access the remote process memory - rm remotememory.RemoteMemory - - // bias is the ELF DSO load bias - bias libpf.Address - - // prefixes is list of LPM prefixes added to ebpf maps (to be cleaned up) - prefixes libpf.Set[lpm.Prefix] - - // addrToSymbol maps a JVM class Symbol address to it's string value - addrToSymbol *freelru.LRU[libpf.Address, string] - - // addrToMethod maps a JVM class Method to a hotspotMethod which caches - // the needed data from it. - addrToMethod *freelru.LRU[libpf.Address, *hotspotMethod] - - // addrToJitInfo maps a JVM class nmethod to a hotspotJITInfo which caches - // the needed data from it. - addrToJITInfo *freelru.LRU[libpf.Address, *hotspotJITInfo] - - // addrToStubNameID maps a stub name to its unique identifier. - addrToStubNameID *freelru.LRU[libpf.Address, libpf.AddressOrLineno] - - // mainMappingsInserted stores whether the heap areas and proc data are already populated. - mainMappingsInserted bool - - // heapAreas stores the top-level JIT areas based on the Java heaps. - heapAreas []jitArea - - // stubs stores all known stub routine regions. - stubs map[libpf.Address]StubRoutine -} - -// heapInfo contains info about all HotSpot heaps. -type heapInfo struct { - segmentShift uint32 - ranges []heapRange -} - -// heapRange contains info for an individual heap. -type heapRange struct { - codeStart, codeEnd libpf.Address - segmapStart, segmapEnd libpf.Address -} - -type jitArea struct { - start, end libpf.Address - codeStart libpf.Address - tsid uint64 -} - -func (d *hotspotInstance) GetAndResetMetrics() ([]metrics.Metric, error) { - addrToSymbolStats := d.addrToSymbol.GetAndResetStatistics() - addrToMethodStats := d.addrToMethod.GetAndResetStatistics() - addrToJITInfoStats := d.addrToJITInfo.GetAndResetStatistics() - addrToStubNameIDStats := d.addrToStubNameID.GetAndResetStatistics() - - return []metrics.Metric{ - { - ID: metrics.IDHotspotSymbolizationSuccesses, - Value: metrics.MetricValue(d.successCount.Swap(0)), - }, - { - ID: metrics.IDHotspotSymbolizationFailures, - Value: metrics.MetricValue(d.failCount.Swap(0)), - }, - { - ID: metrics.IDHotspotAddrToSymbolHit, - Value: metrics.MetricValue(addrToSymbolStats.Hit), - }, - { - ID: metrics.IDHotspotAddrToSymbolMiss, - Value: metrics.MetricValue(addrToSymbolStats.Miss), - }, - { - ID: metrics.IDHotspotAddrToSymbolAdd, - Value: metrics.MetricValue(addrToSymbolStats.Added), - }, - { - ID: metrics.IDHotspotAddrToSymbolDel, - Value: metrics.MetricValue(addrToSymbolStats.Deleted), - }, - { - ID: metrics.IDHotspotAddrToMethodHit, - Value: metrics.MetricValue(addrToMethodStats.Hit), - }, - { - ID: metrics.IDHotspotAddrToMethodMiss, - Value: metrics.MetricValue(addrToMethodStats.Miss), - }, - { - ID: metrics.IDHotspotAddrToMethodAdd, - Value: metrics.MetricValue(addrToMethodStats.Added), - }, - { - ID: metrics.IDHotspotAddrToMethodDel, - Value: metrics.MetricValue(addrToMethodStats.Deleted), - }, - { - ID: metrics.IDHotspotAddrToJITInfoHit, - Value: metrics.MetricValue(addrToJITInfoStats.Hit), - }, - { - ID: metrics.IDHotspotAddrToJITInfoMiss, - Value: metrics.MetricValue(addrToJITInfoStats.Miss), - }, - { - ID: metrics.IDHotspotAddrToJITInfoAdd, - Value: metrics.MetricValue(addrToJITInfoStats.Added), - }, - { - ID: metrics.IDHotspotAddrToJITInfoDel, - Value: metrics.MetricValue(addrToJITInfoStats.Deleted), - }, - { - ID: metrics.IDHotspotAddrToStubNameIDHit, - Value: metrics.MetricValue(addrToStubNameIDStats.Hit), - }, - { - ID: metrics.IDHotspotAddrToStubNameIDMiss, - Value: metrics.MetricValue(addrToStubNameIDStats.Miss), - }, - { - ID: metrics.IDHotspotAddrToStubNameIDAdd, - Value: metrics.MetricValue(addrToStubNameIDStats.Added), - }, - { - ID: metrics.IDHotspotAddrToStubNameIDDel, - Value: metrics.MetricValue(addrToStubNameIDStats.Deleted), - }, - }, nil -} - -// getSymbol extracts a class Symbol value from the given address in the target JVM process -func (d *hotspotInstance) getSymbol(addr libpf.Address) string { - if value, ok := d.addrToSymbol.Get(addr); ok { - return value - } - vms := d.d.Get().vmStructs - - // Read the symbol length and readahead bytes in attempt to avoid second - // system call to read the target string. 128 is chosen arbitrarily as "hopefully - // good enough"; this value can be increased if it turns out to be necessary. - var buf [128]byte - if d.rm.Read(addr, buf[:]) != nil { - return "" - } - symLen := npsr.Uint16(buf[:], vms.Symbol.Length) - if symLen == 0 { - return "" - } - - // Always allocate the string separately so it does not hold the backing - // buffer that might be larger than needed - tmp := make([]byte, symLen) - copy(tmp, buf[vms.Symbol.Body:]) - if vms.Symbol.Body+uint(symLen) > uint(len(buf)) { - prefixLen := uint(len(buf[vms.Symbol.Body:])) - if d.rm.Read(addr+libpf.Address(vms.Symbol.Body+prefixLen), tmp[prefixLen:]) != nil { - return "" - } - } - s := string(tmp) - if !libpf.IsValidString(s) { - log.Debugf("Extracted Hotspot symbol is invalid at 0x%x '%v'", addr, []byte(s)) - return "" - } - d.addrToSymbol.Add(addr, s) - return s -} - -// getPoolSymbol reads a class ConstantPool value from given index, and reads the -// symbol value it is referencing -func (d *hotspotInstance) getPoolSymbol(addr libpf.Address, ndx uint16) string { - // Zero index is not valid - if ndx == 0 { - return "" - } - - vms := &d.d.Get().vmStructs - offs := libpf.Address(vms.ConstantPool.Sizeof) + 8*libpf.Address(ndx) - cpoolVal := d.rm.Ptr(addr + offs) - // The lowest bit is reserved by JVM to indicate if the value has been - // resolved or not. The values see should be always resolved. - // Just ignore the bit as it's meaning has changed between JDK versions. - return d.getSymbol(cpoolVal &^ 1) -} - -// getStubNameID read the stub name from the code blob at given address and generates a ID. -func (d *hotspotInstance) getStubNameID(symbolizer interpreter.Symbolizer, ripOrBci int32, - addr libpf.Address, _ uint32) (libpf.AddressOrLineno, error) { - if value, ok := d.addrToStubNameID.Get(addr); ok { - return value, nil - } - vms := &d.d.Get().vmStructs - constStubNameAddr := d.rm.Ptr(addr + libpf.Address(vms.CodeBlob.Name)) - stubName := d.rm.String(constStubNameAddr) - - a := d.rm.Ptr(addr+libpf.Address(vms.CodeBlob.CodeBegin)) + libpf.Address(ripOrBci) - for _, stub := range d.stubs { - if stub.start <= a && stub.end > a { - stubName = fmt.Sprintf("%s [%s]", stubName, stub.name) - break - } - } - - h := fnv.New128a() - _, _ = h.Write([]byte(stubName)) - nameHash := h.Sum(nil) - stubID := libpf.AddressOrLineno(npsr.Uint64(nameHash, 0)) - - symbolizer.FrameMetadata(hotspotStubsFileID, stubID, 0, 0, stubName, "") - - d.addrToStubNameID.Add(addr, stubID) - return stubID, nil -} - -// getMethod reads and returns the interesting data from "class Method" at given address -func (d *hotspotInstance) getMethod(addr libpf.Address, _ uint32) (*hotspotMethod, error) { - if value, ok := d.addrToMethod.Get(addr); ok { - return value, nil - } - vms := &d.d.Get().vmStructs - constMethodAddr := d.rm.Ptr(addr + libpf.Address(vms.Method.ConstMethod)) - constMethod := make([]byte, vms.ConstMethod.Sizeof) - if err := d.rm.Read(constMethodAddr, constMethod); err != nil { - return nil, fmt.Errorf("invalid ConstMethod ptr: %v", err) - } - - cpoolAddr := npsr.Ptr(constMethod, vms.ConstMethod.Constants) - cpool := make([]byte, vms.ConstantPool.Sizeof) - if err := d.rm.Read(cpoolAddr, cpool); err != nil { - return nil, fmt.Errorf("invalid CostantPool ptr: %v", err) - } - - instanceKlassAddr := npsr.Ptr(cpool, vms.ConstantPool.PoolHolder) - instanceKlass := make([]byte, vms.InstanceKlass.Sizeof) - if err := d.rm.Read(instanceKlassAddr, instanceKlass); err != nil { - return nil, fmt.Errorf("invalid ConstantPool ptr: %v", err) - } - - var sourceFileName string - if vms.ConstantPool.SourceFileNameIndex != 0 { - // JDK15 - sourceFileName = d.getPoolSymbol(cpoolAddr, - npsr.Uint16(cpool, vms.ConstantPool.SourceFileNameIndex)) - } else if vms.InstanceKlass.SourceFileNameIndex != 0 { - // JDK8-14 - sourceFileName = d.getPoolSymbol(cpoolAddr, - npsr.Uint16(instanceKlass, vms.InstanceKlass.SourceFileNameIndex)) - } else { - // JDK7 - sourceFileName = d.getSymbol( - npsr.Ptr(instanceKlass, vms.InstanceKlass.SourceFileName)) - } - if sourceFileName == "" { - // Java and Scala can autogenerate lambdas which have no source - // information available. The HotSpot VM backtraces displays - // "Unknown Source" as the filename for these. - sourceFileName = interpreter.UnknownSourceFile - } - - klassName := d.getSymbol(npsr.Ptr(instanceKlass, vms.Klass.Name)) - methodName := d.getPoolSymbol(cpoolAddr, npsr.Uint16(constMethod, - vms.ConstMethod.NameIndex)) - signature := d.getPoolSymbol(cpoolAddr, npsr.Uint16(constMethod, - vms.ConstMethod.SignatureIndex)) - - // Synthesize a FileID that is unique to this Class/Method that can be - // used as "CodeObjectID" value in the trace as frames FileID. - // Keep the sourcefileName there to start with, and add klass name, method - // name, byte code and the JVM presentation of the source line table. - h := fnv.New128a() - _, _ = h.Write([]byte(sourceFileName)) - _, _ = h.Write([]byte(klassName)) - _, _ = h.Write([]byte(methodName)) - _, _ = h.Write([]byte(signature)) - - // Read the byte code for CodeObjectID - bytecodeSize := npsr.Uint16(constMethod, vms.ConstMethod.CodeSize) - byteCode := make([]byte, bytecodeSize) - err := d.rm.Read(constMethodAddr+libpf.Address(vms.ConstMethod.Sizeof), byteCode) - if err != nil { - return nil, fmt.Errorf("invalid ByteCode ptr: %v", err) - } - - _, _ = h.Write(byteCode) - - var lineTable []byte - startLine := ^uint32(0) - // NOTE: ConstMethod.Flags is either u16 or u32 depending on JVM version. Since we - // only care about flags in the first byte and only operate on little endian - // architectures we can get away with reading it as u8 either way. - if npsr.Uint8(constMethod, vms.ConstMethod.Flags)&ConstMethod_has_linenumber_table != 0 { - // The line number table size is not known ahead of time. It is delta compressed, - // so read it once using buffered read to capture it fully. Get also the smallest - // line number present as the function start line number - this is not perfect - // as it's the first line for which code was generated. Usually one or few lines - // after the actual function definition line. The Byte Code Index (BCI) is just - // used for additional method ID hash input. - var pcLineEntry [4]byte - var curBci, curLine uint32 - err = nil - r := d.rm.Reader(constMethodAddr+libpf.Address(vms.ConstMethod.Sizeof)+ - libpf.Address(bytecodeSize), 256) - dec := d.d.newUnsigned5Decoder(r) - for err == nil { - if curLine > 0 && curLine < startLine { - startLine = curLine - } - err = dec.decodeLineTableEntry(&curBci, &curLine) - - // The BCI and line numbers are read from the target memory in the custom - // format, but the .class file LineNumberTable is big-endian encoded - // { - // u2 start_pc, line_number; - // } line_number_table[line_number_table_length] - // - // This hashes the line_number_table in .class file format, so if we - // ever start indexing .class/.java files to match methods to real source - // file IDs, we can produce the hash in the indexer without additional - // transformations needed. - binary.BigEndian.PutUint16(pcLineEntry[0:2], uint16(curBci)) - binary.BigEndian.PutUint16(pcLineEntry[2:4], uint16(curLine)) - _, _ = h.Write(pcLineEntry[:]) - } - - // If EOF encountered, the table was processed successfully. - if err == io.EOF { - lineTable = r.GetBuffer() - } - } - if startLine == ^uint32(0) { - startLine = 0 - } - // Finalize CodeObjectID generation - objectID, err := libpf.FileIDFromBytes(h.Sum(nil)) - if err != nil { - return nil, fmt.Errorf("failed to create a code object ID: %v", err) - } - sym := &hotspotMethod{ - sourceFileName: sourceFileName, - objectID: objectID, - methodName: demangleJavaMethod(klassName, methodName, signature), - bytecodeSize: bytecodeSize, - lineTable: lineTable, - startLineNo: uint16(startLine), - bciSeen: make(libpf.Set[uint16]), - } - d.addrToMethod.Add(addr, sym) - return sym, nil -} - -// getJITInfo reads and returns the interesting data from "class nmethod" at given address -func (d *hotspotInstance) getJITInfo(addr libpf.Address, - addrCheck uint32) (*hotspotJITInfo, error) { - if jit, ok := d.addrToJITInfo.Get(addr); ok { - if jit.compileID == addrCheck { - return jit, nil - } - } - vms := &d.d.Get().vmStructs - - // Each JIT-ted function is contained in a "class nmethod" - // (derived from CompiledMethod and CodeBlob). - // - // Layout of important bits in such 'class nmethod' pointer is: - // [class CodeBlob fields] - // [class CompiledMethod fields] - // [class nmethod fields] - // ... - // [JIT_code] @ this + CodeBlob._code_start - // ... - // [metadata] @ this + nmethod._metadata_offset \ these three - // [scopes_data] @ CompiledMethod._scopes_data_begin | arrays we need - // [scopes_pcs] @ this + nmethod._scopes_pcs_offset / for inlining info - // [dependencies] @ this + nmethod._dependencies_offset - // ... - // - // see: src/hotspot/share/code/compiledMethod.hpp - // src/hotspot/share/code/nmethod.hpp - // - // The scopes_pcs is a look up table to map RIP to scope_data. scopes_data - // is a list of descriptors that lists the method and it's Byte Code Index (BCI) - // activations for the scope. Finally the metadata is the array that - // maps scope_data method indices to real "class Method*". - nmethod := make([]byte, vms.Nmethod.Sizeof) - if err := d.rm.Read(addr, nmethod); err != nil { - return nil, fmt.Errorf("invalid nmethod ptr: %v", err) - } - - // Since the Java VM might decide recompile or free the JITted nmethods - // we use the nmethod._compile_id (global running number to identify JIT - // method) to uniquely identify that we are using the right data here - // vs. when the pointer was captured by eBPF. - compileID := npsr.Uint32(nmethod, vms.Nmethod.CompileID) - if compileID != addrCheck { - return nil, fmt.Errorf("JIT info evicted since eBPF snapshot") - } - - // Finally read the associated debug information for this method - var scopesOff libpf.Address - metadataOff := npsr.PtrDiff32(nmethod, vms.Nmethod.MetadataOffset) - if vms.CompiledMethod.ScopesDataBegin != 0 { - scopesOff = npsr.Ptr(nmethod, vms.CompiledMethod.ScopesDataBegin) - addr - } else { - scopesOff = npsr.PtrDiff32(nmethod, vms.Nmethod.ScopesDataOffset) - } - scopesPcsOff := npsr.PtrDiff32(nmethod, vms.Nmethod.ScopesPcsOffset) - depsOff := npsr.PtrDiff32(nmethod, vms.Nmethod.DependenciesOffset) - - if metadataOff > scopesOff || scopesOff > scopesPcsOff || scopesPcsOff > depsOff { - return nil, fmt.Errorf("unexpected nmethod layout: %v <= %v <= %v <= %v", - metadataOff, scopesOff, scopesPcsOff, depsOff) - } - - method, err := d.getMethod(npsr.Ptr(nmethod, vms.CompiledMethod.Method), 0) - if err != nil { - return nil, fmt.Errorf("failed to get JIT Method: %v", err) - } - - buf := make([]byte, depsOff-metadataOff) - if err := d.rm.Read(addr+metadataOff, buf); err != nil { - return nil, fmt.Errorf("invalid nmethod metadata: %v", err) - } - - // Buffer is read starting from metadataOff, so adjust accordingly - scopesOff -= metadataOff - scopesPcsOff -= metadataOff - - jit := &hotspotJITInfo{ - compileID: compileID, - method: method, - metadata: buf[0:scopesOff], - scopesData: buf[scopesOff:scopesPcsOff], - scopesPcs: buf[scopesPcsOff:], - } - - d.addrToJITInfo.Add(addr, jit) - return jit, nil -} - -// Symbolize generates symbolization information for given hotspot method and -// a Byte Code Index (BCI) -func (m *hotspotMethod) symbolize(symbolizer interpreter.Symbolizer, bci int32, - ii *hotspotInstance, trace *libpf.Trace) error { - // Make sure the BCI is within the method range - if bci < 0 || bci >= int32(m.bytecodeSize) { - bci = 0 - } - trace.AppendFrame(libpf.HotSpotFrame, m.objectID, libpf.AddressOrLineno(bci)) - - // Check if this is already symbolized - if _, ok := m.bciSeen[uint16(bci)]; ok { - return nil - } - - dec := ii.d.newUnsigned5Decoder(bytes.NewReader(m.lineTable)) - lineNo := dec.mapByteCodeIndexToLine(bci) - functionOffset := uint32(0) - if lineNo > libpf.SourceLineno(m.startLineNo) { - functionOffset = uint32(lineNo) - uint32(m.startLineNo) - } - - symbolizer.FrameMetadata(m.objectID, - libpf.AddressOrLineno(bci), lineNo, functionOffset, - m.methodName, m.sourceFileName) - - // FIXME: The above FrameMetadata call might fail, but we have no idea of it - // due to the requests being queued and send attempts being done asynchronously. - // Until the reporting API gets a way to notify failures, just assume it worked. - m.bciSeen[uint16(bci)] = libpf.Void{} - - log.Debugf("[%d] [%x] %v+%v at %v:%v", len(trace.FrameTypes), - m.objectID, - m.methodName, functionOffset, - m.sourceFileName, lineNo) - - return nil -} - -// Symbolize parses JIT method inlining data and fills in symbolization information -// for each inlined method for given RIP. -func (ji *hotspotJITInfo) symbolize(symbolizer interpreter.Symbolizer, ripDelta int32, - ii *hotspotInstance, trace *libpf.Trace) error { - // nolint:lll - // Unfortunately the data structures read here are not well documented in the JVM - // source, but for reference implementation you can look: - // https://hg.openjdk.java.net/jdk-updates/jdk14u/file/default/src/java.base/solaris/native/libjvm_db/libjvm_db.c - // Search for the functions: get_real_pc(), pc_desc_at(), scope_desc_at() and scopeDesc_chain(). - - // Conceptually, the JIT inlining information is kept in scopes_data as a linked - // list of [ nextScope, methodIndex, byteCodeOffset ] triplets. The innermost scope - // is resolved by looking it up from a table based on RIP (delta from function start). - - // Loop through the scopes_pcs table to map rip_delta to proper scope. - // It seems that the first entry is usually [-1, ] pair, - // so the below loop needs to handle negative pc_deltas correctly. - bestPCDelta := int32(-2) - scopeOff := uint32(0) - vms := &ii.d.Get().vmStructs - for i := uint(0); i < uint(len(ji.scopesPcs)); i += vms.PcDesc.Sizeof { - pcDelta := int32(npsr.Uint32(ji.scopesPcs, i+vms.PcDesc.PcOffset)) - if pcDelta >= bestPCDelta && pcDelta <= ripDelta { - bestPCDelta = pcDelta - scopeOff = npsr.Uint32(ji.scopesPcs, i+vms.PcDesc.ScopeDecodeOffset) - if pcDelta == ripDelta { - // Exact match of RIP to PC. Stop search. - // We could also record here that the symbolization - // result is "accurate" - break - } - } - } - - if scopeOff == 0 { - // It is possible that there is no debug info, or no scope information, - // for the given RIP. In this case we can provide the method name - // from the metadata. - return ji.method.symbolize(symbolizer, 0, ii, trace) - } - - // Found scope data. Expand the inlined scope information from it. - var err error - maxScopeOff := uint32(len(ji.scopesData)) - for scopeOff != 0 && scopeOff < maxScopeOff { - // Keep track of the current scope offset, and use it as the next maximum - // offset. This makes sure the scope offsets decrease monotonically and - // this loop terminates. It has been verified empirically for this assumption - // to hold true, and it would be also very difficult for the JVM to generate - // forward references due to the variable length encoding used. - maxScopeOff = scopeOff - - // The scope data is three unsigned5 encoded integers - r := ii.d.newUnsigned5Decoder(bytes.NewReader(ji.scopesData[scopeOff:])) - scopeOff, err = r.getUint() - if err != nil { - return fmt.Errorf("failed to read next scope offset: %v", err) - } - methodIdx, err := r.getUint() - if err != nil { - return fmt.Errorf("failed to read method index: %v", err) - } - byteCodeIndex, err := r.getUint() - if err != nil { - return fmt.Errorf("failed to read bytecode index: %v", err) - } - - if byteCodeIndex > 0 { - // Analysis shows that the BCI stored in the scopes data - // is one larger than the BCI used by Interpreter or by - // the lookup tables. This is probably a bug in the JVM. - byteCodeIndex-- - } - - if methodIdx != 0 { - methodPtr := npsr.Ptr(ji.metadata, 8*uint(methodIdx-1)) - method, err := ii.getMethod(methodPtr, 0) - if err != nil { - return err - } - err = method.symbolize(symbolizer, int32(byteCodeIndex), ii, trace) - if err != nil { - return err - } - } - } - return nil -} - -// Detach removes all information regarding a given process from the eBPF maps. -func (d *hotspotInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error { - var err error - if d.mainMappingsInserted { - err = ebpf.DeleteProcData(libpf.HotSpot, pid) - } - - for prefix := range d.prefixes { - if err2 := ebpf.DeletePidInterpreterMapping(pid, prefix); err2 != nil { - err = multierr.Append(err, - fmt.Errorf("failed to remove page 0x%x/%d: %v", - prefix.Key, prefix.Length, err2)) - } - } - - if err != nil { - return fmt.Errorf("failed to detach hotspotInstance from PID %d: %v", - pid, err) - } - return nil -} - -// gatherHeapInfo collects information about HotSpot heaps. -func (d *hotspotInstance) gatherHeapInfo(vmd *hotspotVMData) (*heapInfo, error) { - info := &heapInfo{} - - // Determine the location of heap pointers - var heapPtrAddr libpf.Address - var numHeaps uint32 - - vms := &vmd.vmStructs - rm := d.rm - if vms.CodeCache.Heap != 0 { - // JDK -8: one fixed CodeHeap through fixed pointer - numHeaps = 1 - heapPtrAddr = vms.CodeCache.Heap + d.bias - } else { - // JDK 9-: CodeHeap through _heaps array - heaps := make([]byte, vms.GrowableArrayInt.Sizeof) - if err := rm.Read(rm.Ptr(vms.CodeCache.Heaps+d.bias), heaps); err != nil { - return nil, fmt.Errorf("fail to read heap array: %v", err) - } - // Read numHeaps - numHeaps = npsr.Uint32(heaps, vms.GenericGrowableArray.Len) - - heapPtrAddr = npsr.Ptr(heaps, vms.GrowableArrayInt.Data) - if numHeaps == 0 || heapPtrAddr == 0 { - // The heaps are not yet initialized - return nil, nil - } - } - - // Get and sanity check the number of heaps - if numHeaps < 1 || numHeaps > 16 { - return nil, fmt.Errorf("bad hotspot heap count (%v)", numHeaps) - } - - // Extract the heap pointers - heap := make([]byte, vms.CodeHeap.Sizeof) - heapPtrs := make([]byte, 8*numHeaps) - if err := rm.Read(heapPtrAddr, heapPtrs); err != nil { - return nil, fmt.Errorf("fail to read heap array values: %v", err) - } - - // Extract each heap structure individually - for ndx := uint32(0); ndx < numHeaps; ndx++ { - heapPtr := npsr.Ptr(heapPtrs, uint(ndx*8)) - if heapPtr == 0 { - // JVM is not initialized yet. Retry later. - return nil, nil - } - if err := rm.Read(heapPtr, heap); err != nil { - return nil, fmt.Errorf("fail to read heap pointer %d: %v", ndx, err) - } - - // The segment shift is same for all heaps. So record it for the process only. - info.segmentShift = npsr.Uint32(heap, vms.CodeHeap.Log2SegmentSize) - - // The LowBoundary and HighBoundary describe the mapping that was reserved - // with mmap(PROT_NONE). The actual mapping that is committed memory is in - // VirtualSpace.{Low,High}. However, since we are just following pointers we - // really care about the maximum values which do not change. - rng := heapRange{ - codeStart: npsr.Ptr(heap, vms.CodeHeap.Memory+vms.VirtualSpace.LowBoundary), - codeEnd: npsr.Ptr(heap, vms.CodeHeap.Memory+vms.VirtualSpace.HighBoundary), - segmapStart: npsr.Ptr(heap, vms.CodeHeap.Segmap+vms.VirtualSpace.LowBoundary), - segmapEnd: npsr.Ptr(heap, vms.CodeHeap.Segmap+vms.VirtualSpace.HighBoundary), - } - - // Hook the memory area for HotSpot unwinder - if rng.codeStart == 0 || rng.codeEnd == 0 { - return nil, nil - } - - info.ranges = append(info.ranges, rng) - } - - return info, nil -} - -// addJitArea inserts an entry into the PID<->interpreter BPF map. -func (d *hotspotInstance) addJitArea(ebpf interpreter.EbpfHandler, - pid libpf.PID, area jitArea) error { - prefixes, err := lpm.CalculatePrefixList(uint64(area.start), uint64(area.end)) - if err != nil { - return fmt.Errorf("LPM prefix calculation error for %x-%x", area.start, area.end) - } - - for _, prefix := range prefixes { - if _, exists := d.prefixes[prefix]; exists { - continue - } - - if err = ebpf.UpdatePidInterpreterMapping(pid, prefix, - support.ProgUnwindHotspot, host.FileID(area.tsid), - uint64(area.codeStart)); err != nil { - return fmt.Errorf( - "failed to insert LPM entry for pid %d, page 0x%x/%d: %v", - pid, prefix.Key, prefix.Length, err) - } - - d.prefixes[prefix] = libpf.Void{} - } - - log.Debugf("HotSpot jitArea: pid: %d, code %x-%x tsid: %x (%d tries)", - pid, area.start, area.end, area.tsid, len(prefixes)) - - return nil -} - -// populateMainMappings populates all important BPF map entries that are available -// immediately after interpreter startup (once VM structs becomes available). This -// allows the BPF code to start unwinding even if some more detailed information -// about e.g. stub routines is not yet available. -func (d *hotspotInstance) populateMainMappings(vmd *hotspotVMData, - ebpf interpreter.EbpfHandler, pid libpf.PID) error { - if d.mainMappingsInserted { - // Already populated: nothing to do here. - return nil - } - - heap, err := d.gatherHeapInfo(vmd) - if err != nil { - return err - } - if heap == nil || len(heap.ranges) == 0 { - return nil - } - - // Construct and insert heap areas. - for _, rng := range heap.ranges { - tsid := (uint64(rng.segmapStart) & support.HSTSIDSegMapMask) << support.HSTSIDSegMapBit - - area := jitArea{ - start: rng.codeStart, - end: rng.codeEnd, - codeStart: rng.codeStart, - tsid: tsid, - } - - if err = d.addJitArea(ebpf, pid, area); err != nil { - return err - } - - d.heapAreas = append(d.heapAreas, area) - } - - // Set up the main eBPF info structure. - vms := &vmd.vmStructs - procInfo := C.HotspotProcInfo{ - compiledmethod_deopt_handler: C.u16(vms.CompiledMethod.DeoptHandlerBegin), - nmethod_compileid: C.u16(vms.Nmethod.CompileID), - nmethod_orig_pc_offset: C.u16(vms.Nmethod.OrigPcOffset), - codeblob_name: C.u8(vms.CodeBlob.Name), - codeblob_codestart: C.u8(vms.CodeBlob.CodeBegin), - codeblob_codeend: C.u8(vms.CodeBlob.CodeEnd), - codeblob_framecomplete: C.u8(vms.CodeBlob.FrameCompleteOffset), - codeblob_framesize: C.u8(vms.CodeBlob.FrameSize), - cmethod_size: C.u8(vms.ConstMethod.Sizeof), - heapblock_size: C.u8(vms.HeapBlock.Sizeof), - method_constmethod: C.u8(vms.Method.ConstMethod), - jvm_version: C.u8(vmd.version >> 24), - segment_shift: C.u8(heap.segmentShift), - } - - if vms.CodeCache.LowBound == 0 { - // JDK-8 has only one heap, use its bounds - procInfo.codecache_start = C.u64(heap.ranges[0].codeStart) - procInfo.codecache_end = C.u64(heap.ranges[0].codeEnd) - } else { - // JDK9+ the VM tracks it separately - procInfo.codecache_start = C.u64(d.rm.Ptr(vms.CodeCache.LowBound + d.bias)) - procInfo.codecache_end = C.u64(d.rm.Ptr(vms.CodeCache.HighBound + d.bias)) - } - - if err = ebpf.UpdateProcData(libpf.HotSpot, pid, unsafe.Pointer(&procInfo)); err != nil { - return err - } - - d.mainMappingsInserted = true - return nil -} - -// updateStubMappings adds new stub routines that are not yet tracked in our -// stubs map and, if necessary on the architecture, inserts unwinding instructions -// for them in the PID mappings BPF map. -func (d *hotspotInstance) updateStubMappings(vmd *hotspotVMData, - ebpf interpreter.EbpfHandler, pid libpf.PID) { - for _, stub := range findStubBounds(vmd, d.bias, d.rm) { - if _, exists := d.stubs[stub.start]; exists { - continue - } - - d.stubs[stub.start] = stub - - // Separate stub areas are only required on ARM64. - if runtime.GOARCH != "arm64" { - continue - } - - // Find corresponding heap jitArea. - var stubHeapArea *jitArea - for i := range d.heapAreas { - heapArea := &d.heapAreas[i] - if stub.start >= heapArea.start && stub.end <= heapArea.end { - stubHeapArea = heapArea - break - } - } - if stubHeapArea == nil { - log.Warnf("Unable to find heap for stub: pid = %d, stub.start = 0x%x", - pid, stub.start) - continue - } - - // Create and insert a jitArea for the stub. - stubArea, err := jitAreaForStubArm64(&stub, stubHeapArea, d.rm) - if err != nil { - log.Warnf("Failed to create JIT area for stub (pid = %d, stub.start = 0x%x): %v", - pid, stub.start, err) - continue - } - if err = d.addJitArea(ebpf, pid, stubArea); err != nil { - log.Warnf("Failed to insert JIT area for stub (pid = %d, stub.start = 0x%x): %v", - pid, stub.start, err) - continue - } - } -} - -func (d *hotspotInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler, - _ reporter.SymbolReporter, pr process.Process, _ []process.Mapping) error { - vmd, err := d.d.GetOrInit(d.initVMData) - if err != nil { - return err - } - - // Check for permanent errors - if vmd.err != nil { - return vmd.err - } - - // Populate main mappings, if not done previously. - pid := pr.PID() - err = d.populateMainMappings(vmd, ebpf, pid) - if err != nil { - return err - } - if !d.mainMappingsInserted { - // Not ready yet: try later. - return nil - } - - d.updateStubMappings(vmd, ebpf, pid) - - return nil -} - -// Symbolize interpreters Hotspot eBPF uwinder given data containing target -// process address and translates it to static IDs expanding any inlined frames -// to multiple new frames. Associated symbolization metadata is extracted and -// queued to be sent to collection agent. -func (d *hotspotInstance) Symbolize(symbolReporter reporter.SymbolReporter, - frame *host.Frame, trace *libpf.Trace) error { - if !frame.Type.IsInterpType(libpf.HotSpot) { - return interpreter.ErrMismatchInterpreterType - } - - // Extract the HotSpot frame bitfields from the file and line variables - ptr := libpf.Address(frame.File) - subtype := uint32(frame.Lineno>>60) & 0xf - ripOrBci := int32(frame.Lineno>>32) & 0x0fffffff - ptrCheck := uint32(frame.Lineno) - - var err error - sfCounter := successfailurecounter.New(&d.successCount, &d.failCount) - defer sfCounter.DefaultToFailure() - - switch subtype { - case C.FRAME_HOTSPOT_STUB, C.FRAME_HOTSPOT_VTABLE: - // These are stub frames that may or may not be interesting - // to be seen in the trace. - stubID, err1 := d.getStubNameID(symbolReporter, ripOrBci, ptr, ptrCheck) - if err1 != nil { - return err - } - trace.AppendFrame(libpf.HotSpotFrame, hotspotStubsFileID, stubID) - case C.FRAME_HOTSPOT_INTERPRETER: - method, err1 := d.getMethod(ptr, ptrCheck) - if err1 != nil { - return err - } - err = method.symbolize(symbolReporter, ripOrBci, d, trace) - case C.FRAME_HOTSPOT_NATIVE: - jitinfo, err1 := d.getJITInfo(ptr, ptrCheck) - if err1 != nil { - return err1 - } - err = jitinfo.symbolize(symbolReporter, ripOrBci, d, trace) - default: - return fmt.Errorf("hotspot frame subtype %v is not supported", subtype) - } - - if err != nil { - return err - } - sfCounter.ReportSuccess() - return nil -} - -func (d *hotspotData) newUnsigned5Decoder(r io.ByteReader) *unsigned5Decoder { - return &unsigned5Decoder{ - r: r, - x: d.Get().unsigned5X, - } -} - -func (d *hotspotData) String() string { - if vmd := d.Get(); vmd != nil { - return fmt.Sprintf("Java HotSpot VM %d.%d.%d+%d (%v)", - (vmd.version>>24)&0xff, (vmd.version>>16)&0xff, - (vmd.version>>8)&0xff, vmd.version&0xff, - vmd.versionStr) - } - return "" -} - -// Attach loads to the ebpf program the needed pointers and sizes to unwind given hotspot process. -// As the hotspot unwinder depends on the native unwinder, a part of the cleanup is done by the -// process manager and not the corresponding Detach() function of hotspot objects. -func (d *hotspotData) Attach(_ interpreter.EbpfHandler, _ libpf.PID, bias libpf.Address, - rm remotememory.RemoteMemory) (ii interpreter.Instance, err error) { - // Each function has four symbols: source filename, class name, - // method name and signature. However, most of them are shared across - // different methods, so assume about 2 unique symbols per function. - addrToSymbol, err := - freelru.New[libpf.Address, string](2*interpreter.LruFunctionCacheSize, - libpf.Address.Hash32) - if err != nil { - return nil, err - } - addrToMethod, err := - freelru.New[libpf.Address, *hotspotMethod](interpreter.LruFunctionCacheSize, - libpf.Address.Hash32) - if err != nil { - return nil, err - } - addrToJITInfo, err := - freelru.New[libpf.Address, *hotspotJITInfo](interpreter.LruFunctionCacheSize, - libpf.Address.Hash32) - if err != nil { - return nil, err - } - // In total there are about 100 to 200 intrinsics. We don't expect to encounter - // everyone single one. So we use a small cache size here than LruFunctionCacheSize. - addrToStubNameID, err := - freelru.New[libpf.Address, libpf.AddressOrLineno](128, - libpf.Address.Hash32) - if err != nil { - return nil, err - } - - return &hotspotInstance{ - d: d, - rm: rm, - bias: bias, - addrToSymbol: addrToSymbol, - addrToMethod: addrToMethod, - addrToJITInfo: addrToJITInfo, - addrToStubNameID: addrToStubNameID, - prefixes: libpf.Set[lpm.Prefix]{}, - stubs: map[libpf.Address]StubRoutine{}, - }, nil -} - -// fieldByJavaName searches obj for a field by its JVM name using the struct tags. -func fieldByJavaName(obj reflect.Value, fieldName string) reflect.Value { - var catchAll reflect.Value - - objType := obj.Type() - for i := 0; i < obj.NumField(); i++ { - objField := objType.Field(i) - if nameTag, ok := objField.Tag.Lookup("name"); ok { - for _, javaName := range strings.Split(nameTag, ",") { - if fieldName == javaName { - return obj.Field(i) - } - if javaName == "*" { - catchAll = obj.Field(i) - } - } - } - if fieldName == objField.Name { - return obj.Field(i) - } - } - - return catchAll -} - -// parseIntrospection loads and parses HotSpot introspection tables. It will then fill in -// hotspotData.vmStructs using reflection to gather the offsets and sizes -// we are interested about. -func (vmd *hotspotVMData) parseIntrospection(it *hotspotIntrospectionTable, - rm remotememory.RemoteMemory, loadBias libpf.Address) error { - stride := libpf.Address(rm.Uint64(it.stride + loadBias)) - typeOffs := uint(rm.Uint64(it.typeOffset + loadBias)) - addrOffs := uint(rm.Uint64(it.addressOffset + loadBias)) - fieldOffs := uint(rm.Uint64(it.fieldOffset + loadBias)) - valOffs := uint(rm.Uint64(it.valueOffset + loadBias)) - base := it.base + loadBias - - if !it.skipBaseDref { - base = rm.Ptr(base) - } - - if base == 0 || stride == 0 { - return fmt.Errorf("bad introspection table data (%#x / %d)", base, stride) - } - - // Parse the introspection table - e := make([]byte, stride) - vm := reflect.ValueOf(&vmd.vmStructs).Elem() - for addr := base; true; addr += stride { - if err := rm.Read(addr, e); err != nil { - return err - } - - typeNamePtr := npsr.Ptr(e, typeOffs) - if typeNamePtr == 0 { - break - } - - typeName := rm.String(typeNamePtr) - f := fieldByJavaName(vm, typeName) - if !f.IsValid() { - continue - } - - // If parsing the Types table, we have sizes. Otherwise, we are - // parsing offsets for fields. - fieldName := "Sizeof" - if it.fieldOffset != 0 { - fieldNamePtr := npsr.Ptr(e, fieldOffs) - fieldName = rm.String(fieldNamePtr) - if fieldName == "" || fieldName[0] != '_' { - continue - } - } - - f = fieldByJavaName(f, fieldName) - if !f.IsValid() { - continue - } - - value := uint64(npsr.Ptr(e, addrOffs)) - if value != 0 { - // We just resolved a const pointer. Adjust it by loadBias - // to get a globally cacheable unrelocated virtual address. - value -= uint64(loadBias) - log.Debugf("JVM %v.%v = @ %x", typeName, fieldName, value) - } else { - // Literal value - value = npsr.Uint64(e, valOffs) - log.Debugf("JVM %v.%v = %v", typeName, fieldName, value) - } - - switch f.Kind() { - case reflect.Uint64, reflect.Uint: - f.SetUint(value) - case reflect.Map: - if f.IsNil() { - // maps need explicit init (nil is invalid) - f.Set(reflect.MakeMap(f.Type())) - } - - castedValue := reflect.ValueOf(value).Convert(f.Type().Elem()) - f.SetMapIndex(reflect.ValueOf(fieldName), castedValue) - default: - panic(fmt.Sprintf("bug: unexpected field type in vmStructs: %v", f.Kind())) - } - } - return nil -} - -// forEachItem walks the given struct reflection fields recursively, and calls the visitor -// function for each field item with it's value and name. This does not work with recursively -// linked structs, and is intended currently to be ran with the Hotspot's vmStructs struct only. -// Catch-all fields are ignored and skipped. -func forEachItem(prefix string, t reflect.Value, visitor func(reflect.Value, string) error) error { - if prefix != "" { - prefix += "." - } - for i := 0; i < t.NumField(); i++ { - val := t.Field(i) - fieldName := prefix + t.Type().Field(i).Name - switch val.Kind() { - case reflect.Struct: - if err := forEachItem(fieldName, val, visitor); err != nil { - return err - } - case reflect.Uint, reflect.Uint32, reflect.Uint64: - if err := visitor(val, fieldName); err != nil { - return err - } - case reflect.Map: - continue - default: - panic("unsupported type") - } - } - return nil -} - -// initVMData will fill hotspotVMData introspection data on first use -func (d *hotspotInstance) initVMData() (hotspotVMData, error) { - // Initialize the data with non-zero values so it's easy to check that - // everything got loaded (some fields will get zero values) - vmd := hotspotVMData{} - rm := d.rm - bias := d.bias - _ = forEachItem("", reflect.ValueOf(&vmd.vmStructs).Elem(), - func(item reflect.Value, name string) error { - item.SetUint(^uint64(0)) - return nil - }) - - // First load the sizes of the classes - if err := vmd.parseIntrospection(&d.d.typePtrs, d.rm, bias); err != nil { - return vmd, err - } - // And the field offsets and static values - if err := vmd.parseIntrospection(&d.d.structPtrs, d.rm, bias); err != nil { - return vmd, err - } - if d.d.jvmciStructPtrs.base != 0 { - if err := vmd.parseIntrospection(&d.d.jvmciStructPtrs, d.rm, bias); err != nil { - return vmd, err - } - } - - // Failures after this point are permanent - vms := &vmd.vmStructs - jdkVersion := rm.Uint32(vms.JdkVersion.Current + bias) - major := jdkVersion & 0xff - minor := (jdkVersion >> 8) & 0xff - patch := (jdkVersion >> 16) & 0xff - build := rm.Uint32(vms.AbstractVMVersion.BuildNumber + bias) - vmd.version = major<<24 + minor<<16 + patch<<8 + build - vmd.versionStr = rm.StringPtr(vms.AbstractVMVersion.Release + bias) - - // Check minimum supported version. JDK 7-20 supported. Assume newer JDK - // works if the needed symbols are found. - if major < 7 { - vmd.err = fmt.Errorf("JVM version %d.%d.%d+%d (minimum is 7)", - major, minor, patch, build) - return vmd, nil - } - - if vms.ConstantPool.SourceFileNameIndex != ^uint(0) { - // JDK15: Use ConstantPool.SourceFileNameIndex - vms.InstanceKlass.SourceFileNameIndex = 0 - vms.InstanceKlass.SourceFileName = 0 - } else if vms.InstanceKlass.SourceFileNameIndex != ^uint(0) { - // JDK8-14: Use InstanceKlass.SourceFileNameIndex - vms.ConstantPool.SourceFileNameIndex = 0 - vms.InstanceKlass.SourceFileName = 0 - } else { - // JDK7: File name is direct Symbol*, adjust offsets with OopDesc due - // to the base pointer type changes - vms.InstanceKlass.SourceFileName += vms.OopDesc.Sizeof - if vms.Klass.Name != ^uint(0) { - vms.Klass.Name += vms.OopDesc.Sizeof - } - vms.ConstantPool.SourceFileNameIndex = 0 - vms.InstanceKlass.SourceFileNameIndex = 0 - } - - // JDK-8: Only single CodeCache Heap, some CodeBlob and Nmethod changes - if vms.CodeCache.Heap != ^libpf.Address(0) { - // Validate values that can be missing, fixup CompiledMethod offsets - vms.CodeCache.Heaps = 0 - vms.CodeCache.HighBound = 0 - vms.CodeCache.LowBound = 0 - vms.CompiledMethod.Sizeof = vms.Nmethod.Sizeof - vms.CompiledMethod.DeoptHandlerBegin = vms.Nmethod.DeoptimizeOffset - vms.CompiledMethod.Method = vms.Nmethod.Method - vms.CompiledMethod.ScopesDataBegin = 0 - } else { - // Reset the compatibility symbols not needed - vms.CodeCache.Heap = 0 - vms.Nmethod.Method = 0 - vms.Nmethod.DeoptimizeOffset = 0 - vms.Nmethod.ScopesDataOffset = 0 - } - - // JDK12+: Use Symbol.Length_and_refcount for Symbol.Length - if vms.Symbol.LengthAndRefcount != ^uint(0) { - // The symbol _length was merged and renamed to _symbol_length_and_refcount. - // Calculate the _length offset from it. - vms.Symbol.Length = vms.Symbol.LengthAndRefcount + 2 - } else { - // Reset the non-used symbols so the check below does not fail - vms.Symbol.LengthAndRefcount = 0 - } - - // JDK16: use GenericGrowableArray as in JDK9-15 case - if vms.GrowableArrayBase.Len != ^uint(0) { - vms.GenericGrowableArray.Len = vms.GrowableArrayBase.Len - } else { - // Reset the non-used symbols so the check below does not fail - vms.GrowableArrayBase.Len = 0 - } - - // JDK20+: UNSIGNED5 encoding change (since 20.0.15) - // https://github.com/openjdk/jdk20u/commit/8d3399bf5f354931b0c62d2ed8095e554be71680 - if vmd.version >= 0x1400000f { - vmd.unsigned5X = 1 - } - - // Check that all symbols got loaded from JVM introspection data - err := forEachItem("", reflect.ValueOf(&vmd.vmStructs).Elem(), - func(item reflect.Value, name string) error { - switch item.Kind() { - case reflect.Uint, reflect.Uint64: - if item.Uint() != ^uint64(0) { - return nil - } - case reflect.Uint32: - if item.Uint() != uint64(^uint32(0)) { - return nil - } - } - return fmt.Errorf("JVM symbol '%v' not found", name) - }) - if err != nil { - vmd.err = err - return vmd, nil - } - - if vms.Symbol.Sizeof > 32 { - // Additional sanity for Symbol.Sizeof which normally is - // just 8 byte or so. The getSymbol() hard codes the first read - // as 128 bytes and it needs to be more than this. - vmd.err = fmt.Errorf("JVM Symbol.Sizeof value %d", vms.Symbol.Sizeof) - return vmd, nil - } - - // Verify that all struct fields are within limits - structs := reflect.ValueOf(&vmd.vmStructs).Elem() - for i := 0; i < structs.NumField(); i++ { - klass := structs.Field(i) - sizeOf := klass.FieldByName("Sizeof") - if !sizeOf.IsValid() { - continue - } - maxOffset := sizeOf.Uint() - for j := 0; j < klass.NumField(); j++ { - field := klass.Field(j) - if field.Kind() == reflect.Map { - continue - } - - if field.Uint() > maxOffset { - vmd.err = fmt.Errorf("%s.%s offset %v is larger than class size %v", - structs.Type().Field(i).Name, - klass.Type().Field(j).Name, - field.Uint(), maxOffset) - return vmd, nil - } - } - } - - return vmd, nil -} - -// locateJvmciVMStructs attempts to heuristically locate the JVMCI VM structs by -// searching for references to the string `Klass_vtable_start_offset`. In all JVM -// versions >= 9.0, this corresponds to the first entry in the VM structs: -// -// nolint:lll -// https://github.com/openjdk/jdk/blob/jdk-9%2B181/hotspot/src/share/vm/jvmci/vmStructs_jvmci.cpp#L48 -// https://github.com/openjdk/jdk/blob/jdk-22%2B10/src/hotspot/share/jvmci/vmStructs_jvmci.cpp#L49 -func locateJvmciVMStructs(ef *pfelf.File) (libpf.Address, error) { - const maxDataReadSize = 1 * 1024 * 1024 // seen in practice: 192 KiB - const maxRodataReadSize = 4 * 1024 * 1024 // seen in practice: 753 KiB - - rodataSec := ef.Section(".rodata") - if rodataSec == nil { - return 0, errors.New("unable to find `.rodata` section") - } - - rodata, err := rodataSec.Data(maxRodataReadSize) - if err != nil { - return 0, err - } - - offs := bytes.Index(rodata, []byte("Klass_vtable_start_offset")) - if offs == -1 { - return 0, errors.New("unable to find string for heuristic") - } - - ptr := rodataSec.Addr + uint64(offs) - ptrEncoded := make([]byte, 8) - binary.LittleEndian.PutUint64(ptrEncoded, ptr) - - dataSec := ef.Section(".data") - if dataSec == nil { - return 0, errors.New("unable to find `.data` section") - } - - data, err := dataSec.Data(maxDataReadSize) - if err != nil { - return 0, err - } - - offs = bytes.Index(data, ptrEncoded) - if offs == -1 { - return 0, errors.New("unable to find string pointer") - } - - // 8 in the expression below is what we'd usually read from - // gHotSpotVMStructEntryFieldNameOffset. This value unfortunately lives in - // BSS, so we have no choice but to hard-code it. Fortunately enough this - // offset hasn't changed since at least JDK 9. - return libpf.Address(dataSec.Addr + uint64(offs) - 8), nil -} + _ interpreter.Data = &hotspotData{} + _ interpreter.Instance = &hotspotInstance{} +) // Loader is the main function for ProcessManager to recognize and hook the HotSpot // libjvm for enabling JVM unwinding and symbolization. @@ -1889,47 +140,9 @@ func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interprete } log.Debugf("HotSpot inspecting %v", info.FileName()) - ef, err := info.GetELF() if err != nil { return nil, err } - - d := &hotspotData{} - err = d.structPtrs.resolveSymbols(ef, - []string{ - "gHotSpotVMStructs", - "gHotSpotVMStructEntryArrayStride", - "gHotSpotVMStructEntryTypeNameOffset", - "gHotSpotVMStructEntryFieldNameOffset", - "gHotSpotVMStructEntryOffsetOffset", - "gHotSpotVMStructEntryAddressOffset", - }) - if err != nil { - return nil, err - } - - err = d.typePtrs.resolveSymbols(ef, - []string{ - "gHotSpotVMTypes", - "gHotSpotVMTypeEntryArrayStride", - "gHotSpotVMTypeEntryTypeNameOffset", - "", - "gHotSpotVMTypeEntrySizeOffset", - "", - }) - if err != nil { - return nil, err - } - - if ptr, err := locateJvmciVMStructs(ef); err == nil { - // Everything except for the base pointer is identical. - d.jvmciStructPtrs = d.structPtrs - d.jvmciStructPtrs.base = ptr - d.jvmciStructPtrs.skipBaseDref = true - } else { - log.Warnf("%s: unable to read JVMCI VM structs: %v", info.FileName(), err) - } - - return d, nil + return newHotspotData(info.FileName(), ef) } diff --git a/interpreter/hotspot/hotspot_test.go b/interpreter/hotspot/hotspot_test.go deleted file mode 100644 index 44804745..00000000 --- a/interpreter/hotspot/hotspot_test.go +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -package hotspot - -import ( - "bytes" - "encoding/binary" - "fmt" - "io" - "os" - "strings" - "testing" - "unsafe" - - "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/freelru" - "github.com/elastic/otel-profiling-agent/libpf/remotememory" - "github.com/elastic/otel-profiling-agent/lpm" -) - -func TestJavaDemangling(t *testing.T) { - cases := []struct { - klass, method, signature, demangled string - }{ - {"java/lang/Object", "", "()V", - "void java.lang.Object.()"}, - {"java/lang/StringLatin1", "equals", "([B[B)Z", - "boolean java.lang.StringLatin1.equals(byte[], byte[])"}, - {"java/util/zip/ZipUtils", "CENSIZ", "([BI)J", - "long java.util.zip.ZipUtils.CENSIZ(byte[], int)"}, - {"java/util/regex/Pattern$BmpCharProperty", "match", - "(Ljava/util/regex/Matcher;ILjava/lang/CharSequence;)Z", - "boolean java.util.regex.Pattern$BmpCharProperty.match" + - "(java.util.regex.Matcher, int, java.lang.CharSequence)"}, - {"java/lang/AbstractStringBuilder", "appendChars", "(Ljava/lang/String;II)V", - "void java.lang.AbstractStringBuilder.appendChars" + - "(java.lang.String, int, int)"}, - {"foo/test", "bar", "([)J", "long foo.test.bar()"}, - } - - for _, c := range cases { - demangled := demangleJavaMethod(c.klass, c.method, c.signature) - if demangled != c.demangled { - t.Errorf("signature '%s' != '%s'", demangled, c.demangled) - } - } -} - -// TestJavaLineNumbers tests that the Hotspot delta encoded line table decoding works. -// The set here is an actually table extracting from JVM. It is fairly easy to encode -// these numbers if needed, but we don't need to generate them currently for anything. -func TestJavaLineNumbers(t *testing.T) { - bciLine := []struct { - bci, line uint32 - }{ - {0, 478}, - {5, 479}, - {9, 480}, - {19, 481}, - {26, 482}, - {33, 483}, - {47, 490}, - {50, 485}, - {52, 486}, - {58, 490}, - {61, 488}, - {63, 489}, - {68, 491}, - } - - decoder := unsigned5Decoder{ - r: bytes.NewReader([]byte{ - 255, 0, 252, 11, 41, 33, 81, 57, 57, 119, - 255, 6, 9, 17, 52, 255, 6, 3, 17, 42, 0}), - } - - var bci, line uint32 - for i := 0; i < len(bciLine); i++ { - if err := decoder.decodeLineTableEntry(&bci, &line); err != nil { - t.Fatalf("line table decoding failed: %v", err) - } - if bciLine[i].bci != bci || bciLine[i].line != line { - t.Fatalf("{%v,%v} != {%v,%v}\n", bci, line, bciLine[i].bci, bciLine[i].line) - } - } - if err := decoder.decodeLineTableEntry(&bci, &line); err != io.EOF { - if err == nil { - err = fmt.Errorf("compressed data has more entries than expected") - } - t.Fatalf("line table not empty at end: %v", err) - } -} - -func TestJavaSymbolExtraction(t *testing.T) { - rm := remotememory.NewProcessVirtualMemory(libpf.PID(os.Getpid())) - id := hotspotData{} - vmd, _ := id.GetOrInit(func() (hotspotVMData, error) { - vmd := hotspotVMData{} - vmd.vmStructs.Symbol.Length = 2 - vmd.vmStructs.Symbol.Body = 4 - return vmd, nil - }) - - addrToSymbol, err := freelru.New[libpf.Address, string](2, libpf.Address.Hash32) - if err != nil { - t.Fatalf("symbol cache lru: %v", err) - } - ii := hotspotInstance{ - d: &id, - rm: rm, - addrToSymbol: addrToSymbol, - prefixes: libpf.Set[lpm.Prefix]{}, - stubs: map[libpf.Address]StubRoutine{}, - } - maxLength := 1024 - sym := make([]byte, vmd.vmStructs.Symbol.Body+uint(maxLength)) - str := strings.Repeat("a", maxLength) - copy(sym[vmd.vmStructs.Symbol.Body:], str) - for i := 0; i <= maxLength; i++ { - binary.LittleEndian.PutUint16(sym[vmd.vmStructs.Symbol.Length:], uint16(i)) - address := libpf.Address(uintptr(unsafe.Pointer(&sym[0]))) - got := ii.getSymbol(address) - if str[:i] != got { - t.Errorf("sym '%s' != '%s'", str[:i], got) - } - ii.addrToSymbol.Purge() - } -} diff --git a/interpreter/hotspot/instance.go b/interpreter/hotspot/instance.go new file mode 100644 index 00000000..82e4598a --- /dev/null +++ b/interpreter/hotspot/instance.go @@ -0,0 +1,828 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package hotspot + +import ( + "encoding/binary" + "errors" + "fmt" + "hash/fnv" + "io" + "runtime" + "sync/atomic" + "unsafe" + + log "github.com/sirupsen/logrus" + + "github.com/elastic/go-freelru" + + "github.com/elastic/otel-profiling-agent/host" + "github.com/elastic/otel-profiling-agent/interpreter" + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/lpm" + "github.com/elastic/otel-profiling-agent/metrics" + npsr "github.com/elastic/otel-profiling-agent/nopanicslicereader" + "github.com/elastic/otel-profiling-agent/process" + "github.com/elastic/otel-profiling-agent/remotememory" + "github.com/elastic/otel-profiling-agent/reporter" + "github.com/elastic/otel-profiling-agent/successfailurecounter" + "github.com/elastic/otel-profiling-agent/support" + "github.com/elastic/otel-profiling-agent/util" +) + +// #include "../../support/ebpf/types.h" +// #include "../../support/ebpf/frametypes.h" +import "C" + +// heapRange contains info for an individual heap. +type heapRange struct { + codeStart, codeEnd libpf.Address + segmapStart, segmapEnd libpf.Address +} + +// heapInfo contains info about all HotSpot heaps. +type heapInfo struct { + segmentShift uint32 + ranges []heapRange +} + +type jitArea struct { + start, end libpf.Address + codeStart libpf.Address + tsid uint64 +} + +// hotspotInstance contains information about one running HotSpot instance (pid) +type hotspotInstance struct { + interpreter.InstanceStubs + + // Hotspot symbolization metrics + successCount atomic.Uint64 + failCount atomic.Uint64 + + // d is the interpreter data from jvm.so (shared between processes) + d *hotspotData + + // rm is used to access the remote process memory + rm remotememory.RemoteMemory + + // bias is the ELF DSO load bias + bias libpf.Address + + // prefixes is list of LPM prefixes added to ebpf maps (to be cleaned up) + prefixes libpf.Set[lpm.Prefix] + + // addrToSymbol maps a JVM class Symbol address to it's string value + addrToSymbol *freelru.LRU[libpf.Address, string] + + // addrToMethod maps a JVM class Method to a hotspotMethod which caches + // the needed data from it. + addrToMethod *freelru.LRU[libpf.Address, *hotspotMethod] + + // addrToJitInfo maps a JVM class nmethod to a hotspotJITInfo which caches + // the needed data from it. + addrToJITInfo *freelru.LRU[libpf.Address, *hotspotJITInfo] + + // addrToStubNameID maps a stub name to its unique identifier. + addrToStubNameID *freelru.LRU[libpf.Address, libpf.AddressOrLineno] + + // mainMappingsInserted stores whether the heap areas and proc data are already populated. + mainMappingsInserted bool + + // heapAreas stores the top-level JIT areas based on the Java heaps. + heapAreas []jitArea + + // stubs stores all known stub routine regions. + stubs map[libpf.Address]StubRoutine +} + +func (d *hotspotInstance) GetAndResetMetrics() ([]metrics.Metric, error) { + addrToSymbolStats := d.addrToSymbol.ResetMetrics() + addrToMethodStats := d.addrToMethod.ResetMetrics() + addrToJITInfoStats := d.addrToJITInfo.ResetMetrics() + addrToStubNameIDStats := d.addrToStubNameID.ResetMetrics() + + return []metrics.Metric{ + { + ID: metrics.IDHotspotSymbolizationSuccesses, + Value: metrics.MetricValue(d.successCount.Swap(0)), + }, + { + ID: metrics.IDHotspotSymbolizationFailures, + Value: metrics.MetricValue(d.failCount.Swap(0)), + }, + { + ID: metrics.IDHotspotAddrToSymbolHit, + Value: metrics.MetricValue(addrToSymbolStats.Hits), + }, + { + ID: metrics.IDHotspotAddrToSymbolMiss, + Value: metrics.MetricValue(addrToSymbolStats.Misses), + }, + { + ID: metrics.IDHotspotAddrToSymbolAdd, + Value: metrics.MetricValue(addrToSymbolStats.Inserts), + }, + { + ID: metrics.IDHotspotAddrToSymbolDel, + Value: metrics.MetricValue(addrToSymbolStats.Removals), + }, + { + ID: metrics.IDHotspotAddrToMethodHit, + Value: metrics.MetricValue(addrToMethodStats.Hits), + }, + { + ID: metrics.IDHotspotAddrToMethodMiss, + Value: metrics.MetricValue(addrToMethodStats.Misses), + }, + { + ID: metrics.IDHotspotAddrToMethodAdd, + Value: metrics.MetricValue(addrToMethodStats.Inserts), + }, + { + ID: metrics.IDHotspotAddrToMethodDel, + Value: metrics.MetricValue(addrToMethodStats.Removals), + }, + { + ID: metrics.IDHotspotAddrToJITInfoHit, + Value: metrics.MetricValue(addrToJITInfoStats.Hits), + }, + { + ID: metrics.IDHotspotAddrToJITInfoMiss, + Value: metrics.MetricValue(addrToJITInfoStats.Misses), + }, + { + ID: metrics.IDHotspotAddrToJITInfoAdd, + Value: metrics.MetricValue(addrToJITInfoStats.Inserts), + }, + { + ID: metrics.IDHotspotAddrToJITInfoDel, + Value: metrics.MetricValue(addrToJITInfoStats.Removals), + }, + { + ID: metrics.IDHotspotAddrToStubNameIDHit, + Value: metrics.MetricValue(addrToStubNameIDStats.Hits), + }, + { + ID: metrics.IDHotspotAddrToStubNameIDMiss, + Value: metrics.MetricValue(addrToStubNameIDStats.Misses), + }, + { + ID: metrics.IDHotspotAddrToStubNameIDAdd, + Value: metrics.MetricValue(addrToStubNameIDStats.Inserts), + }, + { + ID: metrics.IDHotspotAddrToStubNameIDDel, + Value: metrics.MetricValue(addrToStubNameIDStats.Removals), + }, + }, nil +} + +// getSymbol extracts a class Symbol value from the given address in the target JVM process +func (d *hotspotInstance) getSymbol(addr libpf.Address) string { + if value, ok := d.addrToSymbol.Get(addr); ok { + return value + } + vms := d.d.Get().vmStructs + + // Read the symbol length and readahead bytes in attempt to avoid second + // system call to read the target string. 128 is chosen arbitrarily as "hopefully + // good enough"; this value can be increased if it turns out to be necessary. + var buf [128]byte + if d.rm.Read(addr, buf[:]) != nil { + return "" + } + symLen := npsr.Uint16(buf[:], vms.Symbol.Length) + if symLen == 0 { + return "" + } + + // Always allocate the string separately so it does not hold the backing + // buffer that might be larger than needed + tmp := make([]byte, symLen) + copy(tmp, buf[vms.Symbol.Body:]) + if vms.Symbol.Body+uint(symLen) > uint(len(buf)) { + prefixLen := uint(len(buf[vms.Symbol.Body:])) + if d.rm.Read(addr+libpf.Address(vms.Symbol.Body+prefixLen), tmp[prefixLen:]) != nil { + return "" + } + } + s := string(tmp) + if !util.IsValidString(s) { + log.Debugf("Extracted Hotspot symbol is invalid at 0x%x '%v'", addr, []byte(s)) + return "" + } + d.addrToSymbol.Add(addr, s) + return s +} + +// getPoolSymbol reads a class ConstantPool value from given index, and reads the +// symbol value it is referencing +func (d *hotspotInstance) getPoolSymbol(addr libpf.Address, ndx uint16) string { + // Zero index is not valid + if ndx == 0 { + return "" + } + + vms := &d.d.Get().vmStructs + offs := libpf.Address(vms.ConstantPool.Sizeof) + 8*libpf.Address(ndx) + cpoolVal := d.rm.Ptr(addr + offs) + // The lowest bit is reserved by JVM to indicate if the value has been + // resolved or not. The values see should be always resolved. + // Just ignore the bit as it's meaning has changed between JDK versions. + return d.getSymbol(cpoolVal &^ 1) +} + +// getStubNameID read the stub name from the code blob at given address and generates a ID. +func (d *hotspotInstance) getStubNameID(symbolReporter reporter.SymbolReporter, ripOrBci int32, + addr libpf.Address, _ uint32) (libpf.AddressOrLineno, error) { + if value, ok := d.addrToStubNameID.Get(addr); ok { + return value, nil + } + vms := &d.d.Get().vmStructs + constStubNameAddr := d.rm.Ptr(addr + libpf.Address(vms.CodeBlob.Name)) + stubName := d.rm.String(constStubNameAddr) + + a := d.rm.Ptr(addr+libpf.Address(vms.CodeBlob.CodeBegin)) + libpf.Address(ripOrBci) + for _, stub := range d.stubs { + if stub.start <= a && stub.end > a { + stubName = fmt.Sprintf("%s [%s]", stubName, stub.name) + break + } + } + + h := fnv.New128a() + _, _ = h.Write([]byte(stubName)) + nameHash := h.Sum(nil) + stubID := libpf.AddressOrLineno(npsr.Uint64(nameHash, 0)) + + symbolReporter.FrameMetadata(hotspotStubsFileID, stubID, 0, 0, stubName, "") + + d.addrToStubNameID.Add(addr, stubID) + return stubID, nil +} + +// getMethod reads and returns the interesting data from "class Method" at given address +func (d *hotspotInstance) getMethod(addr libpf.Address, _ uint32) (*hotspotMethod, error) { + if value, ok := d.addrToMethod.Get(addr); ok { + return value, nil + } + vms := &d.d.Get().vmStructs + constMethodAddr := d.rm.Ptr(addr + libpf.Address(vms.Method.ConstMethod)) + constMethod := make([]byte, vms.ConstMethod.Sizeof) + if err := d.rm.Read(constMethodAddr, constMethod); err != nil { + return nil, fmt.Errorf("invalid ConstMethod ptr: %v", err) + } + + cpoolAddr := npsr.Ptr(constMethod, vms.ConstMethod.Constants) + cpool := make([]byte, vms.ConstantPool.Sizeof) + if err := d.rm.Read(cpoolAddr, cpool); err != nil { + return nil, fmt.Errorf("invalid CostantPool ptr: %v", err) + } + + instanceKlassAddr := npsr.Ptr(cpool, vms.ConstantPool.PoolHolder) + instanceKlass := make([]byte, vms.InstanceKlass.Sizeof) + if err := d.rm.Read(instanceKlassAddr, instanceKlass); err != nil { + return nil, fmt.Errorf("invalid ConstantPool ptr: %v", err) + } + + var sourceFileName string + switch { + case vms.ConstantPool.SourceFileNameIndex != 0: + // JDK15 + sourceFileName = d.getPoolSymbol(cpoolAddr, + npsr.Uint16(cpool, vms.ConstantPool.SourceFileNameIndex)) + case vms.InstanceKlass.SourceFileNameIndex != 0: + // JDK8-14 + sourceFileName = d.getPoolSymbol(cpoolAddr, + npsr.Uint16(instanceKlass, vms.InstanceKlass.SourceFileNameIndex)) + default: + // JDK7 + sourceFileName = d.getSymbol( + npsr.Ptr(instanceKlass, vms.InstanceKlass.SourceFileName)) + } + klassName := d.getSymbol(npsr.Ptr(instanceKlass, vms.Klass.Name)) + methodName := d.getPoolSymbol(cpoolAddr, npsr.Uint16(constMethod, + vms.ConstMethod.NameIndex)) + signature := d.getPoolSymbol(cpoolAddr, npsr.Uint16(constMethod, + vms.ConstMethod.SignatureIndex)) + + if sourceFileName == "" { + // Java and Scala can autogenerate lambdas which have no source + // information available. The HotSpot VM backtraces displays + // "Unknown Source" as the filename for these. + sourceFileName = interpreter.UnknownSourceFile + + // Java 15 introduced "Hidden Classes" via JEP 371. These class names + // contain pointers. Mask the pointers to reduce cardinality. + klassName = hiddenClassRegex.ReplaceAllString(klassName, hiddenClassMask) + } + + // Synthesize a FileID that is unique to this Class/Method that can be + // used as "CodeObjectID" value in the trace as frames FileID. + // Keep the sourcefileName there to start with, and add klass name, method + // name, byte code and the JVM presentation of the source line table. + h := fnv.New128a() + _, _ = h.Write([]byte(sourceFileName)) + _, _ = h.Write([]byte(klassName)) + _, _ = h.Write([]byte(methodName)) + _, _ = h.Write([]byte(signature)) + + // Read the byte code for CodeObjectID + bytecodeSize := npsr.Uint16(constMethod, vms.ConstMethod.CodeSize) + byteCode := make([]byte, bytecodeSize) + err := d.rm.Read(constMethodAddr+libpf.Address(vms.ConstMethod.Sizeof), byteCode) + if err != nil { + return nil, fmt.Errorf("invalid ByteCode ptr: %v", err) + } + + _, _ = h.Write(byteCode) + + var lineTable []byte + startLine := ^uint32(0) + // NOTE: ConstMethod.Flags is either u16 or u32 depending on JVM version. Since we + // only care about flags in the first byte and only operate on little endian + // architectures we can get away with reading it as u8 either way. + if npsr.Uint8(constMethod, vms.ConstMethod.Flags)&ConstMethod_has_linenumber_table != 0 { + // The line number table size is not known ahead of time. It is delta compressed, + // so read it once using buffered read to capture it fully. Get also the smallest + // line number present as the function start line number - this is not perfect + // as it's the first line for which code was generated. Usually one or few lines + // after the actual function definition line. The Byte Code Index (BCI) is just + // used for additional method ID hash input. + var pcLineEntry [4]byte + var curBci, curLine uint32 + err = nil + r := newRecordingReader(d.rm, int64(constMethodAddr)+int64(vms.ConstMethod.Sizeof)+ + int64(bytecodeSize), 256) + dec := d.d.newUnsigned5Decoder(r) + for err == nil { + if curLine > 0 && curLine < startLine { + startLine = curLine + } + err = dec.decodeLineTableEntry(&curBci, &curLine) + + // The BCI and line numbers are read from the target memory in the custom + // format, but the .class file LineNumberTable is big-endian encoded + // { + // u2 start_pc, line_number; + // } line_number_table[line_number_table_length] + // + // This hashes the line_number_table in .class file format, so if we + // ever start indexing .class/.java files to match methods to real source + // file IDs, we can produce the hash in the indexer without additional + // transformations needed. + binary.BigEndian.PutUint16(pcLineEntry[0:2], uint16(curBci)) + binary.BigEndian.PutUint16(pcLineEntry[2:4], uint16(curLine)) + _, _ = h.Write(pcLineEntry[:]) + } + + // If EOF encountered, the table was processed successfully. + if err == io.EOF { + lineTable = r.GetBuffer() + } + } + if startLine == ^uint32(0) { + startLine = 0 + } + // Finalize CodeObjectID generation + objectID, err := libpf.FileIDFromBytes(h.Sum(nil)) + if err != nil { + return nil, fmt.Errorf("failed to create a code object ID: %v", err) + } + + sym := &hotspotMethod{ + sourceFileName: sourceFileName, + objectID: objectID, + methodName: demangleJavaMethod(klassName, methodName, signature), + bytecodeSize: bytecodeSize, + lineTable: lineTable, + startLineNo: uint16(startLine), + bciSeen: make(libpf.Set[uint16]), + } + d.addrToMethod.Add(addr, sym) + return sym, nil +} + +// getJITInfo reads and returns the interesting data from "class nmethod" at given address +func (d *hotspotInstance) getJITInfo(addr libpf.Address, + addrCheck uint32) (*hotspotJITInfo, error) { + if jit, ok := d.addrToJITInfo.Get(addr); ok { + if jit.compileID == addrCheck { + return jit, nil + } + } + vms := &d.d.Get().vmStructs + + // Each JIT-ted function is contained in a "class nmethod" + // (derived from CompiledMethod and CodeBlob). + // + // Layout of important bits in such 'class nmethod' pointer is: + // [class CodeBlob fields] + // [class CompiledMethod fields] + // [class nmethod fields] + // ... + // [JIT_code] @ this + CodeBlob._code_start + // ... + // [metadata] @ this + nmethod._metadata_offset \ these three + // [scopes_data] @ CompiledMethod._scopes_data_begin | arrays we need + // [scopes_pcs] @ this + nmethod._scopes_pcs_offset / for inlining info + // [dependencies] @ this + nmethod._dependencies_offset + // ... + // + // see: src/hotspot/share/code/compiledMethod.hpp + // src/hotspot/share/code/nmethod.hpp + // + // The scopes_pcs is a look up table to map RIP to scope_data. scopes_data + // is a list of descriptors that lists the method and it's Byte Code Index (BCI) + // activations for the scope. Finally the metadata is the array that + // maps scope_data method indices to real "class Method*". + nmethod := make([]byte, vms.Nmethod.Sizeof) + if err := d.rm.Read(addr, nmethod); err != nil { + return nil, fmt.Errorf("invalid nmethod ptr: %v", err) + } + + // Since the Java VM might decide recompile or free the JITted nmethods + // we use the nmethod._compile_id (global running number to identify JIT + // method) to uniquely identify that we are using the right data here + // vs. when the pointer was captured by eBPF. + compileID := npsr.Uint32(nmethod, vms.Nmethod.CompileID) + if compileID != addrCheck { + return nil, errors.New("JIT info evicted since eBPF snapshot") + } + + // Finally read the associated debug information for this method + var scopesOff libpf.Address + metadataOff := npsr.PtrDiff32(nmethod, vms.Nmethod.MetadataOffset) + if vms.CompiledMethod.ScopesDataBegin != 0 { + scopesOff = npsr.Ptr(nmethod, vms.CompiledMethod.ScopesDataBegin) - addr + } else { + scopesOff = npsr.PtrDiff32(nmethod, vms.Nmethod.ScopesDataOffset) + } + scopesPcsOff := npsr.PtrDiff32(nmethod, vms.Nmethod.ScopesPcsOffset) + depsOff := npsr.PtrDiff32(nmethod, vms.Nmethod.DependenciesOffset) + + if metadataOff > scopesOff || scopesOff > scopesPcsOff || scopesPcsOff > depsOff { + return nil, fmt.Errorf("unexpected nmethod layout: %v <= %v <= %v <= %v", + metadataOff, scopesOff, scopesPcsOff, depsOff) + } + + method, err := d.getMethod(npsr.Ptr(nmethod, vms.CompiledMethod.Method), 0) + if err != nil { + return nil, fmt.Errorf("failed to get JIT Method: %v", err) + } + + buf := make([]byte, depsOff-metadataOff) + if err := d.rm.Read(addr+metadataOff, buf); err != nil { + return nil, fmt.Errorf("invalid nmethod metadata: %v", err) + } + + // Buffer is read starting from metadataOff, so adjust accordingly + scopesOff -= metadataOff + scopesPcsOff -= metadataOff + + jit := &hotspotJITInfo{ + compileID: compileID, + method: method, + metadata: buf[0:scopesOff], + scopesData: buf[scopesOff:scopesPcsOff], + scopesPcs: buf[scopesPcsOff:], + } + + d.addrToJITInfo.Add(addr, jit) + return jit, nil +} + +// Detach removes all information regarding a given process from the eBPF maps. +func (d *hotspotInstance) Detach(ebpf interpreter.EbpfHandler, pid util.PID) error { + var err error + if d.mainMappingsInserted { + err = ebpf.DeleteProcData(libpf.HotSpot, pid) + } + + for prefix := range d.prefixes { + if err2 := ebpf.DeletePidInterpreterMapping(pid, prefix); err2 != nil { + err = errors.Join(err, + fmt.Errorf("failed to remove page 0x%x/%d: %v", + prefix.Key, prefix.Length, err2)) + } + } + + if err != nil { + return fmt.Errorf("failed to detach hotspotInstance from PID %d: %v", + pid, err) + } + return nil +} + +// gatherHeapInfo collects information about HotSpot heaps. +func (d *hotspotInstance) gatherHeapInfo(vmd *hotspotVMData) (*heapInfo, error) { + info := &heapInfo{} + + // Determine the location of heap pointers + var heapPtrAddr libpf.Address + var numHeaps uint32 + + vms := &vmd.vmStructs + rm := d.rm + if vms.CodeCache.Heap != 0 { + // JDK -8: one fixed CodeHeap through fixed pointer + numHeaps = 1 + heapPtrAddr = vms.CodeCache.Heap + d.bias + } else { + // JDK 9-: CodeHeap through _heaps array + heaps := make([]byte, vms.GrowableArrayInt.Sizeof) + if err := rm.Read(rm.Ptr(vms.CodeCache.Heaps+d.bias), heaps); err != nil { + return nil, fmt.Errorf("fail to read heap array: %v", err) + } + // Read numHeaps + numHeaps = npsr.Uint32(heaps, vms.GenericGrowableArray.Len) + + heapPtrAddr = npsr.Ptr(heaps, vms.GrowableArrayInt.Data) + if numHeaps == 0 || heapPtrAddr == 0 { + // The heaps are not yet initialized + return nil, nil + } + } + + // Get and sanity check the number of heaps + if numHeaps < 1 || numHeaps > 16 { + return nil, fmt.Errorf("bad hotspot heap count (%v)", numHeaps) + } + + // Extract the heap pointers + heap := make([]byte, vms.CodeHeap.Sizeof) + heapPtrs := make([]byte, 8*numHeaps) + if err := rm.Read(heapPtrAddr, heapPtrs); err != nil { + return nil, fmt.Errorf("fail to read heap array values: %v", err) + } + + // Extract each heap structure individually + for ndx := uint32(0); ndx < numHeaps; ndx++ { + heapPtr := npsr.Ptr(heapPtrs, uint(ndx*8)) + if heapPtr == 0 { + // JVM is not initialized yet. Retry later. + return nil, nil + } + if err := rm.Read(heapPtr, heap); err != nil { + return nil, fmt.Errorf("fail to read heap pointer %d: %v", ndx, err) + } + + // The segment shift is same for all heaps. So record it for the process only. + info.segmentShift = npsr.Uint32(heap, vms.CodeHeap.Log2SegmentSize) + + // The LowBoundary and HighBoundary describe the mapping that was reserved + // with mmap(PROT_NONE). The actual mapping that is committed memory is in + // VirtualSpace.{Low,High}. However, since we are just following pointers we + // really care about the maximum values which do not change. + rng := heapRange{ + codeStart: npsr.Ptr(heap, vms.CodeHeap.Memory+vms.VirtualSpace.LowBoundary), + codeEnd: npsr.Ptr(heap, vms.CodeHeap.Memory+vms.VirtualSpace.HighBoundary), + segmapStart: npsr.Ptr(heap, vms.CodeHeap.Segmap+vms.VirtualSpace.LowBoundary), + segmapEnd: npsr.Ptr(heap, vms.CodeHeap.Segmap+vms.VirtualSpace.HighBoundary), + } + + // Hook the memory area for HotSpot unwinder + if rng.codeStart == 0 || rng.codeEnd == 0 { + return nil, nil + } + + info.ranges = append(info.ranges, rng) + } + + return info, nil +} + +// addJitArea inserts an entry into the PID<->interpreter BPF map. +func (d *hotspotInstance) addJitArea(ebpf interpreter.EbpfHandler, + pid util.PID, area jitArea) error { + prefixes, err := lpm.CalculatePrefixList(uint64(area.start), uint64(area.end)) + if err != nil { + return fmt.Errorf("LPM prefix calculation error for %x-%x", area.start, area.end) + } + + for _, prefix := range prefixes { + if _, exists := d.prefixes[prefix]; exists { + continue + } + + if err = ebpf.UpdatePidInterpreterMapping(pid, prefix, + support.ProgUnwindHotspot, host.FileID(area.tsid), + uint64(area.codeStart)); err != nil { + return fmt.Errorf( + "failed to insert LPM entry for pid %d, page 0x%x/%d: %v", + pid, prefix.Key, prefix.Length, err) + } + + d.prefixes[prefix] = libpf.Void{} + } + + log.Debugf("HotSpot jitArea: pid: %d, code %x-%x tsid: %x (%d tries)", + pid, area.start, area.end, area.tsid, len(prefixes)) + + return nil +} + +// populateMainMappings populates all important BPF map entries that are available +// immediately after interpreter startup (once VM structs becomes available). This +// allows the BPF code to start unwinding even if some more detailed information +// about e.g. stub routines is not yet available. +func (d *hotspotInstance) populateMainMappings(vmd *hotspotVMData, + ebpf interpreter.EbpfHandler, pid util.PID) error { + if d.mainMappingsInserted { + // Already populated: nothing to do here. + return nil + } + + heap, err := d.gatherHeapInfo(vmd) + if err != nil { + return err + } + if heap == nil || len(heap.ranges) == 0 { + return nil + } + + // Construct and insert heap areas. + for _, rng := range heap.ranges { + tsid := (uint64(rng.segmapStart) & support.HSTSIDSegMapMask) << support.HSTSIDSegMapBit + + area := jitArea{ + start: rng.codeStart, + end: rng.codeEnd, + codeStart: rng.codeStart, + tsid: tsid, + } + + if err = d.addJitArea(ebpf, pid, area); err != nil { + return err + } + + d.heapAreas = append(d.heapAreas, area) + } + + // Set up the main eBPF info structure. + vms := &vmd.vmStructs + procInfo := C.HotspotProcInfo{ + compiledmethod_deopt_handler: C.u16(vms.CompiledMethod.DeoptHandlerBegin), + nmethod_compileid: C.u16(vms.Nmethod.CompileID), + nmethod_orig_pc_offset: C.u16(vms.Nmethod.OrigPcOffset), + codeblob_name: C.u8(vms.CodeBlob.Name), + codeblob_codestart: C.u8(vms.CodeBlob.CodeBegin), + codeblob_codeend: C.u8(vms.CodeBlob.CodeEnd), + codeblob_framecomplete: C.u8(vms.CodeBlob.FrameCompleteOffset), + codeblob_framesize: C.u8(vms.CodeBlob.FrameSize), + cmethod_size: C.u8(vms.ConstMethod.Sizeof), + heapblock_size: C.u8(vms.HeapBlock.Sizeof), + method_constmethod: C.u8(vms.Method.ConstMethod), + jvm_version: C.u8(vmd.version >> 24), + segment_shift: C.u8(heap.segmentShift), + } + + if vms.CodeCache.LowBound == 0 { + // JDK-8 has only one heap, use its bounds + procInfo.codecache_start = C.u64(heap.ranges[0].codeStart) + procInfo.codecache_end = C.u64(heap.ranges[0].codeEnd) + } else { + // JDK9+ the VM tracks it separately + procInfo.codecache_start = C.u64(d.rm.Ptr(vms.CodeCache.LowBound + d.bias)) + procInfo.codecache_end = C.u64(d.rm.Ptr(vms.CodeCache.HighBound + d.bias)) + } + + if err = ebpf.UpdateProcData(libpf.HotSpot, pid, unsafe.Pointer(&procInfo)); err != nil { + return err + } + + d.mainMappingsInserted = true + return nil +} + +// updateStubMappings adds new stub routines that are not yet tracked in our +// stubs map and, if necessary on the architecture, inserts unwinding instructions +// for them in the PID mappings BPF map. +func (d *hotspotInstance) updateStubMappings(vmd *hotspotVMData, + ebpf interpreter.EbpfHandler, pid util.PID) { + for _, stub := range findStubBounds(vmd, d.bias, d.rm) { + if _, exists := d.stubs[stub.start]; exists { + continue + } + + d.stubs[stub.start] = stub + + // Separate stub areas are only required on ARM64. + if runtime.GOARCH != "arm64" { + continue + } + + // Find corresponding heap jitArea. + var stubHeapArea *jitArea + for i := range d.heapAreas { + heapArea := &d.heapAreas[i] + if stub.start >= heapArea.start && stub.end <= heapArea.end { + stubHeapArea = heapArea + break + } + } + if stubHeapArea == nil { + log.Warnf("Unable to find heap for stub: pid = %d, stub.start = 0x%x", + pid, stub.start) + continue + } + + // Create and insert a jitArea for the stub. + stubArea, err := jitAreaForStubArm64(&stub, stubHeapArea, d.rm) + if err != nil { + log.Warnf("Failed to create JIT area for stub (pid = %d, stub.start = 0x%x): %v", + pid, stub.start, err) + continue + } + if err = d.addJitArea(ebpf, pid, stubArea); err != nil { + log.Warnf("Failed to insert JIT area for stub (pid = %d, stub.start = 0x%x): %v", + pid, stub.start, err) + continue + } + } +} + +func (d *hotspotInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler, + _ reporter.SymbolReporter, pr process.Process, _ []process.Mapping) error { + vmd, err := d.d.GetOrInit(func() (hotspotVMData, error) { return d.d.newVMData(d.rm, d.bias) }) + if err != nil { + return err + } + + // Check for permanent errors + if vmd.err != nil { + return vmd.err + } + + // Populate main mappings, if not done previously. + pid := pr.PID() + err = d.populateMainMappings(vmd, ebpf, pid) + if err != nil { + return err + } + if !d.mainMappingsInserted { + // Not ready yet: try later. + return nil + } + + d.updateStubMappings(vmd, ebpf, pid) + + return nil +} + +// Symbolize interpreters Hotspot eBPF uwinder given data containing target +// process address and translates it to static IDs expanding any inlined frames +// to multiple new frames. Associated symbolization metadata is extracted and +// queued to be sent to collection agent. +func (d *hotspotInstance) Symbolize(symbolReporter reporter.SymbolReporter, + frame *host.Frame, trace *libpf.Trace) error { + if !frame.Type.IsInterpType(libpf.HotSpot) { + return interpreter.ErrMismatchInterpreterType + } + + // Extract the HotSpot frame bitfields from the file and line variables + ptr := libpf.Address(frame.File) + subtype := uint32(frame.Lineno>>60) & 0xf + ripOrBci := int32(frame.Lineno>>32) & 0x0fffffff + ptrCheck := uint32(frame.Lineno) + + var err error + sfCounter := successfailurecounter.New(&d.successCount, &d.failCount) + defer sfCounter.DefaultToFailure() + + switch subtype { + case C.FRAME_HOTSPOT_STUB, C.FRAME_HOTSPOT_VTABLE: + // These are stub frames that may or may not be interesting + // to be seen in the trace. + stubID, err1 := d.getStubNameID(symbolReporter, ripOrBci, ptr, ptrCheck) + if err1 != nil { + return err + } + trace.AppendFrame(libpf.HotSpotFrame, hotspotStubsFileID, stubID) + case C.FRAME_HOTSPOT_INTERPRETER: + method, err1 := d.getMethod(ptr, ptrCheck) + if err1 != nil { + return err + } + err = method.symbolize(symbolReporter, ripOrBci, d, trace) + case C.FRAME_HOTSPOT_NATIVE: + jitinfo, err1 := d.getJITInfo(ptr, ptrCheck) + if err1 != nil { + return err1 + } + err = jitinfo.symbolize(symbolReporter, ripOrBci, d, trace) + default: + return fmt.Errorf("hotspot frame subtype %v is not supported", subtype) + } + + if err != nil { + return err + } + sfCounter.ReportSuccess() + return nil +} diff --git a/interpreter/hotspot/instance_test.go b/interpreter/hotspot/instance_test.go new file mode 100644 index 00000000..6cef54f8 --- /dev/null +++ b/interpreter/hotspot/instance_test.go @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package hotspot + +import ( + "encoding/binary" + "os" + "strings" + "testing" + "unsafe" + + "github.com/elastic/go-freelru" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/lpm" + "github.com/elastic/otel-profiling-agent/remotememory" + "github.com/elastic/otel-profiling-agent/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestJavaSymbolExtraction(t *testing.T) { + rm := remotememory.NewProcessVirtualMemory(util.PID(os.Getpid())) + id := hotspotData{} + vmd, _ := id.GetOrInit(func() (hotspotVMData, error) { + vmd := hotspotVMData{} + vmd.vmStructs.Symbol.Length = 2 + vmd.vmStructs.Symbol.Body = 4 + return vmd, nil + }) + + addrToSymbol, err := freelru.New[libpf.Address, string](2, libpf.Address.Hash32) + require.NoError(t, err, "symbol cache failed") + + ii := hotspotInstance{ + d: &id, + rm: rm, + addrToSymbol: addrToSymbol, + prefixes: libpf.Set[lpm.Prefix]{}, + stubs: map[libpf.Address]StubRoutine{}, + } + maxLength := 1024 + sym := make([]byte, vmd.vmStructs.Symbol.Body+uint(maxLength)) + str := strings.Repeat("a", maxLength) + copy(sym[vmd.vmStructs.Symbol.Body:], str) + for i := 0; i <= maxLength; i++ { + binary.LittleEndian.PutUint16(sym[vmd.vmStructs.Symbol.Length:], uint16(i)) + address := libpf.Address(uintptr(unsafe.Pointer(&sym[0]))) + got := ii.getSymbol(address) + assert.Equal(t, str[:i], got, "symbol length %d mismatched read", i) + ii.addrToSymbol.Purge() + } +} diff --git a/interpreter/hotspot/method.go b/interpreter/hotspot/method.go new file mode 100644 index 00000000..afaad4d1 --- /dev/null +++ b/interpreter/hotspot/method.go @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package hotspot + +import ( + "bytes" + "fmt" + + log "github.com/sirupsen/logrus" + + "github.com/elastic/otel-profiling-agent/libpf" + npsr "github.com/elastic/otel-profiling-agent/nopanicslicereader" + "github.com/elastic/otel-profiling-agent/reporter" + "github.com/elastic/otel-profiling-agent/util" +) + +// Constants for the JVM internals that have never changed +// nolint:golint,stylecheck,revive +const ConstMethod_has_linenumber_table = 0x0001 + +// hotspotMethod contains symbolization information for one Java method. It caches +// information from Hotspot class Method, the connected class ConstMethod, and +// chasing the pointers in the ConstantPool and other dynamic parts. +type hotspotMethod struct { + sourceFileName string + objectID libpf.FileID + methodName string + bytecodeSize uint16 + startLineNo uint16 + lineTable []byte + bciSeen libpf.Set[uint16] +} + +// Symbolize generates symbolization information for given hotspot method and +// a Byte Code Index (BCI) +func (m *hotspotMethod) symbolize(symbolReporter reporter.SymbolReporter, bci int32, + ii *hotspotInstance, trace *libpf.Trace) error { + // Make sure the BCI is within the method range + if bci < 0 || bci >= int32(m.bytecodeSize) { + bci = 0 + } + trace.AppendFrame(libpf.HotSpotFrame, m.objectID, libpf.AddressOrLineno(bci)) + + // Check if this is already symbolized + if _, ok := m.bciSeen[uint16(bci)]; ok { + return nil + } + + dec := ii.d.newUnsigned5Decoder(bytes.NewReader(m.lineTable)) + lineNo := dec.mapByteCodeIndexToLine(bci) + functionOffset := uint32(0) + if lineNo > util.SourceLineno(m.startLineNo) { + functionOffset = uint32(lineNo) - uint32(m.startLineNo) + } + + symbolReporter.FrameMetadata(m.objectID, + libpf.AddressOrLineno(bci), lineNo, functionOffset, + m.methodName, m.sourceFileName) + + // FIXME: The above FrameMetadata call might fail, but we have no idea of it + // due to the requests being queued and send attempts being done asynchronously. + // Until the reporting API gets a way to notify failures, just assume it worked. + m.bciSeen[uint16(bci)] = libpf.Void{} + + log.Debugf("[%d] [%x] %v+%v at %v:%v", len(trace.FrameTypes), + m.objectID, + m.methodName, functionOffset, + m.sourceFileName, lineNo) + + return nil +} + +// hotspotJITInfo contains symbolization and debug information for one JIT compiled +// method or JVM internal stub/function. The main JVM class it extracts the data +// from is class nmethod, and it caches the connected class Method and inlining info. +type hotspotJITInfo struct { + // compileID is the global unique id (running number) for this code blob + compileID uint32 + // method contains the Java method data for this JITted instance of it + method *hotspotMethod + // scopesPcs contains PC (RIP) to inlining scope mapping information + scopesPcs []byte + // scopesData contains information about inlined scopes + scopesData []byte + // metadata is the object addresses for the scopes data + metadata []byte +} + +// Symbolize parses JIT method inlining data and fills in symbolization information +// for each inlined method for given RIP. +func (ji *hotspotJITInfo) symbolize(symbolReporter reporter.SymbolReporter, ripDelta int32, + ii *hotspotInstance, trace *libpf.Trace) error { + // nolint:lll + // Unfortunately the data structures read here are not well documented in the JVM + // source, but for reference implementation you can look: + // https://hg.openjdk.java.net/jdk-updates/jdk14u/file/default/src/java.base/solaris/native/libjvm_db/libjvm_db.c + // Search for the functions: get_real_pc(), pc_desc_at(), scope_desc_at() and scopeDesc_chain(). + + // Conceptually, the JIT inlining information is kept in scopes_data as a linked + // list of [ nextScope, methodIndex, byteCodeOffset ] triplets. The innermost scope + // is resolved by looking it up from a table based on RIP (delta from function start). + + // Loop through the scopes_pcs table to map rip_delta to proper scope. + // It seems that the first entry is usually [-1, ] pair, + // so the below loop needs to handle negative pc_deltas correctly. + bestPCDelta := int32(-2) + scopeOff := uint32(0) + vms := &ii.d.Get().vmStructs + for i := uint(0); i < uint(len(ji.scopesPcs)); i += vms.PcDesc.Sizeof { + pcDelta := int32(npsr.Uint32(ji.scopesPcs, i+vms.PcDesc.PcOffset)) + if pcDelta >= bestPCDelta && pcDelta <= ripDelta { + bestPCDelta = pcDelta + scopeOff = npsr.Uint32(ji.scopesPcs, i+vms.PcDesc.ScopeDecodeOffset) + if pcDelta == ripDelta { + // Exact match of RIP to PC. Stop search. + // We could also record here that the symbolization + // result is "accurate" + break + } + } + } + + if scopeOff == 0 { + // It is possible that there is no debug info, or no scope information, + // for the given RIP. In this case we can provide the method name + // from the metadata. + return ji.method.symbolize(symbolReporter, 0, ii, trace) + } + + // Found scope data. Expand the inlined scope information from it. + var err error + maxScopeOff := uint32(len(ji.scopesData)) + for scopeOff != 0 && scopeOff < maxScopeOff { + // Keep track of the current scope offset, and use it as the next maximum + // offset. This makes sure the scope offsets decrease monotonically and + // this loop terminates. It has been verified empirically for this assumption + // to hold true, and it would be also very difficult for the JVM to generate + // forward references due to the variable length encoding used. + maxScopeOff = scopeOff + + // The scope data is three unsigned5 encoded integers + r := ii.d.newUnsigned5Decoder(bytes.NewReader(ji.scopesData[scopeOff:])) + scopeOff, err = r.getUint() + if err != nil { + return fmt.Errorf("failed to read next scope offset: %v", err) + } + methodIdx, err := r.getUint() + if err != nil { + return fmt.Errorf("failed to read method index: %v", err) + } + byteCodeIndex, err := r.getUint() + if err != nil { + return fmt.Errorf("failed to read bytecode index: %v", err) + } + + if byteCodeIndex > 0 { + // Analysis shows that the BCI stored in the scopes data + // is one larger than the BCI used by Interpreter or by + // the lookup tables. This is probably a bug in the JVM. + byteCodeIndex-- + } + + if methodIdx != 0 { + methodPtr := npsr.Ptr(ji.metadata, 8*uint(methodIdx-1)) + method, err := ii.getMethod(methodPtr, 0) + if err != nil { + return err + } + err = method.symbolize(symbolReporter, int32(byteCodeIndex), ii, trace) + if err != nil { + return err + } + } + } + return nil +} diff --git a/interpreter/hotspot/recordingreader.go b/interpreter/hotspot/recordingreader.go new file mode 100644 index 00000000..61f5cd5b --- /dev/null +++ b/interpreter/hotspot/recordingreader.go @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package hotspot + +import ( + "io" +) + +// RecordingReader allows reading data from the remote process using io.ReadByte interface. +// It provides basic buffering by reading memory in pieces of 'chunk' bytes and it also +// records all read memory in a backing buffer to be later stored as a whole. +type RecordingReader struct { + // reader is the ReaderAt from which we are reading the data from + reader io.ReaderAt + // buf contains all data read from the target process + buf []byte + // offs is the offset to continue reading from + offs int64 + // i is the index to the buf[] byte which is to be returned next in ReadByte() + i int + // chunk is the number of bytes to read from target process when mora data is needed + chunk int +} + +// ReadByte implements io.ByteReader interface to read memory single byte at a time. +func (rr *RecordingReader) ReadByte() (byte, error) { + // Readahead to buffer if needed + if rr.i >= len(rr.buf) { + buf := make([]byte, len(rr.buf)+rr.chunk) + copy(buf, rr.buf) + _, err := rr.reader.ReadAt(buf[len(rr.buf):], rr.offs) + if err != nil { + return 0, err + } + rr.offs += int64(rr.chunk) + rr.buf = buf + } + // Return byte from buffer + b := rr.buf[rr.i] + rr.i++ + return b, nil +} + +// GetBuffer returns all the data so far as a single slice. +func (rr *RecordingReader) GetBuffer() []byte { + return rr.buf[0:rr.i] +} + +// newRecordingReader returns a RecordingReader to read and record data from given start. +func newRecordingReader(reader io.ReaderAt, offs int64, chunkSize uint) *RecordingReader { + return &RecordingReader{ + reader: reader, + offs: offs, + chunk: int(chunkSize), + } +} diff --git a/interpreter/hotspot/recordingreader_test.go b/interpreter/hotspot/recordingreader_test.go new file mode 100644 index 00000000..d2d0b3a8 --- /dev/null +++ b/interpreter/hotspot/recordingreader_test.go @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package hotspot + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRecordingReader(t *testing.T) { + data := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} + + rr := newRecordingReader(bytes.NewReader(data), 0, 2) + for i := 0; i < len(data)-1; i++ { + b, err := rr.ReadByte() + require.NoError(t, err) + assert.Equal(t, data[i], b) + } + assert.Len(t, rr.GetBuffer(), len(data)-1) +} diff --git a/interpreter/hotspot/stubs.go b/interpreter/hotspot/stubs.go index 15556938..9be7f6f0 100644 --- a/interpreter/hotspot/stubs.go +++ b/interpreter/hotspot/stubs.go @@ -13,12 +13,13 @@ import ( "sort" "strings" - "github.com/elastic/otel-profiling-agent/debug/log" + "github.com/elastic/otel-profiling-agent/armhelpers" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/armhelpers" - "github.com/elastic/otel-profiling-agent/libpf/remotememory" + "github.com/elastic/otel-profiling-agent/remotememory" "github.com/elastic/otel-profiling-agent/support" aa "golang.org/x/arch/arm64/arm64asm" + + log "github.com/sirupsen/logrus" ) // nextAligned aligns a pointer up, to the next multiple of align. diff --git a/interpreter/hotspot/unsigned5.go b/interpreter/hotspot/unsigned5.go new file mode 100644 index 00000000..def68bb3 --- /dev/null +++ b/interpreter/hotspot/unsigned5.go @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package hotspot + +import ( + "fmt" + "io" + + "github.com/elastic/otel-profiling-agent/util" +) + +// unsigned5Decoder is a decoder for UNSIGNED5 based byte streams. +type unsigned5Decoder struct { + // r is the byte reader interface to read from + r io.ByteReader + + // x is the number of exclusion bytes in encoding (JDK20+) + x uint8 +} + +// getUint decodes one "standard" J2SE Pack200 UNSIGNED5 number +func (d *unsigned5Decoder) getUint() (uint32, error) { + const L = uint8(192) + x := d.x + r := d.r + + ch, err := r.ReadByte() + if err != nil { + return 0, err + } + if ch < x { + return 0, fmt.Errorf("byte %#x is in excluded range", ch) + } + + sum := uint32(ch - x) + for shift := 6; ch >= L && shift < 30; shift += 6 { + ch, err = r.ReadByte() + if err != nil { + return 0, err + } + if ch < x { + return 0, fmt.Errorf("byte %#x is in excluded range", ch) + } + sum += uint32(ch-x) << shift + } + return sum, nil +} + +// getSigned decodes one signed number +func (d *unsigned5Decoder) getSigned() (int32, error) { + val, err := d.getUint() + if err != nil { + return 0, err + } + return int32(val>>1) ^ -int32(val&1), nil +} + +// decodeLineTableEntry incrementally parses one line-table entry consisting of the source +// line number and a byte code index (BCI) from the decoder. The delta encoded line +// table format is specific to HotSpot VM which compresses the unpacked class file line +// tables during class loading. +func (d *unsigned5Decoder) decodeLineTableEntry(bci, line *uint32) error { + b, err := d.r.ReadByte() + if err != nil { + return fmt.Errorf("failed to read line table: %v", err) + } + switch b { + case 0x00: // End-of-Stream + return io.EOF + case 0xff: // Escape for long deltas + val, err := d.getSigned() + if err != nil { + return fmt.Errorf("failed to read byte code index delta: %v", err) + } + *bci += uint32(val) + val, err = d.getSigned() + if err != nil { + return fmt.Errorf("failed to read line number delta: %v", err) + } + *line += uint32(val) + default: // Short encoded delta + *bci += uint32(b >> 3) + *line += uint32(b & 7) + } + return nil +} + +// mapByteCodeIndexToLine decodes a line table to map a given Byte Code Index (BCI) +// to a line number +func (d *unsigned5Decoder) mapByteCodeIndexToLine(bci int32) util.SourceLineno { + // The line numbers array is a short array of 2-tuples [start_pc, line_number]. + // Not necessarily sorted. Encoded as delta-encoded numbers. + var curBci, curLine, bestBci, bestLine uint32 + + for d.decodeLineTableEntry(&curBci, &curLine) == nil { + if curBci == uint32(bci) { + return util.SourceLineno(curLine) + } + if curBci >= bestBci && curBci < uint32(bci) { + bestBci = curBci + bestLine = curLine + } + } + return util.SourceLineno(bestLine) +} diff --git a/interpreter/hotspot/unsigned5_test.go b/interpreter/hotspot/unsigned5_test.go new file mode 100644 index 00000000..b63153bc --- /dev/null +++ b/interpreter/hotspot/unsigned5_test.go @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package hotspot + +import ( + "bytes" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestJavaLineNumbers tests that the Hotspot delta encoded line table decoding works. +// The set here is an actually table extracting from JVM. It is fairly easy to encode +// these numbers if needed, but we don't need to generate them currently for anything. +func TestJavaLineNumbers(t *testing.T) { + bciLine := []struct { + bci, line uint32 + }{ + {0, 478}, + {5, 479}, + {9, 480}, + {19, 481}, + {26, 482}, + {33, 483}, + {47, 490}, + {50, 485}, + {52, 486}, + {58, 490}, + {61, 488}, + {63, 489}, + {68, 491}, + } + + decoder := unsigned5Decoder{ + r: bytes.NewReader([]byte{ + 255, 0, 252, 11, 41, 33, 81, 57, 57, 119, + 255, 6, 9, 17, 52, 255, 6, 3, 17, 42, 0}), + } + + var bci, line uint32 + for i := 0; i < len(bciLine); i++ { + err := decoder.decodeLineTableEntry(&bci, &line) + require.NoError(t, err) + assert.Equal(t, bciLine[i].bci, bci) + assert.Equal(t, bciLine[i].line, line) + } + err := decoder.decodeLineTableEntry(&bci, &line) + assert.ErrorIs(t, err, io.EOF, "line table not empty at end") +} diff --git a/interpreter/instancestubs.go b/interpreter/instancestubs.go index 543e626f..0de3ff7d 100644 --- a/interpreter/instancestubs.go +++ b/interpreter/instancestubs.go @@ -9,10 +9,11 @@ package interpreter import ( "github.com/elastic/otel-profiling-agent/host" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/process" "github.com/elastic/otel-profiling-agent/metrics" + "github.com/elastic/otel-profiling-agent/process" "github.com/elastic/otel-profiling-agent/reporter" "github.com/elastic/otel-profiling-agent/tpbase" + "github.com/elastic/otel-profiling-agent/util" ) // InstanceStubs provides empty implementations of Instance hooks that are @@ -25,7 +26,7 @@ func (is *InstanceStubs) SynchronizeMappings(EbpfHandler, reporter.SymbolReporte return nil } -func (is *InstanceStubs) UpdateTSDInfo(EbpfHandler, libpf.PID, tpbase.TSDInfo) error { +func (is *InstanceStubs) UpdateTSDInfo(EbpfHandler, util.PID, tpbase.TSDInfo) error { return nil } diff --git a/interpreter/loaderinfo.go b/interpreter/loaderinfo.go index bb0a6653..dd551d0b 100644 --- a/interpreter/loaderinfo.go +++ b/interpreter/loaderinfo.go @@ -12,6 +12,7 @@ import ( "github.com/elastic/otel-profiling-agent/host" "github.com/elastic/otel-profiling-agent/libpf" "github.com/elastic/otel-profiling-agent/libpf/pfelf" + "github.com/elastic/otel-profiling-agent/util" ) // LoaderInfo contains information about an ELF that is passed to @@ -22,11 +23,11 @@ type LoaderInfo struct { // elfRef provides a cached access to the ELF file. elfRef *pfelf.Reference // gaps represents holes in the stack deltas of the executable. - gaps []libpf.Range + gaps []util.Range } // NewLoaderInfo returns a populated LoaderInfo struct. -func NewLoaderInfo(fileID host.FileID, elfRef *pfelf.Reference, gaps []libpf.Range) *LoaderInfo { +func NewLoaderInfo(fileID host.FileID, elfRef *pfelf.Reference, gaps []util.Range) *LoaderInfo { return &LoaderInfo{ fileID: fileID, elfRef: elfRef, @@ -40,7 +41,7 @@ func (i *LoaderInfo) GetELF() (*pfelf.File, error) { } // GetSymbolAsRanges returns the normalized virtual address ranges for the named symbol -func (i *LoaderInfo) GetSymbolAsRanges(symbol libpf.SymbolName) ([]libpf.Range, error) { +func (i *LoaderInfo) GetSymbolAsRanges(symbol libpf.SymbolName) ([]util.Range, error) { ef, err := i.GetELF() if err != nil { return nil, err @@ -50,7 +51,7 @@ func (i *LoaderInfo) GetSymbolAsRanges(symbol libpf.SymbolName) ([]libpf.Range, return nil, fmt.Errorf("symbol '%v' not found: %w", symbol, err) } start := uint64(sym.Address) - return []libpf.Range{{ + return []util.Range{{ Start: start, End: start + uint64(sym.Size)}, }, nil @@ -67,6 +68,6 @@ func (i *LoaderInfo) FileName() string { } // Gaps returns the gaps for the executable of this LoaderInfo. -func (i *LoaderInfo) Gaps() []libpf.Range { +func (i *LoaderInfo) Gaps() []util.Range { return i.gaps } diff --git a/interpreter/nodev8/v8.go b/interpreter/nodev8/v8.go index ab616990..30959a4e 100644 --- a/interpreter/nodev8/v8.go +++ b/interpreter/nodev8/v8.go @@ -155,6 +155,7 @@ package nodev8 import ( "bytes" + "errors" "fmt" "hash/fnv" "io" @@ -167,20 +168,21 @@ import ( log "github.com/sirupsen/logrus" + "github.com/elastic/go-freelru" + "github.com/elastic/otel-profiling-agent/host" "github.com/elastic/otel-profiling-agent/interpreter" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/freelru" - npsr "github.com/elastic/otel-profiling-agent/libpf/nopanicslicereader" "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/elastic/otel-profiling-agent/libpf/process" - "github.com/elastic/otel-profiling-agent/libpf/remotememory" - "github.com/elastic/otel-profiling-agent/libpf/successfailurecounter" "github.com/elastic/otel-profiling-agent/lpm" "github.com/elastic/otel-profiling-agent/metrics" + npsr "github.com/elastic/otel-profiling-agent/nopanicslicereader" + "github.com/elastic/otel-profiling-agent/process" + "github.com/elastic/otel-profiling-agent/remotememory" "github.com/elastic/otel-profiling-agent/reporter" + "github.com/elastic/otel-profiling-agent/successfailurecounter" "github.com/elastic/otel-profiling-agent/support" - "go.uber.org/multierr" + "github.com/elastic/otel-profiling-agent/util" ) // #include "../../support/ebpf/types.h" @@ -455,7 +457,7 @@ type v8Data struct { } // snapshotRange is the LOAD segment area where V8 Snapshot code blob is - snapshotRange libpf.Range + snapshotRange util.Range // version contains the V8 version version uint32 @@ -523,17 +525,17 @@ type v8SFI struct { bytecode []byte funcName string funcID libpf.FileID - funcStartLine libpf.SourceLineno + funcStartLine util.SourceLineno funcStartPos int funcEndPos int bytecodeLength uint32 } -func (i *v8Instance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error { +func (i *v8Instance) Detach(ebpf interpreter.EbpfHandler, pid util.PID) error { err := ebpf.DeleteProcData(libpf.V8, pid) for prefix := range i.prefixes { if err2 := ebpf.DeletePidInterpreterMapping(pid, prefix); err2 != nil { - err = multierr.Append(err, + err = errors.Join(err, fmt.Errorf("failed to remove page 0x%x/%d: %v", prefix.Key, prefix.Length, err2)) } @@ -602,10 +604,10 @@ func (i *v8Instance) SynchronizeMappings(ebpf interpreter.EbpfHandler, } func (i *v8Instance) GetAndResetMetrics() ([]metrics.Metric, error) { - addrToStringStats := i.addrToString.GetAndResetStatistics() - addrToSFIStats := i.addrToSFI.GetAndResetStatistics() - addrToCodeStats := i.addrToCode.GetAndResetStatistics() - addrToSourceStats := i.addrToSource.GetAndResetStatistics() + addrToStringStats := i.addrToString.ResetMetrics() + addrToSFIStats := i.addrToSFI.ResetMetrics() + addrToCodeStats := i.addrToCode.ResetMetrics() + addrToSourceStats := i.addrToSource.ResetMetrics() return []metrics.Metric{ { @@ -618,67 +620,67 @@ func (i *v8Instance) GetAndResetMetrics() ([]metrics.Metric, error) { }, { ID: metrics.IDV8AddrToStringHit, - Value: metrics.MetricValue(addrToStringStats.Hit), + Value: metrics.MetricValue(addrToStringStats.Hits), }, { ID: metrics.IDV8AddrToStringMiss, - Value: metrics.MetricValue(addrToStringStats.Miss), + Value: metrics.MetricValue(addrToStringStats.Misses), }, { ID: metrics.IDV8AddrToStringAdd, - Value: metrics.MetricValue(addrToStringStats.Added), + Value: metrics.MetricValue(addrToStringStats.Inserts), }, { ID: metrics.IDV8AddrToStringDel, - Value: metrics.MetricValue(addrToStringStats.Deleted), + Value: metrics.MetricValue(addrToStringStats.Removals), }, { ID: metrics.IDV8AddrToSFIHit, - Value: metrics.MetricValue(addrToSFIStats.Hit), + Value: metrics.MetricValue(addrToSFIStats.Hits), }, { ID: metrics.IDV8AddrToSFIMiss, - Value: metrics.MetricValue(addrToSFIStats.Miss), + Value: metrics.MetricValue(addrToSFIStats.Misses), }, { ID: metrics.IDV8AddrToSFIAdd, - Value: metrics.MetricValue(addrToSFIStats.Added), + Value: metrics.MetricValue(addrToSFIStats.Inserts), }, { ID: metrics.IDV8AddrToSFIDel, - Value: metrics.MetricValue(addrToSFIStats.Deleted), + Value: metrics.MetricValue(addrToSFIStats.Removals), }, { ID: metrics.IDV8AddrToFuncHit, - Value: metrics.MetricValue(addrToCodeStats.Hit), + Value: metrics.MetricValue(addrToCodeStats.Hits), }, { ID: metrics.IDV8AddrToFuncMiss, - Value: metrics.MetricValue(addrToCodeStats.Miss), + Value: metrics.MetricValue(addrToCodeStats.Misses), }, { ID: metrics.IDV8AddrToFuncAdd, - Value: metrics.MetricValue(addrToCodeStats.Added), + Value: metrics.MetricValue(addrToCodeStats.Inserts), }, { ID: metrics.IDV8AddrToFuncDel, - Value: metrics.MetricValue(addrToCodeStats.Deleted), + Value: metrics.MetricValue(addrToCodeStats.Removals), }, { ID: metrics.IDV8AddrToSourceHit, - Value: metrics.MetricValue(addrToSourceStats.Hit), + Value: metrics.MetricValue(addrToSourceStats.Hits), }, { ID: metrics.IDV8AddrToSourceMiss, - Value: metrics.MetricValue(addrToSourceStats.Miss), + Value: metrics.MetricValue(addrToSourceStats.Misses), }, { ID: metrics.IDV8AddrToSourceAdd, - Value: metrics.MetricValue(addrToSourceStats.Added), + Value: metrics.MetricValue(addrToSourceStats.Inserts), }, { ID: metrics.IDV8AddrToSourceDel, - Value: metrics.MetricValue(addrToSourceStats.Deleted), + Value: metrics.MetricValue(addrToSourceStats.Removals), }, }, nil } @@ -707,13 +709,13 @@ func isHeapObject(val libpf.Address) bool { } // calculateAndSymbolizeStubID calculates the hash for a given string, and symbolizes it. -func (i *v8Instance) calculateAndSymbolizeStubID(symbolizer interpreter.Symbolizer, +func (i *v8Instance) calculateAndSymbolizeStubID(symbolReporter reporter.SymbolReporter, name string) libpf.AddressOrLineno { h := fnv.New128a() _, _ = h.Write([]byte(name)) nameHash := h.Sum(nil) stubID := libpf.AddressOrLineno(npsr.Uint64(nameHash, 0)) - symbolizer.FrameMetadata(v8StubsFileID, stubID, 0, 0, name, "") + symbolReporter.FrameMetadata(v8StubsFileID, stubID, 0, 0, name, "") return stubID } @@ -724,7 +726,7 @@ func insertFrame(trace *libpf.Trace, fileID libpf.FileID, line libpf.AddressOrLi } // symbolizeMarkerFrame symbolizes and adds to trace a V8 stub frame -func (i *v8Instance) symbolizeMarkerFrame(symbolizer interpreter.Symbolizer, marker uint64, +func (i *v8Instance) symbolizeMarkerFrame(symbolReporter reporter.SymbolReporter, marker uint64, trace *libpf.Trace) error { if marker >= MaxFrameType { return fmt.Errorf("v8 tracer returned invalid marker: %d", marker) @@ -741,7 +743,7 @@ func (i *v8Instance) symbolizeMarkerFrame(symbolizer interpreter.Symbolizer, mar break } } - stubID = i.calculateAndSymbolizeStubID(symbolizer, name) + stubID = i.calculateAndSymbolizeStubID(symbolReporter, name) i.d.frametypeToID[marker] = stubID log.Debugf("[%d] V8 marker %v is %s, stubID %x", len(trace.FrameTypes), @@ -856,7 +858,7 @@ func (i *v8Instance) extractString(ptr libpf.Address, tag uint16, cb func(string } } case vms.Fixed.TwoByteStringTag: - return fmt.Errorf("two byte string not supported") + return errors.New("two byte string not supported") default: return fmt.Errorf("unsupported encoding: %#x", tag) } @@ -901,7 +903,7 @@ func (i *v8Instance) getString(ptr libpf.Address, tag uint16) (string, error) { if err != nil { return "", err } - if str != "" && !libpf.IsValidString(str) { + if str != "" && !util.IsValidString(str) { return "", fmt.Errorf("invalid string at 0x%x", ptr) } @@ -1188,7 +1190,7 @@ func (i *v8Instance) readCode(taggedPtr libpf.Address, cookie uint32, sfi *v8SFI // Baseline Code does not have deoptimization data if codeKind == vms.CodeKind.Baseline { if sfi == nil { - return nil, fmt.Errorf("baseline function without SFI") + return nil, errors.New("baseline function without SFI") } log.Debugf("Baseline Code %#x read: posSize: %v, cookie: %x", @@ -1431,7 +1433,7 @@ func decodePosition(table []byte, delta uint64) sourcePosition { // mapPositionToLine maps a file position (byte offset) to a line number. This is // done against a table containing a offsets where each line ends. -func mapPositionToLine(lineEnds []uint32, pos int32) libpf.SourceLineno { +func mapPositionToLine(lineEnds []uint32, pos int32) util.SourceLineno { if len(lineEnds) == 0 || pos < 0 { return 0 } @@ -1439,11 +1441,11 @@ func mapPositionToLine(lineEnds []uint32, pos int32) libpf.SourceLineno { index := sort.Search(len(lineEnds), func(ndx int) bool { return lineEnds[ndx] >= uint32(pos) }) - return libpf.SourceLineno(index + 1) + return util.SourceLineno(index + 1) } // scriptOffsetToLine maps a sourcePosition to a line number in the corresponding source -func (sfi *v8SFI) scriptOffsetToLine(position sourcePosition) libpf.SourceLineno { +func (sfi *v8SFI) scriptOffsetToLine(position sourcePosition) util.SourceLineno { scriptOffset := position.scriptOffset() // The scriptOffset is offset by one, to make kNoSourcePosition zero. // nolint:lll @@ -1455,14 +1457,14 @@ func (sfi *v8SFI) scriptOffsetToLine(position sourcePosition) libpf.SourceLineno } // symbolize symbolizes the raw frame data -func (i *v8Instance) symbolize(symbolizer interpreter.Symbolizer, sfi *v8SFI, - pos libpf.AddressOrLineno, lineNo libpf.SourceLineno) { +func (i *v8Instance) symbolize(symbolReporter reporter.SymbolReporter, sfi *v8SFI, + pos libpf.AddressOrLineno, lineNo util.SourceLineno) { funcOffset := uint32(0) if lineNo > sfi.funcStartLine { funcOffset = uint32(lineNo - sfi.funcStartLine) } - symbolizer.FrameMetadata( + symbolReporter.FrameMetadata( sfi.funcID, pos, lineNo, funcOffset, sfi.funcName, sfi.source.fileName) @@ -1473,7 +1475,7 @@ func (i *v8Instance) symbolize(symbolizer interpreter.Symbolizer, sfi *v8SFI, } // generateNativeFrame and conditionally symbolizes a native frame. -func (i *v8Instance) generateNativeFrame(symbolizer interpreter.Symbolizer, +func (i *v8Instance) generateNativeFrame(symbolReporter reporter.SymbolReporter, sourcePos sourcePosition, sfi *v8SFI, seen bool, trace *libpf.Trace) { if sourcePos.isExternal() { @@ -1481,7 +1483,7 @@ func (i *v8Instance) generateNativeFrame(symbolizer interpreter.Symbolizer, // Just generate a place holder stub frame for external reference. if i.d.externalStubID == 0 { i.d.externalStubID = i.calculateAndSymbolizeStubID( - symbolizer, "") + symbolReporter, "") } insertFrame(trace, v8StubsFileID, i.d.externalStubID) return @@ -1491,25 +1493,25 @@ func (i *v8Instance) generateNativeFrame(symbolizer interpreter.Symbolizer, addressOrLineno := libpf.AddressOrLineno(lineNo) + nativeCodeBaseAddress insertFrame(trace, sfi.funcID, addressOrLineno) if !seen { - i.symbolize(symbolizer, sfi, addressOrLineno, lineNo) + i.symbolize(symbolReporter, sfi, addressOrLineno, lineNo) } } // symbolizeBytecode symbolizes and records to a trace a Bytecode based frame. -func (i *v8Instance) symbolizeBytecode(symbolizer interpreter.Symbolizer, sfi *v8SFI, +func (i *v8Instance) symbolizeBytecode(symbolReporter reporter.SymbolReporter, sfi *v8SFI, delta uint64, trace *libpf.Trace) error { insertFrame(trace, sfi.funcID, libpf.AddressOrLineno(delta)) if _, ok := sfi.bytecodeDeltaSeen[uint32(delta)]; !ok { sourcePos := decodePosition(sfi.bytecodePositionTable, delta) lineNo := sfi.scriptOffsetToLine(sourcePos) - i.symbolize(symbolizer, sfi, libpf.AddressOrLineno(delta), lineNo) + i.symbolize(symbolReporter, sfi, libpf.AddressOrLineno(delta), lineNo) sfi.bytecodeDeltaSeen[uint32(delta)] = libpf.Void{} } return nil } // symbolizeSFI symbolizes and records to a trace a SharedFunctionInfo based frame. -func (i *v8Instance) symbolizeSFI(symbolizer interpreter.Symbolizer, pointer libpf.Address, +func (i *v8Instance) symbolizeSFI(symbolReporter reporter.SymbolReporter, pointer libpf.Address, delta uint64, trace *libpf.Trace) error { vms := &i.d.vmStructs sfi, err := i.getSFI(pointer) @@ -1529,7 +1531,7 @@ func (i *v8Instance) symbolizeSFI(symbolizer interpreter.Symbolizer, pointer lib // Invalid value bytecodeDelta = nativeCodeBaseAddress - 1 } - return i.symbolizeBytecode(symbolizer, sfi, uint64(bytecodeDelta), trace) + return i.symbolizeBytecode(symbolReporter, sfi, uint64(bytecodeDelta), trace) } // getBytecodeLength decodes the length at the start of bytecode array @@ -1619,7 +1621,7 @@ func (i *v8Instance) mapBaselineCodeOffsetToBytecode(code *v8Code, pcDelta uint3 } // symbolizeBaselineCode symbolizes and records to a trace a Baseline Code based frame. -func (i *v8Instance) symbolizeBaselineCode(symbolizer interpreter.Symbolizer, code *v8Code, +func (i *v8Instance) symbolizeBaselineCode(symbolReporter reporter.SymbolReporter, code *v8Code, delta uint32, trace *libpf.Trace) error { if bytecodeDelta, ok := code.codeDeltaToPosition[delta]; ok { // We've seen this frame before, so just insert the frame @@ -1630,12 +1632,12 @@ func (i *v8Instance) symbolizeBaselineCode(symbolizer interpreter.Symbolizer, co // Decode bytecode delta, memoize it, and symbolize frame bytecodeDelta := i.mapBaselineCodeOffsetToBytecode(code, delta) code.codeDeltaToPosition[delta] = sourcePosition(bytecodeDelta) - return i.symbolizeBytecode(symbolizer, code.sfi, uint64(bytecodeDelta), trace) + return i.symbolizeBytecode(symbolReporter, code.sfi, uint64(bytecodeDelta), trace) } // symbolizeCode symbolizes and records to a trace a Code based frame. -func (i *v8Instance) symbolizeCode(symbolizer interpreter.Symbolizer, code *v8Code, delta uint64, - trace *libpf.Trace) error { +func (i *v8Instance) symbolizeCode(symbolReporter reporter.SymbolReporter, code *v8Code, + delta uint64, trace *libpf.Trace) error { var err error sfi := code.sfi delta &= C.V8_LINE_DELTA_MASK @@ -1647,7 +1649,7 @@ func (i *v8Instance) symbolizeCode(symbolizer interpreter.Symbolizer, code *v8Co } if code.isBaseline { - return i.symbolizeBaselineCode(symbolizer, code, uint32(delta), trace) + return i.symbolizeBaselineCode(symbolReporter, code, uint32(delta), trace) } // Memoize the delta to position mapping to improve speed. We can't just @@ -1680,7 +1682,7 @@ func (i *v8Instance) symbolizeCode(symbolizer interpreter.Symbolizer, code *v8Co return fmt.Errorf("failed to get inlined SFI: %w", err) } } - i.generateNativeFrame(symbolizer, sourcePos, inlinedSFI, deltaSeen, trace) + i.generateNativeFrame(symbolReporter, sourcePos, inlinedSFI, deltaSeen, trace) sourcePos = sourcePosition(npsr.Uint64(code.inliningPositions, itemOff)) if sourcePos.inliningID() > inliningID { @@ -1690,7 +1692,7 @@ func (i *v8Instance) symbolizeCode(symbolizer interpreter.Symbolizer, code *v8Co sourcePos.inliningID(), inliningID) } } - i.generateNativeFrame(symbolizer, sourcePos, sfi, deltaSeen, trace) + i.generateNativeFrame(symbolReporter, sourcePos, sfi, deltaSeen, trace) return nil } @@ -1756,7 +1758,7 @@ func mapFramePointerOffset(relBytes uint8) C.u8 { return C.u8(slotOffset) } -func (d *v8Data) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, _ libpf.Address, +func (d *v8Data) Attach(ebpf interpreter.EbpfHandler, pid util.PID, _ libpf.Address, rm remotememory.RemoteMemory) (interpreter.Instance, error) { vms := &d.vmStructs data := C.V8ProcInfo{ @@ -2120,7 +2122,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr vms.Fixed.HeapObjectTagMask != HeapObjectTagMask || vms.Fixed.SmiTag != SmiTag || vms.Fixed.SmiTagMask != SmiTagMask || vms.Fixed.SmiShiftSize != SmiValueShift-SmiTagShift { - return nil, fmt.Errorf("incompatible tagging scheme") + return nil, errors.New("incompatible tagging scheme") } if mapFramePointerOffset(vms.FramePointer.Context) >= C.V8_FP_CONTEXT_SIZE || @@ -2133,7 +2135,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr if d.snapshotRange.Start != 0 { if err = ebpf.UpdateInterpreterOffsets(support.ProgUnwindV8, info.FileID(), - []libpf.Range{d.snapshotRange}); err != nil { + []util.Range{d.snapshotRange}); err != nil { return nil, err } } diff --git a/interpreter/perl/data.go b/interpreter/perl/data.go new file mode 100644 index 00000000..3b88b95a --- /dev/null +++ b/interpreter/perl/data.go @@ -0,0 +1,305 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package perl + +import ( + "fmt" + "sync" + + "github.com/elastic/go-freelru" + log "github.com/sirupsen/logrus" + + "github.com/elastic/otel-profiling-agent/interpreter" + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/libpf/pfelf" + "github.com/elastic/otel-profiling-agent/remotememory" + "github.com/elastic/otel-profiling-agent/support" + "github.com/elastic/otel-profiling-agent/util" +) + +// #include "../../support/ebpf/types.h" +import "C" + +type perlData struct { + // vmStructs reflects the Perl internal class names and the offsets of named field + // The struct names are based on the Perl C "struct name", the alternate typedef seen + // mostly in code is in parenthesis. + // nolint:golint,stylecheck,revive + vmStructs struct { + // interpreter struct (PerlInterpreter) is defined in intrpvar.h via macro trickery + // https://github.com/Perl/perl5/blob/v5.32.0/intrpvar.h + interpreter struct { + curcop uint + curstackinfo uint + } + // stackinfo struct (PERL_SI) is defined in cop.h + // https://github.com/Perl/perl5/blob/v5.32.0/cop.h#L1037-L1055 + stackinfo struct { + si_cxstack uint + si_next uint + si_cxix uint + si_type uint + } + // context struct (PERL_CONTEXT) is defined in cop.h + // https://github.com/Perl/perl5/blob/v5.32.0/cop.h#L878-L884 + context struct { + cx_type uint + blk_oldcop uint + blk_sub_retop uint + blk_sub_cv uint + sizeof uint + } + // cop struct (COP), a "control op" is defined in cop.h + // https://github.com/Perl/perl5/blob/v5.32.0/cop.h#L397-L424 + cop struct { + cop_line uint + cop_file uint + sizeof uint + } + // sv struct (SV) is "Scalar Value", the generic "base" for all + // perl variants, and is horrendously cast to other types as needed. + // https://github.com/Perl/perl5/blob/v5.32.0/sv.h#L233-L236 + sv struct { + sv_any uint + sv_flags uint + svu_gp uint + svu_hash uint + sizeof uint + } + // xpvcv struct (XPVCV) is "Code Value object" (the data PV points to) + // https://github.com/Perl/perl5/blob/v5.32.0/cv.h#L13-L16 + xpvcv struct { + xcv_flags uint + xcv_gv uint + } + // xpvgv struct (XPVGV) is "Glob Value object" (the data GV points to) + // https://github.com/Perl/perl5/blob/v5.32.0/sv.h#L571-L575 + xpvgv struct { + xivu_namehek uint + xgv_stash uint + } + // xpvhv struct (XPVHV) is a "Hash Value" (that is the Hash struct) + // https://github.com/Perl/perl5/blob/v5.32.0/hv.h#L135-L140 + xpvhv struct { + xhv_max uint + } + // xpvhv_with_aux is the successor of XPVHV starting in Perl 5.36. + // https://github.com/Perl/perl5/blob/v5.36.0/hv.h#L149-L155 + xpvhv_with_aux struct { + xpvhv_aux uint + } + // xpvhv_aux struct is the Hash ancillary data structure + // https://github.com/Perl/perl5/blob/v5.32.0/hv.h#L108-L128 + xpvhv_aux struct { + xhv_name_u uint + xhv_name_count uint + sizeof uint + pointer_size uint + } + // gp struct (GP) is apparently "Glob Private", essentially a function definition + // https://github.com/Perl/perl5/blob/v5.32.0/gv.h#L11-L24 + gp struct { + gp_egv uint + } + // hek struct (HEK) is "Hash Entry Key", a hash/len/key triplet + // https://github.com/Perl/perl5/blob/v5.32.0/hv.h#L44-L57 + hek struct { + hek_len uint + hek_key uint + } + } + + // stateAddr is the address of the Perl state address (TSD or global) + stateAddr libpf.SymbolValue + + // version contains the Perl version + version uint32 + + // stateInTSD is set if the we have state TSD key address + stateInTSD bool +} + +func (d *perlData) String() string { + ver := d.version + return fmt.Sprintf("Perl %d.%d.%d", (ver>>16)&0xff, (ver>>8)&0xff, ver&0xff) +} + +func (d *perlData) Attach(_ interpreter.EbpfHandler, _ util.PID, bias libpf.Address, + rm remotememory.RemoteMemory) (interpreter.Instance, error) { + addrToHEK, err := freelru.New[libpf.Address, string](interpreter.LruFunctionCacheSize, + libpf.Address.Hash32) + if err != nil { + return nil, err + } + + addrToCOP, err := freelru.New[copKey, *perlCOP](interpreter.LruFunctionCacheSize*8, + hashCOPKey) + if err != nil { + return nil, err + } + + addrToGV, err := freelru.New[libpf.Address, string](interpreter.LruFunctionCacheSize, + libpf.Address.Hash32) + if err != nil { + return nil, err + } + + return &perlInstance{ + d: d, + rm: rm, + bias: C.u64(bias), + addrToHEK: addrToHEK, + addrToCOP: addrToCOP, + addrToGV: addrToGV, + memPool: sync.Pool{ + New: func() any { + // To avoid resizing of the returned byte slize we size new + // allocations to hekLenLimit. + buf := make([]byte, hekLenLimit) + return &buf + }, + }, + }, nil +} + +func newData(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo, + ef *pfelf.File) (*perlData, error) { + // The version is encoded in these globals since Perl 5.15.0. + // https://github.com/Perl/perl5/blob/v5.32.0/perl.h#L4745-L4754 + var verBytes [3]byte + for i, sym := range []libpf.SymbolName{"PL_revision", "PL_version", "PL_subversion"} { + addr, err := ef.LookupSymbolAddress(sym) + if err == nil { + _, err = ef.ReadVirtualMemory(verBytes[i:i+1], int64(addr)) + } + if err != nil { + return nil, fmt.Errorf("perl symbol '%s': %v", sym, err) + } + } + + version := perlVersion(verBytes[0], verBytes[1], verBytes[2]) + log.Debugf("Perl version %v.%v.%v", verBytes[0], verBytes[1], verBytes[2]) + + // Currently tested and supported 5.28.x - 5.38.x. + // Could possibly support older Perl versions somewhere back to 5.14-5.20, by just + // checking the introspection offset validity. 5.14 had major rework for internals. + // And 5.18 had some HV related changes. + minVer := perlVersion(5, 28, 0) + maxVer := perlVersion(5, 39, 0) + if version < minVer || version >= maxVer { + return nil, fmt.Errorf("unsupported Perl %d.%d.%d (need >= %d.%d and < %d.%d)", + verBytes[0], verBytes[1], verBytes[2], + (minVer>>16)&0xff, (minVer>>8)&0xff, + (maxVer>>16)&0xff, (maxVer>>8)&0xff) + } + + // "PL_thr_key" contains the TSD key since Perl 5.15.2 + // https://github.com/Perl/perl5/blob/v5.32.0/perlvars.h#L45 + stateInTSD := true + var curcopAddr, cursiAddr libpf.SymbolValue + stateAddr, err := ef.LookupSymbolAddress("PL_thr_key") + if err != nil { + // If Perl is built without threading support, this symbol is not found. + // Fallback to using the global interpreter state. + curcopAddr, err = ef.LookupSymbolAddress("PL_curcop") + if err != nil { + return nil, fmt.Errorf("perl %x: PL_curcop not found: %v", version, err) + } + cursiAddr, err = ef.LookupSymbolAddress("PL_curstackinfo") + if err != nil { + return nil, fmt.Errorf("perl %x: PL_curstackinfo not found: %v", version, err) + } + stateInTSD = false + if curcopAddr < cursiAddr { + stateAddr = curcopAddr + } else { + stateAddr = cursiAddr + } + } + + // Perl_runops_standard is the main loop since Perl 5.6.0 (1999) + // https://github.com/Perl/perl5/blob/v5.32.0/run.c#L37 + // Also Perl_runops_debug exists which is used when the perl debugger is + // active, but this is not supported currently. + interpRanges, err := info.GetSymbolAsRanges("Perl_runops_standard") + if err != nil { + return nil, err + } + + d := &perlData{ + version: version, + stateAddr: stateAddr, + stateInTSD: stateInTSD, + } + + // Perl does not provide introspection data, hard code the struct field + // offsets based on detected version. Some values can be fairly easily + // calculated from the struct definitions, but some are looked up by + // using gdb and getting the field offset directly from debug data. + vms := &d.vmStructs + if stateInTSD { + if version >= perlVersion(5, 34, 0) { + // For Perl 5.34 PerlInterpreter changed and so did its offsets. + vms.interpreter.curcop = 0xd0 + vms.interpreter.curstackinfo = 0xe0 + } else { + vms.interpreter.curcop = 0xe0 + vms.interpreter.curstackinfo = 0xf0 + } + } else { + vms.interpreter.curcop = uint(curcopAddr - stateAddr) + vms.interpreter.curstackinfo = uint(cursiAddr - stateAddr) + } + vms.stackinfo.si_cxstack = 0x08 + vms.stackinfo.si_next = 0x18 + vms.stackinfo.si_cxix = 0x20 + vms.stackinfo.si_type = 0x28 + vms.context.cx_type = 0 + vms.context.blk_oldcop = 0x10 + vms.context.blk_sub_retop = 0x30 + vms.context.blk_sub_cv = 0x40 + vms.context.sizeof = 0x60 + vms.cop.cop_line = 0x24 + vms.cop.cop_file = 0x30 + vms.cop.sizeof = 0x50 + vms.sv.sv_any = 0x0 + vms.sv.sv_flags = 0xc + vms.sv.svu_gp = 0x10 + vms.sv.svu_hash = 0x10 + vms.sv.sizeof = 0x18 + vms.xpvcv.xcv_flags = 0x5c + vms.xpvcv.xcv_gv = 0x38 + vms.xpvgv.xivu_namehek = 0x20 + vms.xpvgv.xgv_stash = 0x28 + vms.xpvhv.xhv_max = 0x18 + vms.xpvhv_aux.xhv_name_u = 0x0 + vms.xpvhv_aux.xhv_name_count = 0x1c + vms.xpvhv_aux.sizeof = 0x38 + vms.xpvhv_aux.pointer_size = 8 + vms.gp.gp_egv = 0x38 + vms.hek.hek_len = 4 + vms.hek.hek_key = 8 + + if version >= perlVersion(5, 32, 0) { + vms.stackinfo.si_type = 0x2c + vms.context.blk_sub_cv = 0x48 + vms.context.sizeof = 0x68 + vms.cop.sizeof = 0x58 + } + + if version >= perlVersion(5, 35, 0) { + vms.xpvhv_aux.xhv_name_count = 0x3c + vms.xpvhv_with_aux.xpvhv_aux = 0x20 + } + + if err = ebpf.UpdateInterpreterOffsets(support.ProgUnwindPerl, + info.FileID(), interpRanges); err != nil { + return nil, err + } + + return d, nil +} diff --git a/interpreter/perl/instance.go b/interpreter/perl/instance.go new file mode 100644 index 00000000..180beaeb --- /dev/null +++ b/interpreter/perl/instance.go @@ -0,0 +1,477 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package perl + +import ( + "errors" + "fmt" + "hash/fnv" + "sync" + "sync/atomic" + "unsafe" + + "github.com/cespare/xxhash/v2" + "github.com/elastic/go-freelru" + log "github.com/sirupsen/logrus" + + "github.com/elastic/otel-profiling-agent/host" + "github.com/elastic/otel-profiling-agent/interpreter" + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/metrics" + npsr "github.com/elastic/otel-profiling-agent/nopanicslicereader" + "github.com/elastic/otel-profiling-agent/remotememory" + "github.com/elastic/otel-profiling-agent/reporter" + "github.com/elastic/otel-profiling-agent/successfailurecounter" + "github.com/elastic/otel-profiling-agent/tpbase" + "github.com/elastic/otel-profiling-agent/util" +) + +// #include "../../support/ebpf/types.h" +import "C" + +type perlInstance struct { + interpreter.InstanceStubs + + // Symbolization metrics + successCount atomic.Uint64 + failCount atomic.Uint64 + + d *perlData + rm remotememory.RemoteMemory + bias C.u64 + + // addrToHEK maps a PERL Hash Element Key (string with hash) to a Go string + addrToHEK *freelru.LRU[libpf.Address, string] + + // addrToCOP maps a PERL Control OP (COP) structure to a perlCOP which caches data from it + addrToCOP *freelru.LRU[copKey, *perlCOP] + + // addrToGV maps a PERL Glob Value (GV) aka "symbol" to its name string + addrToGV *freelru.LRU[libpf.Address, string] + + // memPool provides pointers to byte arrays for efficient memory reuse. + memPool sync.Pool + + // hekLen is the largest number we did see in the last reporting interval for hekLen + // in getHEK. + hekLen atomic.Uint32 + + // procInfoInserted tracks whether we've already inserted process info into BPF maps. + procInfoInserted bool +} + +// perlCOP contains information about Perl Control OPS structure +type perlCOP struct { + fileID libpf.FileID + sourceFileName string + line libpf.AddressOrLineno +} + +// copKey is used as cache key for Perl Control OPS structures. +type copKey struct { + copAddr libpf.Address + funcName string +} + +// hashCopKey returns a 32 bits hash of the input. +// It's main purpose is to hash keys for caching perlCOP values. +func hashCOPKey(k copKey) uint32 { + h := k.copAddr.Hash() + return uint32(h ^ xxhash.Sum64String(k.funcName)) +} + +func (i *perlInstance) UpdateTSDInfo(ebpf interpreter.EbpfHandler, pid util.PID, + tsdInfo tpbase.TSDInfo) error { + d := i.d + stateInTSD := C.u8(0) + if d.stateInTSD { + stateInTSD = 1 + } + vms := &d.vmStructs + data := C.PerlProcInfo{ + version: C.uint(d.version), + stateAddr: C.u64(d.stateAddr) + i.bias, + stateInTSD: stateInTSD, + + tsdInfo: C.TSDInfo{ + offset: C.s16(tsdInfo.Offset), + multiplier: C.u8(tsdInfo.Multiplier), + indirect: C.u8(tsdInfo.Indirect), + }, + + interpreter_curcop: C.u16(vms.interpreter.curcop), + interpreter_curstackinfo: C.u16(vms.interpreter.curstackinfo), + + si_cxstack: C.u8(vms.stackinfo.si_cxstack), + si_next: C.u8(vms.stackinfo.si_next), + si_cxix: C.u8(vms.stackinfo.si_cxix), + si_type: C.u8(vms.stackinfo.si_type), + + context_type: C.u8(vms.context.cx_type), + context_blk_oldcop: C.u8(vms.context.blk_oldcop), + context_blk_sub_retop: C.u8(vms.context.blk_sub_retop), + context_blk_sub_cv: C.u8(vms.context.blk_sub_cv), + context_sizeof: C.u8(vms.context.sizeof), + + sv_flags: C.u8(vms.sv.sv_flags), + sv_any: C.u8(vms.sv.sv_any), + svu_gp: C.u8(vms.sv.svu_gp), + xcv_flags: C.u8(vms.xpvcv.xcv_flags), + xcv_gv: C.u8(vms.xpvcv.xcv_gv), + gp_egv: C.u8(vms.gp.gp_egv), + } + + err := ebpf.UpdateProcData(libpf.Perl, pid, unsafe.Pointer(&data)) + if err != nil { + return err + } + + i.procInfoInserted = true + return nil +} + +func (i *perlInstance) Detach(ebpf interpreter.EbpfHandler, pid util.PID) error { + if !i.procInfoInserted { + return nil + } + return ebpf.DeleteProcData(libpf.Perl, pid) +} + +func (i *perlInstance) GetAndResetMetrics() ([]metrics.Metric, error) { + addrToHEKStats := i.addrToHEK.ResetMetrics() + addrToCOPStats := i.addrToCOP.ResetMetrics() + addrToGVStats := i.addrToGV.ResetMetrics() + + return []metrics.Metric{ + { + ID: metrics.IDPerlSymbolizationSuccess, + Value: metrics.MetricValue(i.successCount.Swap(0)), + }, + { + ID: metrics.IDPerlSymbolizationFailure, + Value: metrics.MetricValue(i.failCount.Swap(0)), + }, + { + ID: metrics.IDPerlAddrToHEKHit, + Value: metrics.MetricValue(addrToHEKStats.Hits), + }, + { + ID: metrics.IDPerlAddrToHEKMiss, + Value: metrics.MetricValue(addrToHEKStats.Misses), + }, + { + ID: metrics.IDPerlAddrToHEKAdd, + Value: metrics.MetricValue(addrToHEKStats.Inserts), + }, + { + ID: metrics.IDPerlAddrToHEKDel, + Value: metrics.MetricValue(addrToHEKStats.Removals), + }, + { + ID: metrics.IDPerlAddrToCOPHit, + Value: metrics.MetricValue(addrToCOPStats.Hits), + }, + { + ID: metrics.IDPerlAddrToCOPMiss, + Value: metrics.MetricValue(addrToCOPStats.Misses), + }, + { + ID: metrics.IDPerlAddrToCOPAdd, + Value: metrics.MetricValue(addrToCOPStats.Inserts), + }, + { + ID: metrics.IDPerlAddrToCOPDel, + Value: metrics.MetricValue(addrToCOPStats.Removals), + }, + { + ID: metrics.IDPerlAddrToGVHit, + Value: metrics.MetricValue(addrToGVStats.Hits), + }, + { + ID: metrics.IDPerlAddrToGVMiss, + Value: metrics.MetricValue(addrToGVStats.Misses), + }, + { + ID: metrics.IDPerlAddrToGVAdd, + Value: metrics.MetricValue(addrToGVStats.Inserts), + }, + { + ID: metrics.IDPerlAddrToGVDel, + Value: metrics.MetricValue(addrToGVStats.Removals), + }, + { + ID: metrics.IDPerlHekLen, + Value: metrics.MetricValue(i.hekLen.Swap(0)), + }, + }, nil +} + +func (i *perlInstance) getHEK(addr libpf.Address) (string, error) { + if addr == 0 { + return "", errors.New("null hek pointer") + } + if value, ok := i.addrToHEK.Get(addr); ok { + return value, nil + } + vms := &i.d.vmStructs + + // Read the Hash Element Key (HEK) length and readahead bytes in + // attempt to avoid second system call to read the target string. + // 128 is chosen arbitrarily as "hopefully good enough"; this value can + // be increased if it turns out to be necessary. + var buf [128]byte + if err := i.rm.Read(addr, buf[:]); err != nil { + return "", err + } + hekLen := npsr.Uint32(buf[:], vms.hek.hek_len) + + // For our better understanding and future improvement we track the maximum value we get for + // hekLen and report it. + util.AtomicUpdateMaxUint32(&i.hekLen, hekLen) + + if hekLen > hekLenLimit { + return "", fmt.Errorf("hek too large (%d)", hekLen) + } + + syncPoolData := i.memPool.Get().(*[]byte) + if syncPoolData == nil { + return "", errors.New("failed to get memory from sync pool") + } + + defer func() { + // Reset memory and return it for reuse. + for j := uint32(0); j < hekLen; j++ { + (*syncPoolData)[j] = 0x0 + } + i.memPool.Put(syncPoolData) + }() + + tmp := (*syncPoolData)[:hekLen] + // Always allocate the string separately so it does not hold the backing + // buffer that might be larger than needed + numCopied := copy(tmp, buf[vms.hek.hek_key:]) + if hekLen > uint32(numCopied) { + err := i.rm.Read(addr+libpf.Address(vms.hek.hek_key+uint(numCopied)), tmp[numCopied:]) + if err != nil { + return "", err + } + } + s := string(tmp) + if !util.IsValidString(s) { + log.Debugf("Extracted invalid hek string at 0x%x '%v'", addr, []byte(s)) + return "", fmt.Errorf("extracted invalid hek string at 0x%x", addr) + } + i.addrToHEK.Add(addr, s) + + return s, nil +} + +func (i *perlInstance) getHVName(hvAddr libpf.Address) (string, error) { + if hvAddr == 0 { + return "", nil + } + vms := &i.d.vmStructs + hv := make([]byte, vms.sv.sizeof) + if err := i.rm.Read(hvAddr, hv); err != nil { + return "", err + } + hvFlags := npsr.Uint32(hv, vms.sv.sv_flags) + if hvFlags&SVt_MASK != SVt_PVHV { + return "", errors.New("not a HV") + } + + xpvhvAddr := npsr.Ptr(hv, vms.sv.sv_any) + max := i.rm.Uint64(xpvhvAddr + libpf.Address(vms.xpvhv.xhv_max)) + + xpvhvAux := make([]byte, vms.xpvhv_aux.sizeof) + if i.d.version < perlVersion(5, 35, 0) { + // The aux structure is at the end of the array. Calculate its address. + arrayAddr := npsr.Ptr(hv, vms.sv.svu_hash) + xpvhvAuxAddr := arrayAddr + libpf.Address((max+1)*8) + if err := i.rm.Read(xpvhvAuxAddr, xpvhvAux); err != nil { + return "", err + } + } else { + // In Perl 5.36.x.XPVHV got replaced with xpvhv_with_aux to hold this information. + // https://github.com/Perl/perl5/commit/94ee6ed79dbca73d0345b745534477e4017fb990 + if err := i.rm.Read(xpvhvAddr+libpf.Address(vms.xpvhv_with_aux.xpvhv_aux), + xpvhvAux); err != nil { + return "", err + } + } + + nameCount := npsr.Int32(xpvhvAux, vms.xpvhv_aux.xhv_name_count) + hekAddr := npsr.Ptr(xpvhvAux, vms.xpvhv_aux.xhv_name_u) + // A non-zero name count here implies that the + // GV belongs to a symbol table that has been + // altered in some way (Perl calls this a Stash, see + // https://www.perlmonks.org/?node=perlguts#Stashes_and_Globs for more). + // + // Stashes can be manipulated directly from Perl code, but it + // can also happen during normal operation and it messes with the layout of HVs. + // The exact link for this behavior is here: + // https://github.com/Perl/perl5/blob/v5.32.0/hv.h#L114 + if nameCount > 0 { + // When xhv_name_count > 0, it points to a HEK** array and the + // first element is the name. + hekAddr = i.rm.Ptr(hekAddr) + } else if nameCount < 0 { + // When xhv_name_count < 0, it points to a HEK** array and the + // second element is the name. + hekAddr = i.rm.Ptr(hekAddr + libpf.Address(vms.xpvhv_aux.pointer_size)) + } + + return i.getHEK(hekAddr) +} + +func (i *perlInstance) getGV(gvAddr libpf.Address, nameOnly bool) (string, error) { + if gvAddr == 0 { + return "", nil + } + if value, ok := i.addrToGV.Get(gvAddr); ok { + return value, nil + } + + vms := &i.d.vmStructs + + // Follow the GV's "body" pointer to get the function name + xpvgvAddr := i.rm.Ptr(gvAddr + libpf.Address(vms.sv.sv_any)) + hekAddr := i.rm.Ptr(xpvgvAddr + libpf.Address(vms.xpvgv.xivu_namehek)) + gvName, err := i.getHEK(hekAddr) + if err != nil { + return "", err + } + + if !nameOnly && gvName != "" { + stashAddr := i.rm.Ptr(xpvgvAddr + libpf.Address(vms.xpvgv.xgv_stash)) + packageName, err := i.getHVName(stashAddr) + if err != nil { + return "", err + } + + // Build the qualified name + if packageName == "" { + // per Perl_gv_fullname4 + packageName = "__ANON__" + } + gvName = packageName + "::" + gvName + } + + i.addrToGV.Add(gvAddr, gvName) + + return gvName, nil +} + +// getCOP reads and caches a Control OP from remote interpreter. On success, the COP +// and a bool if it was cached, is returned. On error, the error. +func (i *perlInstance) getCOP(copAddr libpf.Address, funcName string) (*perlCOP, bool, error) { + key := copKey{ + copAddr: copAddr, + funcName: funcName, + } + if value, ok := i.addrToCOP.Get(key); ok { + return value, true, nil + } + + vms := &i.d.vmStructs + cop := make([]byte, vms.cop.sizeof) + if err := i.rm.Read(copAddr, cop); err != nil { + return nil, false, err + } + + sourceFileName := interpreter.UnknownSourceFile + if i.d.stateInTSD { + // cop_file is a pointer to nul terminated string + sourceFileAddr := npsr.Ptr(cop, vms.cop.cop_file) + sourceFileName = i.rm.String(sourceFileAddr) + } else { + // cop_file is a pointer to GV + sourceFileGVAddr := npsr.Ptr(cop, vms.cop.cop_file) + var err error + sourceFileName, err = i.getGV(sourceFileGVAddr, true) + if err == nil && len(sourceFileName) <= 2 { + err = fmt.Errorf("sourcefile gv length too small (%d)", len(sourceFileName)) + } + if err != nil { + return nil, false, err + } + sourceFileName = sourceFileName[2:] + } + if !util.IsValidString(sourceFileName) { + log.Debugf("Extracted invalid source file name '%v'", []byte(sourceFileName)) + return nil, false, errors.New("extracted invalid source file name") + } + + line := npsr.Uint32(cop, vms.cop.cop_line) + + // Synthesize a FileID. + // The fnv hash Write() method calls cannot fail, so it's safe to ignore the errors. + h := fnv.New128a() + _, _ = h.Write([]byte{uint8(libpf.PerlFrame)}) + _, _ = h.Write([]byte(sourceFileName)) + // Unfortunately there is very little information to extract for each function + // from the GV. Use just the function name at this time. + _, _ = h.Write([]byte(funcName)) + fileID, err := libpf.FileIDFromBytes(h.Sum(nil)) + if err != nil { + return nil, false, fmt.Errorf("failed to create a file ID: %v", err) + } + + c := &perlCOP{ + sourceFileName: sourceFileName, + fileID: fileID, + line: libpf.AddressOrLineno(line), + } + i.addrToCOP.Add(key, c) + return c, false, nil +} + +func (i *perlInstance) Symbolize(symbolReporter reporter.SymbolReporter, + frame *host.Frame, trace *libpf.Trace) error { + if !frame.Type.IsInterpType(libpf.Perl) { + return interpreter.ErrMismatchInterpreterType + } + + sfCounter := successfailurecounter.New(&i.successCount, &i.failCount) + defer sfCounter.DefaultToFailure() + + gvAddr := libpf.Address(frame.File) + functionName, err := i.getGV(gvAddr, false) + if err != nil { + return fmt.Errorf("failed to get Perl GV %x: %v", gvAddr, err) + } + + // This can only happen if gvAddr is 0, + // which we use to denote code at the top level (e.g + // code in the file not inside a function). + if functionName == "" { + functionName = interpreter.TopLevelFunctionName + } + copAddr := libpf.Address(frame.Lineno) + cop, seen, err := i.getCOP(copAddr, functionName) + if err != nil { + return fmt.Errorf("failed to get Perl COP %x: %v", copAddr, err) + } + + lineno := cop.line + + trace.AppendFrame(libpf.PerlFrame, cop.fileID, lineno) + + if !seen { + symbolReporter.FrameMetadata( + cop.fileID, lineno, util.SourceLineno(lineno), 0, + functionName, cop.sourceFileName) + + log.Debugf("[%d] [%x] %v at %v:%v", + len(trace.FrameTypes), + cop.fileID, functionName, + cop.sourceFileName, lineno) + } + + sfCounter.ReportSuccess() + return nil +} diff --git a/interpreter/perl/perl.go b/interpreter/perl/perl.go index 98e26958..c4c19cf9 100644 --- a/interpreter/perl/perl.go +++ b/interpreter/perl/perl.go @@ -50,34 +50,11 @@ package perl import ( "debug/elf" - "errors" - "fmt" - "hash/fnv" "regexp" - "sync" - "sync/atomic" - "unsafe" - "github.com/cespare/xxhash/v2" - - "github.com/elastic/otel-profiling-agent/host" "github.com/elastic/otel-profiling-agent/interpreter" - "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/freelru" - npsr "github.com/elastic/otel-profiling-agent/libpf/nopanicslicereader" - "github.com/elastic/otel-profiling-agent/libpf/remotememory" - "github.com/elastic/otel-profiling-agent/libpf/successfailurecounter" - "github.com/elastic/otel-profiling-agent/metrics" - "github.com/elastic/otel-profiling-agent/reporter" - "github.com/elastic/otel-profiling-agent/support" - "github.com/elastic/otel-profiling-agent/tpbase" - - log "github.com/sirupsen/logrus" ) -// #include "../../support/ebpf/types.h" -import "C" - // nolint:golint,stylecheck,revive const ( // Scalar Value types (SVt) @@ -99,589 +76,9 @@ var ( _ interpreter.Instance = &perlInstance{} ) -type perlData struct { - // vmStructs reflects the Perl internal class names and the offsets of named field - // The struct names are based on the Perl C "struct name", the alternate typedef seen - // mostly in code is in parenthesis. - // nolint:golint,stylecheck,revive - vmStructs struct { - // interpreter struct (PerlInterpreter) is defined in intrpvar.h via macro trickery - // https://github.com/Perl/perl5/blob/v5.32.0/intrpvar.h - interpreter struct { - curcop uint - curstackinfo uint - } - // stackinfo struct (PERL_SI) is defined in cop.h - // https://github.com/Perl/perl5/blob/v5.32.0/cop.h#L1037-L1055 - stackinfo struct { - si_cxstack uint - si_next uint - si_cxix uint - si_type uint - } - // context struct (PERL_CONTEXT) is defined in cop.h - // https://github.com/Perl/perl5/blob/v5.32.0/cop.h#L878-L884 - context struct { - cx_type uint - blk_oldcop uint - blk_sub_retop uint - blk_sub_cv uint - sizeof uint - } - // cop struct (COP), a "control op" is defined in cop.h - // https://github.com/Perl/perl5/blob/v5.32.0/cop.h#L397-L424 - cop struct { - cop_line uint - cop_file uint - sizeof uint - } - // sv struct (SV) is "Scalar Value", the generic "base" for all - // perl variants, and is horrendously cast to other types as needed. - // https://github.com/Perl/perl5/blob/v5.32.0/sv.h#L233-L236 - sv struct { - sv_any uint - sv_flags uint - svu_gp uint - svu_hash uint - sizeof uint - } - // xpvcv struct (XPVCV) is "Code Value object" (the data PV points to) - // https://github.com/Perl/perl5/blob/v5.32.0/cv.h#L13-L16 - xpvcv struct { - xcv_flags uint - xcv_gv uint - } - // xpvgv struct (XPVGV) is "Glob Value object" (the data GV points to) - // https://github.com/Perl/perl5/blob/v5.32.0/sv.h#L571-L575 - xpvgv struct { - xivu_namehek uint - xgv_stash uint - } - // xpvhv struct (XPVHV) is a "Hash Value" (that is the Hash struct) - // https://github.com/Perl/perl5/blob/v5.32.0/hv.h#L135-L140 - xpvhv struct { - xhv_max uint - } - // xpvhv_with_aux is the successor of XPVHV starting in Perl 5.36. - // https://github.com/Perl/perl5/blob/v5.36.0/hv.h#L149-L155 - xpvhv_with_aux struct { - xpvhv_aux uint - } - // xpvhv_aux struct is the Hash ancillary data structure - // https://github.com/Perl/perl5/blob/v5.32.0/hv.h#L108-L128 - xpvhv_aux struct { - xhv_name_u uint - xhv_name_count uint - sizeof uint - pointer_size uint - } - // gp struct (GP) is apparently "Glob Private", essentially a function definition - // https://github.com/Perl/perl5/blob/v5.32.0/gv.h#L11-L24 - gp struct { - gp_egv uint - } - // hek struct (HEK) is "Hash Entry Key", a hash/len/key triplet - // https://github.com/Perl/perl5/blob/v5.32.0/hv.h#L44-L57 - hek struct { - hek_len uint - hek_key uint - } - } - - // stateAddr is the address of the Perl state address (TSD or global) - stateAddr libpf.SymbolValue - - // version contains the Perl version - version uint32 - - // stateInTSD is set if the we have state TSD key address - stateInTSD bool -} - -type perlInstance struct { - interpreter.InstanceStubs - - // Symbolization metrics - successCount atomic.Uint64 - failCount atomic.Uint64 - - d *perlData - rm remotememory.RemoteMemory - bias C.u64 - - // addrToHEK maps a PERL Hash Element Key (string with hash) to a Go string - addrToHEK *freelru.LRU[libpf.Address, string] - - // addrToCOP maps a PERL Control OP (COP) structure to a perlCOP which caches data from it - addrToCOP *freelru.LRU[copKey, *perlCOP] - - // addrToGV maps a PERL Glob Value (GV) aka "symbol" to its name string - addrToGV *freelru.LRU[libpf.Address, string] - - // memPool provides pointers to byte arrays for efficient memory reuse. - memPool sync.Pool - - // hekLen is the largest number we did see in the last reporting interval for hekLen - // in getHEK. - hekLen atomic.Uint32 - - // procInfoInserted tracks whether we've already inserted process info into BPF maps. - procInfoInserted bool -} - -// perlCOP contains information about Perl Control OPS structure -type perlCOP struct { - fileID libpf.FileID - sourceFileName string - line libpf.AddressOrLineno -} - -// copKey is used as cache key for Perl Control OPS structures. -type copKey struct { - copAddr libpf.Address - funcName string -} - -// hashCopKey returns a 32 bits hash of the input. -// It's main purpose is to hash keys for caching perlCOP values. -func hashCOPKey(k copKey) uint32 { - h := k.copAddr.Hash() - return uint32(h ^ xxhash.Sum64String(k.funcName)) -} - -func (i *perlInstance) UpdateTSDInfo(ebpf interpreter.EbpfHandler, pid libpf.PID, - tsdInfo tpbase.TSDInfo) error { - d := i.d - stateInTSD := C.u8(0) - if d.stateInTSD { - stateInTSD = 1 - } - vms := &d.vmStructs - data := C.PerlProcInfo{ - version: C.uint(d.version), - stateAddr: C.u64(d.stateAddr) + i.bias, - stateInTSD: stateInTSD, - - tsdInfo: C.TSDInfo{ - offset: C.s16(tsdInfo.Offset), - multiplier: C.u8(tsdInfo.Multiplier), - indirect: C.u8(tsdInfo.Indirect), - }, - - interpreter_curcop: C.u16(vms.interpreter.curcop), - interpreter_curstackinfo: C.u16(vms.interpreter.curstackinfo), - - si_cxstack: C.u8(vms.stackinfo.si_cxstack), - si_next: C.u8(vms.stackinfo.si_next), - si_cxix: C.u8(vms.stackinfo.si_cxix), - si_type: C.u8(vms.stackinfo.si_type), - - context_type: C.u8(vms.context.cx_type), - context_blk_oldcop: C.u8(vms.context.blk_oldcop), - context_blk_sub_retop: C.u8(vms.context.blk_sub_retop), - context_blk_sub_cv: C.u8(vms.context.blk_sub_cv), - context_sizeof: C.u8(vms.context.sizeof), - - sv_flags: C.u8(vms.sv.sv_flags), - sv_any: C.u8(vms.sv.sv_any), - svu_gp: C.u8(vms.sv.svu_gp), - xcv_flags: C.u8(vms.xpvcv.xcv_flags), - xcv_gv: C.u8(vms.xpvcv.xcv_gv), - gp_egv: C.u8(vms.gp.gp_egv), - } - - err := ebpf.UpdateProcData(libpf.Perl, pid, unsafe.Pointer(&data)) - if err != nil { - return err - } - - i.procInfoInserted = true - return nil -} - -func (i *perlInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error { - if !i.procInfoInserted { - return nil - } - return ebpf.DeleteProcData(libpf.Perl, pid) -} - -func (i *perlInstance) GetAndResetMetrics() ([]metrics.Metric, error) { - addrToHEKStats := i.addrToHEK.GetAndResetStatistics() - addrToCOPStats := i.addrToCOP.GetAndResetStatistics() - addrToGVStats := i.addrToGV.GetAndResetStatistics() - - return []metrics.Metric{ - { - ID: metrics.IDPerlSymbolizationSuccess, - Value: metrics.MetricValue(i.successCount.Swap(0)), - }, - { - ID: metrics.IDPerlSymbolizationFailure, - Value: metrics.MetricValue(i.failCount.Swap(0)), - }, - { - ID: metrics.IDPerlAddrToHEKHit, - Value: metrics.MetricValue(addrToHEKStats.Hit), - }, - { - ID: metrics.IDPerlAddrToHEKMiss, - Value: metrics.MetricValue(addrToHEKStats.Miss), - }, - { - ID: metrics.IDPerlAddrToHEKAdd, - Value: metrics.MetricValue(addrToHEKStats.Added), - }, - { - ID: metrics.IDPerlAddrToHEKDel, - Value: metrics.MetricValue(addrToHEKStats.Deleted), - }, - { - ID: metrics.IDPerlAddrToCOPHit, - Value: metrics.MetricValue(addrToCOPStats.Hit), - }, - { - ID: metrics.IDPerlAddrToCOPMiss, - Value: metrics.MetricValue(addrToCOPStats.Miss), - }, - { - ID: metrics.IDPerlAddrToCOPAdd, - Value: metrics.MetricValue(addrToCOPStats.Added), - }, - { - ID: metrics.IDPerlAddrToCOPDel, - Value: metrics.MetricValue(addrToCOPStats.Deleted), - }, - { - ID: metrics.IDPerlAddrToGVHit, - Value: metrics.MetricValue(addrToGVStats.Hit), - }, - { - ID: metrics.IDPerlAddrToGVMiss, - Value: metrics.MetricValue(addrToGVStats.Miss), - }, - { - ID: metrics.IDPerlAddrToGVAdd, - Value: metrics.MetricValue(addrToGVStats.Added), - }, - { - ID: metrics.IDPerlAddrToGVDel, - Value: metrics.MetricValue(addrToGVStats.Deleted), - }, - { - ID: metrics.IDPerlHekLen, - Value: metrics.MetricValue(i.hekLen.Swap(0)), - }, - }, nil -} - -func (i *perlInstance) getHEK(addr libpf.Address) (string, error) { - if addr == 0 { - return "", errors.New("null hek pointer") - } - if value, ok := i.addrToHEK.Get(addr); ok { - return value, nil - } - vms := &i.d.vmStructs - - // Read the Hash Element Key (HEK) length and readahead bytes in - // attempt to avoid second system call to read the target string. - // 128 is chosen arbitrarily as "hopefully good enough"; this value can - // be increased if it turns out to be necessary. - var buf [128]byte - if err := i.rm.Read(addr, buf[:]); err != nil { - return "", err - } - hekLen := npsr.Uint32(buf[:], vms.hek.hek_len) - - // For our better understanding and future improvement we track the maximum value we get for - // hekLen and report it. - libpf.AtomicUpdateMaxUint32(&i.hekLen, hekLen) - - if hekLen > hekLenLimit { - return "", fmt.Errorf("hek too large (%d)", hekLen) - } - - syncPoolData := i.memPool.Get().(*[]byte) - if syncPoolData == nil { - return "", fmt.Errorf("failed to get memory from sync pool") - } - - defer func() { - // Reset memory and return it for reuse. - for j := uint32(0); j < hekLen; j++ { - (*syncPoolData)[j] = 0x0 - } - i.memPool.Put(syncPoolData) - }() - - tmp := (*syncPoolData)[:hekLen] - // Always allocate the string separately so it does not hold the backing - // buffer that might be larger than needed - numCopied := copy(tmp, buf[vms.hek.hek_key:]) - if hekLen > uint32(numCopied) { - err := i.rm.Read(addr+libpf.Address(vms.hek.hek_key+uint(numCopied)), tmp[numCopied:]) - if err != nil { - return "", err - } - } - s := string(tmp) - if !libpf.IsValidString(s) { - log.Debugf("Extracted invalid hek string at 0x%x '%v'", addr, []byte(s)) - return "", fmt.Errorf("extracted invalid hek string at 0x%x", addr) - } - i.addrToHEK.Add(addr, s) - - return s, nil -} - -func (i *perlInstance) getHVName(hvAddr libpf.Address) (string, error) { - if hvAddr == 0 { - return "", nil - } - vms := &i.d.vmStructs - hv := make([]byte, vms.sv.sizeof) - if err := i.rm.Read(hvAddr, hv); err != nil { - return "", err - } - hvFlags := npsr.Uint32(hv, vms.sv.sv_flags) - if hvFlags&SVt_MASK != SVt_PVHV { - return "", errors.New("not a HV") - } - - xpvhvAddr := npsr.Ptr(hv, vms.sv.sv_any) - max := i.rm.Uint64(xpvhvAddr + libpf.Address(vms.xpvhv.xhv_max)) - - xpvhvAux := make([]byte, vms.xpvhv_aux.sizeof) - if i.d.version < 0x052300 { - // The aux structure is at the end of the array. Calculate its address. - arrayAddr := npsr.Ptr(hv, vms.sv.svu_hash) - xpvhvAuxAddr := arrayAddr + libpf.Address((max+1)*8) - if err := i.rm.Read(xpvhvAuxAddr, xpvhvAux); err != nil { - return "", err - } - } else { - // In Perl 5.36.x.XPVHV got replaced with xpvhv_with_aux to hold this information. - // https://github.com/Perl/perl5/commit/94ee6ed79dbca73d0345b745534477e4017fb990 - if err := i.rm.Read(xpvhvAddr+libpf.Address(vms.xpvhv_with_aux.xpvhv_aux), - xpvhvAux); err != nil { - return "", err - } - } - - nameCount := npsr.Int32(xpvhvAux, vms.xpvhv_aux.xhv_name_count) - hekAddr := npsr.Ptr(xpvhvAux, vms.xpvhv_aux.xhv_name_u) - // A non-zero name count here implies that the - // GV belongs to a symbol table that has been - // altered in some way (Perl calls this a Stash, see - // https://www.perlmonks.org/?node=perlguts#Stashes_and_Globs for more). - // - // Stashes can be manipulated directly from Perl code, but it - // can also happen during normal operation and it messes with the layout of HVs. - // The exact link for this behavior is here: - // https://github.com/Perl/perl5/blob/v5.32.0/hv.h#L114 - if nameCount > 0 { - // When xhv_name_count > 0, it points to a HEK** array and the - // first element is the name. - hekAddr = i.rm.Ptr(hekAddr) - } else if nameCount < 0 { - // When xhv_name_count < 0, it points to a HEK** array and the - // second element is the name. - hekAddr = i.rm.Ptr(hekAddr + libpf.Address(vms.xpvhv_aux.pointer_size)) - } - - return i.getHEK(hekAddr) -} - -func (i *perlInstance) getGV(gvAddr libpf.Address, nameOnly bool) (string, error) { - if gvAddr == 0 { - return "", nil - } - if value, ok := i.addrToGV.Get(gvAddr); ok { - return value, nil - } - - vms := &i.d.vmStructs - - // Follow the GV's "body" pointer to get the function name - xpvgvAddr := i.rm.Ptr(gvAddr + libpf.Address(vms.sv.sv_any)) - hekAddr := i.rm.Ptr(xpvgvAddr + libpf.Address(vms.xpvgv.xivu_namehek)) - gvName, err := i.getHEK(hekAddr) - if err != nil { - return "", err - } - - if !nameOnly && gvName != "" { - stashAddr := i.rm.Ptr(xpvgvAddr + libpf.Address(vms.xpvgv.xgv_stash)) - packageName, err := i.getHVName(stashAddr) - if err != nil { - return "", err - } - - // Build the qualified name - if packageName == "" { - // per Perl_gv_fullname4 - packageName = "__ANON__" - } - gvName = packageName + "::" + gvName - } - - i.addrToGV.Add(gvAddr, gvName) - - return gvName, nil -} - -// getCOP reads and caches a Control OP from remote interpreter. On success, the COP -// and a bool if it was cached, is returned. On error, the error. -func (i *perlInstance) getCOP(copAddr libpf.Address, funcName string) (*perlCOP, bool, error) { - key := copKey{ - copAddr: copAddr, - funcName: funcName, - } - if value, ok := i.addrToCOP.Get(key); ok { - return value, true, nil - } - - vms := &i.d.vmStructs - cop := make([]byte, vms.cop.sizeof) - if err := i.rm.Read(copAddr, cop); err != nil { - return nil, false, err - } - - sourceFileName := interpreter.UnknownSourceFile - if i.d.stateInTSD { - // cop_file is a pointer to nul terminated string - sourceFileAddr := npsr.Ptr(cop, vms.cop.cop_file) - sourceFileName = i.rm.String(sourceFileAddr) - } else { - // cop_file is a pointer to GV - sourceFileGVAddr := npsr.Ptr(cop, vms.cop.cop_file) - var err error - sourceFileName, err = i.getGV(sourceFileGVAddr, true) - if err == nil && len(sourceFileName) <= 2 { - err = fmt.Errorf("sourcefile gv length too small (%d)", len(sourceFileName)) - } - if err != nil { - return nil, false, err - } - sourceFileName = sourceFileName[2:] - } - if !libpf.IsValidString(sourceFileName) { - log.Debugf("Extracted invalid source file name '%v'", []byte(sourceFileName)) - return nil, false, fmt.Errorf("extracted invalid source file name") - } - - line := npsr.Uint32(cop, vms.cop.cop_line) - - // Synthesize a FileID. - // The fnv hash Write() method calls cannot fail, so it's safe to ignore the errors. - h := fnv.New128a() - _, _ = h.Write([]byte{uint8(libpf.PerlFrame)}) - _, _ = h.Write([]byte(sourceFileName)) - // Unfortunately there is very little information to extract for each function - // from the GV. Use just the function name at this time. - _, _ = h.Write([]byte(funcName)) - fileID, err := libpf.FileIDFromBytes(h.Sum(nil)) - if err != nil { - return nil, false, fmt.Errorf("failed to create a file ID: %v", err) - } - - c := &perlCOP{ - sourceFileName: sourceFileName, - fileID: fileID, - line: libpf.AddressOrLineno(line), - } - i.addrToCOP.Add(key, c) - return c, false, nil -} - -func (i *perlInstance) Symbolize(symbolReporter reporter.SymbolReporter, - frame *host.Frame, trace *libpf.Trace) error { - if !frame.Type.IsInterpType(libpf.Perl) { - return interpreter.ErrMismatchInterpreterType - } - - sfCounter := successfailurecounter.New(&i.successCount, &i.failCount) - defer sfCounter.DefaultToFailure() - - gvAddr := libpf.Address(frame.File) - functionName, err := i.getGV(gvAddr, false) - if err != nil { - return fmt.Errorf("failed to get Perl GV %x: %v", gvAddr, err) - } - - // This can only happen if gvAddr is 0, - // which we use to denote code at the top level (e.g - // code in the file not inside a function). - if functionName == "" { - functionName = interpreter.TopLevelFunctionName - } - copAddr := libpf.Address(frame.Lineno) - cop, seen, err := i.getCOP(copAddr, functionName) - if err != nil { - return fmt.Errorf("failed to get Perl COP %x: %v", copAddr, err) - } - - lineno := cop.line - - trace.AppendFrame(libpf.PerlFrame, cop.fileID, lineno) - - if !seen { - symbolReporter.FrameMetadata( - cop.fileID, lineno, libpf.SourceLineno(lineno), 0, - functionName, cop.sourceFileName) - - log.Debugf("[%d] [%x] %v at %v:%v", - len(trace.FrameTypes), - cop.fileID, functionName, - cop.sourceFileName, lineno) - } - - sfCounter.ReportSuccess() - return nil -} - -func (d *perlData) String() string { - ver := d.version - return fmt.Sprintf("Perl %d.%d.%d", (ver>>16)&0xff, (ver>>8)&0xff, ver&0xff) -} - -func (d *perlData) Attach(_ interpreter.EbpfHandler, _ libpf.PID, bias libpf.Address, - rm remotememory.RemoteMemory) (interpreter.Instance, error) { - addrToHEK, err := freelru.New[libpf.Address, string](interpreter.LruFunctionCacheSize, - libpf.Address.Hash32) - if err != nil { - return nil, err - } - - addrToCOP, err := freelru.New[copKey, *perlCOP](interpreter.LruFunctionCacheSize*8, - hashCOPKey) - if err != nil { - return nil, err - } - - addrToGV, err := freelru.New[libpf.Address, string](interpreter.LruFunctionCacheSize, - libpf.Address.Hash32) - if err != nil { - return nil, err - } - - return &perlInstance{ - d: d, - rm: rm, - bias: C.u64(bias), - addrToHEK: addrToHEK, - addrToCOP: addrToCOP, - addrToGV: addrToGV, - memPool: sync.Pool{ - New: func() any { - // To avoid resizing of the returned byte slize we size new - // allocations to hekLenLimit. - buf := make([]byte, hekLenLimit) - return &buf - }, - }, - }, nil +// perlVersion revision, version and subversion to a single uitn32 with the full version +func perlVersion(revision, version, subversion byte) uint32 { + return uint32(revision)*0x10000 + uint32(version)*0x100 + uint32(subversion) } func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { @@ -713,138 +110,5 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr } } - // The version is encoded in these globals since Perl 5.15.0. - // https://github.com/Perl/perl5/blob/v5.32.0/perl.h#L4745-L4754 - var verBytes [3]byte - for i, sym := range []libpf.SymbolName{"PL_revision", "PL_version", "PL_subversion"} { - var addr libpf.SymbolValue - addr, err = ef.LookupSymbolAddress(sym) - if err == nil { - _, err = ef.ReadVirtualMemory(verBytes[i:i+1], int64(addr)) - } - if err != nil { - return nil, fmt.Errorf("perl symbol '%s': %v", sym, err) - } - } - - version := uint32(verBytes[0])*0x10000 + uint32(verBytes[1])*0x100 + uint32(verBytes[2]) - log.Debugf("Perl version %v.%v.%v", verBytes[0], verBytes[1], verBytes[2]) - - // Currently tested and supported 5.28.x - 5.36.x. - // Could possibly support older Perl versions somewhere back to 5.14-5.20, by just - // checking the introspection offset validity. 5.14 had major rework for internals. - // And 5.18 had some HV related changes. - const minVer, maxVer = 0x051c00, 0x052500 - if version < minVer || version >= maxVer { - return nil, fmt.Errorf("unsupported Perl %d.%d.%d (need >= %d.%d and < %d.%d)", - verBytes[0], verBytes[1], verBytes[2], - (minVer>>16)&0xff, (minVer>>8)&0xff, - (maxVer>>16)&0xff, (maxVer>>8)&0xff) - } - - // "PL_thr_key" contains the TSD key since Perl 5.15.2 - // https://github.com/Perl/perl5/blob/v5.32.0/perlvars.h#L45 - stateInTSD := true - var curcopAddr, cursiAddr libpf.SymbolValue - stateAddr, err := ef.LookupSymbolAddress("PL_thr_key") - if err != nil { - // If Perl is built without threading support, this symbol is not found. - // Fallback to using the global interpreter state. - curcopAddr, err = ef.LookupSymbolAddress("PL_curcop") - if err != nil { - return nil, fmt.Errorf("perl %x: PL_curcop not found: %v", version, err) - } - cursiAddr, err = ef.LookupSymbolAddress("PL_curstackinfo") - if err != nil { - return nil, fmt.Errorf("perl %x: PL_curstackinfo not found: %v", version, err) - } - stateInTSD = false - if curcopAddr < cursiAddr { - stateAddr = curcopAddr - } else { - stateAddr = cursiAddr - } - } - - // Perl_runops_standard is the main loop since Perl 5.6.0 (1999) - // https://github.com/Perl/perl5/blob/v5.32.0/run.c#L37 - // Also Perl_runops_debug exists which is used when the perl debugger is - // active, but this is not supported currently. - interpRanges, err := info.GetSymbolAsRanges("Perl_runops_standard") - if err != nil { - return nil, err - } - - d := &perlData{ - version: version, - stateAddr: stateAddr, - stateInTSD: stateInTSD, - } - - // Perl does not provide introspection data, hard code the struct field - // offsets based on detected version. Some values can be fairly easily - // calculated from the struct definitions, but some are looked up by - // using gdb and getting the field offset directly from debug data. - vms := &d.vmStructs - if stateInTSD { - if version >= 0x052200 { - // For Perl 5.34 PerlInterpreter changed and so did its offsets. - vms.interpreter.curcop = 0xd0 - vms.interpreter.curstackinfo = 0xe0 - } else { - vms.interpreter.curcop = 0xe0 - vms.interpreter.curstackinfo = 0xf0 - } - } else { - vms.interpreter.curcop = uint(curcopAddr - stateAddr) - vms.interpreter.curstackinfo = uint(cursiAddr - stateAddr) - } - vms.stackinfo.si_cxstack = 0x08 - vms.stackinfo.si_next = 0x18 - vms.stackinfo.si_cxix = 0x20 - vms.stackinfo.si_type = 0x28 - vms.context.cx_type = 0 - vms.context.blk_oldcop = 0x10 - vms.context.blk_sub_retop = 0x30 - vms.context.blk_sub_cv = 0x40 - vms.context.sizeof = 0x60 - vms.cop.cop_line = 0x24 - vms.cop.cop_file = 0x30 - vms.cop.sizeof = 0x50 - vms.sv.sv_any = 0x0 - vms.sv.sv_flags = 0xc - vms.sv.svu_gp = 0x10 - vms.sv.svu_hash = 0x10 - vms.sv.sizeof = 0x18 - vms.xpvcv.xcv_flags = 0x5c - vms.xpvcv.xcv_gv = 0x38 - vms.xpvgv.xivu_namehek = 0x20 - vms.xpvgv.xgv_stash = 0x28 - vms.xpvhv.xhv_max = 0x18 - vms.xpvhv_aux.xhv_name_u = 0x0 - vms.xpvhv_aux.xhv_name_count = 0x1c - vms.xpvhv_aux.sizeof = 0x38 - vms.xpvhv_aux.pointer_size = 8 - vms.gp.gp_egv = 0x38 - vms.hek.hek_len = 4 - vms.hek.hek_key = 8 - - if version >= 0x052000 { - vms.stackinfo.si_type = 0x2c - vms.context.blk_sub_cv = 0x48 - vms.context.sizeof = 0x68 - vms.cop.sizeof = 0x58 - } - - if version >= 0x052300 { - vms.xpvhv_aux.xhv_name_count = 0x3c - vms.xpvhv_with_aux.xpvhv_aux = 0x20 - } - - if err = ebpf.UpdateInterpreterOffsets(support.ProgUnwindPerl, - info.FileID(), interpRanges); err != nil { - return nil, err - } - - return d, nil + return newData(ebpf, info, ef) } diff --git a/interpreter/php/decode_amd64.go b/interpreter/php/decode_amd64.go index d04179e8..21d824b2 100644 --- a/interpreter/php/decode_amd64.go +++ b/interpreter/php/decode_amd64.go @@ -68,9 +68,9 @@ func retrieveExecuteExJumpLabelAddressWrapper(code []byte, addrBase libpf.Symbol fmt.Errorf("failed to decode execute_ex: %s", phpDecodeErrorToString(err)) } -// RetrieveJITBufferPtrWrapper. This function reads the code blob and returns a pointer +// retrieveJITBufferPtrWrapper. This function reads the code blob and returns a pointer // to the JIT buffer used by PHP (called "dasm_buf" in the PHP source). -func RetrieveJITBufferPtrWrapper(code []byte, addrBase libpf.SymbolValue) ( +func retrieveJITBufferPtrWrapper(code []byte, addrBase libpf.SymbolValue) ( dasmBuf libpf.SymbolValue, dasmSize libpf.SymbolValue, err error) { var bufferAddress, sizeAddress uint err2 := int(C.retrieveJITBufferPtr((*C.uint8_t)(unsafe.Pointer(&code[0])), diff --git a/interpreter/php/decode_arm64.go b/interpreter/php/decode_arm64.go index a3ff897a..e5de8382 100644 --- a/interpreter/php/decode_arm64.go +++ b/interpreter/php/decode_arm64.go @@ -12,7 +12,7 @@ import ( "fmt" "github.com/elastic/otel-profiling-agent/libpf" - ah "github.com/elastic/otel-profiling-agent/libpf/armhelpers" + ah "github.com/elastic/otel-profiling-agent/armhelpers" aa "golang.org/x/arch/arm64/arm64asm" ) @@ -98,9 +98,9 @@ func retrieveExecuteExJumpLabelAddressWrapper( return libpf.SymbolValueInvalid, fmt.Errorf("did not find a BR in the given code blob") } -// RetrieveJITBufferPtrWrapper reads the code blob and returns a pointer to the JIT buffer used by +// retrieveJITBufferPtrWrapper reads the code blob and returns a pointer to the JIT buffer used by // PHP (called "dasm_buf" in the PHP source). -func RetrieveJITBufferPtrWrapper(code []byte, addrBase libpf.SymbolValue) ( +func retrieveJITBufferPtrWrapper(code []byte, addrBase libpf.SymbolValue) ( dasmBuf libpf.SymbolValue, dasmSize libpf.SymbolValue, err error) { // The code for recovering the JIT buffer is a little bit more involved on ARM than on x86. // diff --git a/interpreter/php/instance.go b/interpreter/php/instance.go new file mode 100644 index 00000000..2fdf6281 --- /dev/null +++ b/interpreter/php/instance.go @@ -0,0 +1,242 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package php + +import ( + "errors" + "fmt" + "hash/fnv" + "sync/atomic" + + log "github.com/sirupsen/logrus" + + "github.com/elastic/go-freelru" + + "github.com/elastic/otel-profiling-agent/host" + "github.com/elastic/otel-profiling-agent/interpreter" + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/metrics" + npsr "github.com/elastic/otel-profiling-agent/nopanicslicereader" + "github.com/elastic/otel-profiling-agent/remotememory" + "github.com/elastic/otel-profiling-agent/reporter" + "github.com/elastic/otel-profiling-agent/successfailurecounter" + "github.com/elastic/otel-profiling-agent/util" +) + +// nolint:golint,stylecheck,revive +const ( + // zend_function.type definitions from PHP sources + ZEND_USER_FUNCTION = (1 << 1) + ZEND_EVAL_CODE = (1 << 2) + + // This is used to check if the symbolized frame belongs to + // top-level code. + // From https://github.com/php/php-src/blob/PHP-8.0/Zend/zend_compile.h#L542 + ZEND_CALL_TOP_CODE = (1<<17 | 1<<16) +) + +// phpFunction contains the information we cache for a corresponding +// PHP interpreter's zend_function structure. +type phpFunction struct { + // name is the extracted name + name string + + // sourceFileName is the extracted filename field + sourceFileName string + + // fileID is the synthesized methodID + fileID libpf.FileID + + // lineStart is the first source code line for this function + lineStart uint32 + + // lineSeen is a set of line numbers we have already seen and symbolized + lineSeen libpf.Set[libpf.AddressOrLineno] +} + +type phpInstance struct { + interpreter.InstanceStubs + + // PHP symbolization metrics + successCount atomic.Uint64 + failCount atomic.Uint64 + // Failure count for finding the return address in execute_ex + vmRTCount atomic.Uint64 + + d *phpData + rm remotememory.RemoteMemory + + // addrToFunction maps a PHP Function object to a phpFunction which caches + // the needed data from it. + addrToFunction *freelru.LRU[libpf.Address, *phpFunction] +} + +func (i *phpInstance) Detach(ebpf interpreter.EbpfHandler, pid util.PID) error { + return ebpf.DeleteProcData(libpf.PHP, pid) +} + +func (i *phpInstance) GetAndResetMetrics() ([]metrics.Metric, error) { + addrToFuncStats := i.addrToFunction.ResetMetrics() + + return []metrics.Metric{ + { + ID: metrics.IDPHPSymbolizationSuccess, + Value: metrics.MetricValue(i.successCount.Swap(0)), + }, + { + ID: metrics.IDPHPSymbolizationFailure, + Value: metrics.MetricValue(i.failCount.Swap(0)), + }, + { + ID: metrics.IDPHPAddrToFuncHit, + Value: metrics.MetricValue(addrToFuncStats.Hits), + }, + { + ID: metrics.IDPHPAddrToFuncMiss, + Value: metrics.MetricValue(addrToFuncStats.Misses), + }, + { + ID: metrics.IDPHPAddrToFuncAdd, + Value: metrics.MetricValue(addrToFuncStats.Inserts), + }, + { + ID: metrics.IDPHPAddrToFuncDel, + Value: metrics.MetricValue(addrToFuncStats.Removals), + }, + { + ID: metrics.IDPHPFailedToFindReturnAddress, + Value: metrics.MetricValue(i.vmRTCount.Swap(0)), + }, + }, nil +} + +func (i *phpInstance) getFunction(addr libpf.Address, typeInfo uint32) (*phpFunction, error) { + if addr == 0 { + return nil, errors.New("failed to read code object: null pointer") + } + if value, ok := i.addrToFunction.Get(addr); ok { + return value, nil + } + + vms := &i.d.vmStructs + fobj := make([]byte, vms.zend_function.Sizeof) + if err := i.rm.Read(addr, fobj); err != nil { + return nil, fmt.Errorf("failed to read function object: %v", err) + } + + // Parse the zend_function structure + ftype := npsr.Uint8(fobj, vms.zend_function.common_type) + fname := i.rm.String(npsr.Ptr(fobj, vms.zend_function.common_funcname) + vms.zend_string.val) + + if fname != "" && !util.IsValidString(fname) { + log.Debugf("Extracted invalid PHP function name at 0x%x '%v'", addr, []byte(fname)) + fname = "" + } + + if fname == "" { + // If we're at the top-most scope then we can display that information. + if typeInfo&ZEND_CALL_TOP_CODE > 0 { + fname = interpreter.TopLevelFunctionName + } else { + fname = unknownFunctionName + } + } + + sourceFileName := "" + lineStart := uint32(0) + var lineBytes []byte + switch ftype { + case ZEND_USER_FUNCTION, ZEND_EVAL_CODE: + sourceAddr := npsr.Ptr(fobj, vms.zend_function.op_array_filename) + sourceFileName = i.rm.String(sourceAddr + vms.zend_string.val) + if !util.IsValidString(sourceFileName) { + log.Debugf("Extracted invalid PHP source file name at 0x%x '%v'", + addr, []byte(sourceFileName)) + sourceFileName = "" + } + + if ftype == ZEND_EVAL_CODE { + fname = evalCodeFunctionName + // To avoid duplication we get rid of the filename + // It'll look something like "eval'd code", so no + // information is lost here. + sourceFileName = "" + } + + lineStart = npsr.Uint32(fobj, vms.zend_function.op_array_linestart) + // nolint:lll + lineBytes = fobj[vms.zend_function.op_array_linestart : vms.zend_function.op_array_linestart+8] + } + + // The fnv hash Write() method calls cannot fail, so it's safe to ignore the errors. + h := fnv.New128a() + _, _ = h.Write([]byte(sourceFileName)) + _, _ = h.Write([]byte(fname)) + _, _ = h.Write(lineBytes) + fileID, err := libpf.FileIDFromBytes(h.Sum(nil)) + if err != nil { + return nil, fmt.Errorf("failed to create a file ID: %v", err) + } + + pf := &phpFunction{ + name: fname, + sourceFileName: sourceFileName, + fileID: fileID, + lineStart: lineStart, + lineSeen: make(libpf.Set[libpf.AddressOrLineno]), + } + i.addrToFunction.Add(addr, pf) + return pf, nil +} + +func (i *phpInstance) Symbolize(symbolReporter reporter.SymbolReporter, + frame *host.Frame, trace *libpf.Trace) error { + // With Symbolize() in opcacheInstance there is a dedicated function to symbolize JITTed + // PHP frames. But as we also attach phpInstance to PHP processes with JITTed frames, we + // use this function to symbolize all PHP frames, as the process to do so is the same. + if !frame.Type.IsInterpType(libpf.PHP) && + !frame.Type.IsInterpType(libpf.PHPJIT) { + return interpreter.ErrMismatchInterpreterType + } + + sfCounter := successfailurecounter.New(&i.successCount, &i.failCount) + defer sfCounter.DefaultToFailure() + + funcPtr := libpf.Address(frame.File) + // We pack type info and the line number into linenos + typeInfo := uint32(frame.Lineno >> 32) + line := frame.Lineno & 0xffffffff + + f, err := i.getFunction(funcPtr, typeInfo) + if err != nil { + return fmt.Errorf("failed to get php function %x: %v", funcPtr, err) + } + + trace.AppendFrame(libpf.PHPFrame, f.fileID, line) + + if _, ok := f.lineSeen[line]; ok { + return nil + } + + funcOff := uint32(0) + if f.lineStart != 0 && libpf.AddressOrLineno(f.lineStart) <= line { + funcOff = uint32(line) - f.lineStart + } + symbolReporter.FrameMetadata( + f.fileID, line, util.SourceLineno(line), funcOff, + f.name, f.sourceFileName) + + f.lineSeen[line] = libpf.Void{} + + log.Debugf("[%d] [%x] %v+%v at %v:%v", + len(trace.FrameTypes), + f.fileID, f.name, funcOff, + f.sourceFileName, line) + + sfCounter.ReportSuccess() + return nil +} diff --git a/interpreter/php/phpjit/opcache.go b/interpreter/php/opcache.go similarity index 93% rename from interpreter/php/phpjit/opcache.go rename to interpreter/php/opcache.go index 2002bf87..e147409f 100644 --- a/interpreter/php/phpjit/opcache.go +++ b/interpreter/php/opcache.go @@ -4,7 +4,7 @@ * See the file "LICENSE" for details. */ -package phpjit +package php // nolint:lll // PHP8+ JIT compiler unwinder. @@ -119,27 +119,23 @@ package phpjit import ( "encoding/binary" + "errors" "fmt" "regexp" - "unsafe" + + log "github.com/sirupsen/logrus" "github.com/elastic/otel-profiling-agent/interpreter" - "github.com/elastic/otel-profiling-agent/interpreter/php" "github.com/elastic/otel-profiling-agent/libpf" "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/elastic/otel-profiling-agent/libpf/process" - "github.com/elastic/otel-profiling-agent/libpf/remotememory" "github.com/elastic/otel-profiling-agent/lpm" + "github.com/elastic/otel-profiling-agent/process" + "github.com/elastic/otel-profiling-agent/remotememory" "github.com/elastic/otel-profiling-agent/reporter" "github.com/elastic/otel-profiling-agent/support" - - log "github.com/sirupsen/logrus" - "go.uber.org/multierr" + "github.com/elastic/otel-profiling-agent/util" ) -// #include "../../../support/ebpf/types.h" -import "C" - var ( // Regex from the opcache. opcacheRegex = regexp.MustCompile(`^(?:.*/)?opcache\.so$`) @@ -178,14 +174,14 @@ type opcacheInstance struct { prefixes []lpm.Prefix } -func (i *opcacheInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error { +func (i *opcacheInstance) Detach(ebpf interpreter.EbpfHandler, pid util.PID) error { // Here we just remove the entries relating to the mappings for the // JIT's memory var err error for _, prefix := range i.prefixes { if err2 := ebpf.DeletePidInterpreterMapping(pid, prefix); err2 != nil { - err = multierr.Append(err, fmt.Errorf("failed to remove page 0x%x/%d: %w", + err = errors.Join(err, fmt.Errorf("failed to remove page 0x%x/%d: %w", prefix.Key, prefix.Length, err2)) } } @@ -228,16 +224,7 @@ func (i *opcacheInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler, return err } - data := C.PHPJITProcInfo{ - start: C.u64(dasmBuf), - end: C.u64(dasmBuf + dasmSize), - } - pid := pr.PID() - if err = ebpf.UpdateProcData(libpf.PHPJIT, pid, unsafe.Pointer(&data)); err != nil { - return err - } - i.prefixes = prefixes for _, prefix := range prefixes { err = ebpf.UpdatePidInterpreterMapping(pid, prefix, support.ProgUnwindPHP, 0, 0) @@ -253,7 +240,7 @@ func (d *opcacheData) String() string { return fmt.Sprintf("Opcache %d.%d.%d", (ver>>16)&0xff, (ver>>8)&0xff, ver&0xff) } -func (d *opcacheData) Attach(_ interpreter.EbpfHandler, _ libpf.PID, bias libpf.Address, +func (d *opcacheData) Attach(_ interpreter.EbpfHandler, _ util.PID, bias libpf.Address, rm remotememory.RemoteMemory) (interpreter.Instance, error) { return &opcacheInstance{ d: d, @@ -303,13 +290,13 @@ func determineOPCacheVersion(ef *pfelf.File) (uint, error) { // The version string is the second pointer of this structure rm := ef.GetRemoteMemory() versionString := rm.StringPtr(libpf.Address(moduleExtension + 8)) - if versionString == "" || !libpf.IsValidString(versionString) { + if versionString == "" || !util.IsValidString(versionString) { return 0, fmt.Errorf("extension entry PHP version invalid at 0x%x", moduleExtension) } // We should now have a string that contains the exact right version. - return php.VersionExtract(versionString) + return versionExtract(versionString) } // getOpcacheJITInfo retrieves the starting address and the size of the JIT buffer. @@ -335,7 +322,7 @@ func getOpcacheJITInfo(ef *pfelf.File) (dasmBuf, dasmSize libpf.Address, err err return 0, 0, err } - dasmBufPtr, dasmSizePtr, err := php.RetrieveJITBufferPtrWrapper(code, zendJit) + dasmBufPtr, dasmSizePtr, err := retrieveJITBufferPtrWrapper(code, zendJit) if err != nil { return 0, 0, fmt.Errorf("failed to extract DASM pointers: %w", err) } @@ -348,7 +335,8 @@ func getOpcacheJITInfo(ef *pfelf.File) (dasmBuf, dasmSize libpf.Address, err err return libpf.Address(dasmBufPtr), libpf.Address(dasmSizePtr), nil } -func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { +func OpcacheLoader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) ( + interpreter.Data, error) { if !opcacheRegex.MatchString(info.FileName()) { return nil, nil } @@ -365,7 +353,7 @@ func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interprete } // Expect PHP 8+ for proper JIT support - if version < 0x080000 { + if version < phpVersion(8, 0, 0) { return nil, nil } diff --git a/interpreter/php/php.go b/interpreter/php/php.go index 69f27050..70b1818c 100644 --- a/interpreter/php/php.go +++ b/interpreter/php/php.go @@ -8,48 +8,32 @@ package php import ( "bytes" + "errors" "fmt" - "hash/fnv" "regexp" "strconv" - "sync/atomic" "unsafe" - "github.com/elastic/otel-profiling-agent/host" + log "github.com/sirupsen/logrus" + + "github.com/elastic/go-freelru" + "github.com/elastic/otel-profiling-agent/interpreter" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/freelru" - npsr "github.com/elastic/otel-profiling-agent/libpf/nopanicslicereader" "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/elastic/otel-profiling-agent/libpf/remotememory" - "github.com/elastic/otel-profiling-agent/libpf/successfailurecounter" - "github.com/elastic/otel-profiling-agent/metrics" - "github.com/elastic/otel-profiling-agent/reporter" + "github.com/elastic/otel-profiling-agent/remotememory" "github.com/elastic/otel-profiling-agent/support" - - log "github.com/sirupsen/logrus" + "github.com/elastic/otel-profiling-agent/util" ) // #include "../../support/ebpf/types.h" import "C" -// zend_function.type definitions from PHP sources -// nolint:golint,stylecheck,revive -const ( - ZEND_USER_FUNCTION = (1 << 1) - ZEND_EVAL_CODE = (1 << 2) -) - // nolint:golint,stylecheck,revive const ( // This is used to check if the VM mode is the default one // From https://github.com/php/php-src/blob/PHP-8.0/Zend/zend_vm_opcodes.h#L29 ZEND_VM_KIND_HYBRID = (1 << 2) - - // This is used to check if the symbolized frame belongs to - // top-level code. - // From https://github.com/php/php-src/blob/PHP-8.0/Zend/zend_compile.h#L542 - ZEND_CALL_TOP_CODE = (1<<17 | 1<<16) ) const ( @@ -72,11 +56,15 @@ var ( versionMatch = regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)`) // compiler check to make sure the needed interfaces are satisfied - _ interpreter.Data = &php7Data{} - _ interpreter.Instance = &php7Instance{} + _ interpreter.Data = &phpData{} + _ interpreter.Instance = &phpInstance{} ) -type php7Data struct { +func phpVersion(major, minor, release uint) uint { + return major*0x10000 + minor*0x100 + release +} + +type phpData struct { version uint // egAddr is the `executor_globals` symbol value which is needed by the eBPF @@ -119,214 +107,12 @@ type php7Data struct { } } -type php7Instance struct { - interpreter.InstanceStubs - - // PHP symbolization metrics - successCount atomic.Uint64 - failCount atomic.Uint64 - // Failure count for finding the return address in execute_ex - vmRTCount atomic.Uint64 - - d *php7Data - rm remotememory.RemoteMemory - - // addrToFunction maps a PHP Function object to a phpFunction which caches - // the needed data from it. - addrToFunction *freelru.LRU[libpf.Address, *phpFunction] -} - -// phpFunction contains the information we cache for a corresponding -// PHP interpreter's zend_function structure. -type phpFunction struct { - // name is the extracted name - name string - - // sourceFileName is the extracted filename field - sourceFileName string - - // fileID is the synthesized methodID - fileID libpf.FileID - - // lineStart is the first source code line for this function - lineStart uint32 - - // lineSeen is a set of line numbers we have already seen and symbolized - lineSeen libpf.Set[libpf.AddressOrLineno] -} - -func (i *php7Instance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error { - return ebpf.DeleteProcData(libpf.PHP, pid) -} - -func (i *php7Instance) GetAndResetMetrics() ([]metrics.Metric, error) { - addrToFuncStats := i.addrToFunction.GetAndResetStatistics() - - return []metrics.Metric{ - { - ID: metrics.IDPHPSymbolizationSuccess, - Value: metrics.MetricValue(i.successCount.Swap(0)), - }, - { - ID: metrics.IDPHPSymbolizationFailure, - Value: metrics.MetricValue(i.failCount.Swap(0)), - }, - { - ID: metrics.IDPHPAddrToFuncHit, - Value: metrics.MetricValue(addrToFuncStats.Hit), - }, - { - ID: metrics.IDPHPAddrToFuncMiss, - Value: metrics.MetricValue(addrToFuncStats.Miss), - }, - { - ID: metrics.IDPHPAddrToFuncAdd, - Value: metrics.MetricValue(addrToFuncStats.Added), - }, - { - ID: metrics.IDPHPAddrToFuncDel, - Value: metrics.MetricValue(addrToFuncStats.Deleted), - }, - { - ID: metrics.IDPHPFailedToFindReturnAddress, - Value: metrics.MetricValue(i.vmRTCount.Swap(0)), - }, - }, nil -} - -func (i *php7Instance) getFunction(addr libpf.Address, typeInfo uint32) (*phpFunction, error) { - if addr == 0 { - return nil, fmt.Errorf("failed to read code object: null pointer") - } - if value, ok := i.addrToFunction.Get(addr); ok { - return value, nil - } - - vms := &i.d.vmStructs - fobj := make([]byte, vms.zend_function.Sizeof) - if err := i.rm.Read(addr, fobj); err != nil { - return nil, fmt.Errorf("failed to read function object: %v", err) - } - - // Parse the zend_function structure - ftype := npsr.Uint8(fobj, vms.zend_function.common_type) - fname := i.rm.String(npsr.Ptr(fobj, vms.zend_function.common_funcname) + vms.zend_string.val) - - if fname != "" && !libpf.IsValidString(fname) { - log.Debugf("Extracted invalid PHP function name at 0x%x '%v'", addr, []byte(fname)) - fname = "" - } - - if fname == "" { - // If we're at the top-most scope then we can display that information. - if typeInfo&ZEND_CALL_TOP_CODE > 0 { - fname = interpreter.TopLevelFunctionName - } else { - fname = unknownFunctionName - } - } - - sourceFileName := "" - lineStart := uint32(0) - var lineBytes []byte - switch ftype { - case ZEND_USER_FUNCTION, ZEND_EVAL_CODE: - sourceAddr := npsr.Ptr(fobj, vms.zend_function.op_array_filename) - sourceFileName = i.rm.String(sourceAddr + vms.zend_string.val) - if !libpf.IsValidString(sourceFileName) { - log.Debugf("Extracted invalid PHP source file name at 0x%x '%v'", - addr, []byte(sourceFileName)) - sourceFileName = "" - } - - if ftype == ZEND_EVAL_CODE { - fname = evalCodeFunctionName - // To avoid duplication we get rid of the filename - // It'll look something like "eval'd code", so no - // information is lost here. - sourceFileName = "" - } - - lineStart = npsr.Uint32(fobj, vms.zend_function.op_array_linestart) - // nolint:lll - lineBytes = fobj[vms.zend_function.op_array_linestart : vms.zend_function.op_array_linestart+8] - } - - // The fnv hash Write() method calls cannot fail, so it's safe to ignore the errors. - h := fnv.New128a() - _, _ = h.Write([]byte(sourceFileName)) - _, _ = h.Write([]byte(fname)) - _, _ = h.Write(lineBytes) - fileID, err := libpf.FileIDFromBytes(h.Sum(nil)) - if err != nil { - return nil, fmt.Errorf("failed to create a file ID: %v", err) - } - - pf := &phpFunction{ - name: fname, - sourceFileName: sourceFileName, - fileID: fileID, - lineStart: lineStart, - lineSeen: make(libpf.Set[libpf.AddressOrLineno]), - } - i.addrToFunction.Add(addr, pf) - return pf, nil -} - -func (i *php7Instance) Symbolize(symbolReporter reporter.SymbolReporter, - frame *host.Frame, trace *libpf.Trace) error { - // With Symbolize() in opcacheInstance there is a dedicated function to symbolize JITTed - // PHP frames. But as we also attach php7Instance to PHP processes with JITTed frames, we - // use this function to symbolize all PHP frames, as the process to do so is the same. - if !frame.Type.IsInterpType(libpf.PHP) && - !frame.Type.IsInterpType(libpf.PHPJIT) { - return interpreter.ErrMismatchInterpreterType - } - - sfCounter := successfailurecounter.New(&i.successCount, &i.failCount) - defer sfCounter.DefaultToFailure() - - funcPtr := libpf.Address(frame.File) - // We pack type info and the line number into linenos - typeInfo := uint32(frame.Lineno >> 32) - line := frame.Lineno & 0xffffffff - - f, err := i.getFunction(funcPtr, typeInfo) - if err != nil { - return fmt.Errorf("failed to get php function %x: %v", funcPtr, err) - } - - trace.AppendFrame(libpf.PHPFrame, f.fileID, line) - - if _, ok := f.lineSeen[line]; ok { - return nil - } - - funcOff := uint32(0) - if f.lineStart != 0 && libpf.AddressOrLineno(f.lineStart) <= line { - funcOff = uint32(line) - f.lineStart - } - symbolReporter.FrameMetadata( - f.fileID, line, libpf.SourceLineno(line), funcOff, - f.name, f.sourceFileName) - - f.lineSeen[line] = libpf.Void{} - - log.Debugf("[%d] [%x] %v+%v at %v:%v", - len(trace.FrameTypes), - f.fileID, f.name, funcOff, - f.sourceFileName, line) - - sfCounter.ReportSuccess() - return nil -} - -func (d *php7Data) String() string { +func (d *phpData) String() string { ver := d.version return fmt.Sprintf("PHP %d.%d.%d", (ver>>16)&0xff, (ver>>8)&0xff, ver&0xff) } -func (d *php7Data) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libpf.Address, +func (d *phpData) Attach(ebpf interpreter.EbpfHandler, pid util.PID, bias libpf.Address, rm remotememory.RemoteMemory) (interpreter.Instance, error) { addrToFunction, err := freelru.New[libpf.Address, *phpFunction](interpreter.LruFunctionCacheSize, @@ -351,7 +137,7 @@ func (d *php7Data) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libp return nil, err } - instance := &php7Instance{ + instance := &phpInstance{ d: d, rm: rm, addrToFunction: addrToFunction, @@ -360,30 +146,30 @@ func (d *php7Data) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libp // If we failed to find the return address we need to increment // the value here. This happens once per interpreter instance, // but tracking it will help debugging later. - if d.rtAddr == 0 && d.version >= 0x080000 { + if d.rtAddr == 0 && d.version >= phpVersion(8, 0, 0) { instance.vmRTCount.Store(1) } return instance, nil } -func VersionExtract(rodata string) (uint, error) { +func versionExtract(rodata string) (uint, error) { matches := versionMatch.FindStringSubmatch(rodata) if matches == nil { - return 0, fmt.Errorf("no valid PHP version string found") + return 0, errors.New("no valid PHP version string found") } major, _ := strconv.Atoi(matches[1]) minor, _ := strconv.Atoi(matches[2]) release, _ := strconv.Atoi(matches[3]) - return uint(major*0x10000 + minor*0x100 + release), nil + return phpVersion(uint(major), uint(minor), uint(release)), nil } func determinePHPVersion(ef *pfelf.File) (uint, error) { // There is no ideal way to get the PHP version. This just searches // for a known string with the version number from .rodata. if ef.ROData == nil { - return 0, fmt.Errorf("no RO data") + return 0, errors.New("no RO data") } needle := []byte("X-Powered-By: PHP/") @@ -402,14 +188,14 @@ func determinePHPVersion(ef *pfelf.File) (uint, error) { if zeroIdx < 0 { continue } - version, err := VersionExtract(string(rodata[idx : idx+zeroIdx])) + version, err := versionExtract(string(rodata[idx : idx+zeroIdx])) if err != nil { continue } return version, nil } - return 0, fmt.Errorf("no segment contained X-Powered-By") + return 0, errors.New("no segment contained X-Powered-By") } func recoverExecuteExJumpLabelAddress(ef *pfelf.File) (libpf.SymbolValue, error) { @@ -488,9 +274,9 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr return nil, err } - // Only tested on PHP7.3-PHP8.1. Other similar versions probably only require + // Only tested on PHP7.3-PHP8.3. Other similar versions probably only require // tweaking the offsets. - const minVer, maxVer = 0x070300, 0x080300 + var minVer, maxVer = phpVersion(7, 3, 0), phpVersion(8, 4, 0) if version < minVer || version >= maxVer { return nil, fmt.Errorf("PHP version %d.%d.%d (need >= %d.%d and < %d.%d)", (version>>16)&0xff, (version>>8)&0xff, version&0xff, @@ -517,7 +303,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr // Note that if there is an error in the block below then unwinding will produce // incomplete stack unwindings if the JIT compiler is used. rtAddr := libpf.SymbolValueInvalid - if version >= 0x080000 { + if version >= phpVersion(8, 0, 0) { var vmKind uint vmKind, err = determineVMKind(ef) if err != nil { @@ -532,7 +318,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr } } } - pid := &php7Data{ + pid := &phpData{ version: version, egAddr: libpf.Address(egAddr), rtAddr: libpf.Address(rtAddr), @@ -559,13 +345,17 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr vms.zend_function.Sizeof = 168 vms.zend_string.val = 24 vms.zend_op.lineno = 24 - if version >= 0x080200 { + switch { + case version >= phpVersion(8, 3, 0): + vms.zend_function.op_array_filename = 144 + vms.zend_function.op_array_linestart = 152 + case version >= phpVersion(8, 2, 0): vms.zend_function.op_array_filename = 152 vms.zend_function.op_array_linestart = 160 - } else if version >= 0x080000 { + case version >= phpVersion(8, 0, 0): vms.zend_function.op_array_filename = 144 vms.zend_function.op_array_linestart = 152 - } else if version >= 0x070400 { + case version >= phpVersion(7, 4, 0): vms.zend_function.op_array_filename = 136 vms.zend_function.op_array_linestart = 144 } diff --git a/interpreter/php/php_test.go b/interpreter/php/php_test.go index ac09596b..b36e2b7b 100644 --- a/interpreter/php/php_test.go +++ b/interpreter/php/php_test.go @@ -6,40 +6,38 @@ package php -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestPHPRegexs(t *testing.T) { shouldMatch := []string{"php", "./php", "/foo/bar/php", "./foo/bar/php", "php-fpm", "php-cgi7"} for _, s := range shouldMatch { - if !phpRegex.MatchString(s) { - t.Fatalf("PHP regex %s should match %s", phpRegex.String(), s) - } + assert.True(t, phpRegex.MatchString(s), "PHP regex %s should match %s", + phpRegex.String(), s) } shouldNotMatch := []string{"foophp", "ph p", "ph/p", "php-bar"} for _, s := range shouldNotMatch { - if phpRegex.MatchString(s) { - t.Fatalf("regex %s should not match %s", phpRegex.String(), s) - } + assert.False(t, phpRegex.MatchString(s), "PHP regex %s should not match %s", + phpRegex.String(), s) } } -func version(major, minor, release uint) uint { - return major*0x10000 + minor*0x100 + release -} - func TestVersionExtract(t *testing.T) { tests := map[string]struct { given string expected uint expectError bool }{ - "7.x": {given: "7.4.19", expected: version(7, 4, 19), expectError: false}, - "8.x": {given: "8.2.7", expected: version(8, 2, 7), expectError: false}, - "double-digit": {given: "8.0.27", expected: version(8, 0, 27), expectError: false}, + "7.x": {given: "7.4.19", expected: phpVersion(7, 4, 19), expectError: false}, + "8.x": {given: "8.2.7", expected: phpVersion(8, 2, 7), expectError: false}, + "double-digit": {given: "8.0.27", expected: phpVersion(8, 0, 27), expectError: false}, "suffix": { given: "8.1.2-1ubuntu2.14", - expected: version(8, 1, 2), + expected: phpVersion(8, 1, 2), expectError: false, }, "no-release": {given: "7.4", expected: 0, expectError: true}, @@ -51,15 +49,12 @@ func TestVersionExtract(t *testing.T) { name := name test := test t.Run(name, func(t *testing.T) { - v, err := VersionExtract(test.given) - if v != test.expected { - t.Fatalf("Expected %v, got %v", test.expected, v) - } - if test.expectError && err == nil { - t.Fatalf("Expected error, received no error") - } - if test.expectError == false && err != nil { - t.Fatalf("Expected no error, received error: %v", err) + v, err := versionExtract(test.given) + assert.Equal(t, test.expected, v) + if test.expectError { + assert.Error(t, err, "Expected error, received no error") + } else { + assert.NoError(t, err, "Expected no error, received error: %v", err) } }) } diff --git a/interpreter/python/decode.go b/interpreter/python/decode.go new file mode 100644 index 00000000..812022dc --- /dev/null +++ b/interpreter/python/decode.go @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package python + +import ( + ah "github.com/elastic/otel-profiling-agent/armhelpers" + aa "golang.org/x/arch/arm64/arm64asm" + + "github.com/elastic/otel-profiling-agent/libpf" +) + +// decodeStubArgumentWrapperARM64 disassembles arm64 code and decodes the assumed value +// of requested argument. +func decodeStubArgumentWrapperARM64(code []byte, argNumber uint8, _, + addrBase libpf.SymbolValue) libpf.SymbolValue { + // The concept is to track the latest load offset for all X0..X30 registers. + // These registers are used as the function arguments. Once the first branch + // instruction (function call/tail jump) is found, the state of the requested + // argument register's offset is inspected and returned if found. + // It is seen often that the load with offset happens to intermediate register + // first, and is later moved to the argument register. Because of this, the + // tracking requires extra effort between register moves etc. + + // PyEval_ReleaseLock (Amazon Linux /usr/lib64/libpython3.7m.so.1.0): + // ADRP X0, .+0x148000 + // LDR X1, [X0,#1960] + // ADD X2, X1, #0x5d8 1. X2's regOffset is 0x5d8 (the value we want) + // LDR X0, [X2] 2. The argument register is loaded via X2 + // B .+0xfffffffffffffe88 + + // PyGILState_GetThisThreadState (Amazon Linux /usr/lib64/libpython3.7m.so.1.0): + // ADRP X0, .+0x251000 + // LDR X2, [X0,#1960] + // LDR X1, [X2,#1512] + // CBZ X1, .+0xc + // ADD X0, X2, #0x5f0 1. X0's regOffset gets 0x5f0 + // B .+0xfffffffffffb92b4 + + // PyGILState_GetThisThreadState (Debian 11 /usr/bin/python3): + // ADRP X0, #0x907000 + // ADD X2, X0, #0x880 + // ADD X3, X2, #0x10 + // LDR X1, [X2,#0x260] + // CBZ X1, loc_4740BC + // LDR W0, [X3,#0x25C] ; key + // B .pthread_getspecific + + // Storage for load offsets for each Xn register + var regOffset [32]uint64 + retValue := libpf.SymbolValueInvalid + + for offs := 0; offs < len(code); offs += 4 { + inst, err := aa.Decode(code[offs:]) + if err != nil { + return libpf.SymbolValueInvalid + } + if inst.Op == aa.B { + return retValue + } + + // Interested only on commands modifying Xn + dest, ok := ah.Xreg2num(inst.Args[0]) + if !ok { + continue + } + + instOffset := uint64(0) + instRetval := libpf.SymbolValueInvalid + switch inst.Op { + case aa.ADD: + a2, ok := ah.DecodeImmediate(inst.Args[2]) + if !ok { + break + } + instOffset = a2 + instRetval = addrBase + libpf.SymbolValue(a2) + case aa.LDR: + m, ok := inst.Args[1].(aa.MemImmediate) + if !ok { + break + } + src, ok := ah.Xreg2num(m.Base) + if !ok { + break + } + imm, ok := ah.DecodeImmediate(inst.Args[1]) + if !ok { + break + } + // FIXME: addressing mode not taken into account + // because m.imm is not public, but needed. + instRetval = addrBase + libpf.SymbolValue(regOffset[src]+imm) + } + regOffset[dest] = instOffset + if dest == int(argNumber) { + retValue = instRetval + } + } + + return libpf.SymbolValueInvalid +} diff --git a/interpreter/python/decode_amd64.go b/interpreter/python/decode_amd64.go index c495a64c..8045a4f6 100644 --- a/interpreter/python/decode_amd64.go +++ b/interpreter/python/decode_amd64.go @@ -20,9 +20,14 @@ import ( // #include "../../support/ebpf/types.h" import "C" -func decodeStubArgumentWrapper(code []byte, argNumber uint8, symbolValue, +func decodeStubArgumentWrapperX64(code []byte, argNumber uint8, symbolValue, addrBase libpf.SymbolValue) libpf.SymbolValue { return libpf.SymbolValue(C.decode_stub_argument( (*C.uint8_t)(unsafe.Pointer(&code[0])), C.size_t(len(code)), C.uint8_t(argNumber), C.uint64_t(symbolValue), C.uint64_t(addrBase))) } + +func decodeStubArgumentWrapper(code []byte, argNumber uint8, symbolValue, + addrBase libpf.SymbolValue) libpf.SymbolValue { + return decodeStubArgumentWrapperX64(code, argNumber, symbolValue, addrBase) +} diff --git a/interpreter/python/decode_arm64.go b/interpreter/python/decode_arm64.go index 7b8308d5..ce551e75 100644 --- a/interpreter/python/decode_arm64.go +++ b/interpreter/python/decode_arm64.go @@ -9,95 +9,10 @@ package python import ( - aa "golang.org/x/arch/arm64/arm64asm" - "github.com/elastic/otel-profiling-agent/libpf" - ah "github.com/elastic/otel-profiling-agent/libpf/armhelpers" ) -// decodeStubArgumentWrapper disassembles arm64 code and decodes the assumed value -// of requested argument. func decodeStubArgumentWrapper(code []byte, argNumber uint8, symbolValue, addrBase libpf.SymbolValue) libpf.SymbolValue { - // The concept is to track the latest load offset for all X0..X30 registers. - // These registers are used as the function arguments. Once the first branch - // instruction (function call/tail jump) is found, the state of the requested - // argument register's offset is inspected and returned if found. - // It is seen often that the load with offset happens to intermediate register - // first, and is later moved to the argument register. Because of this, the - // tracking requires extra effort between register moves etc. - - // PyEval_ReleaseLock (Amazon Linux /usr/lib64/libpython3.7m.so.1.0): - // ADRP X0, .+0x148000 - // LDR X1, [X0,#1960] - // ADD X2, X1, #0x5d8 1. X2's regOffset is 0x5d8 (the value we want) - // LDR X0, [X2] 2. The argument register is loaded via X2 - // B .+0xfffffffffffffe88 - - // PyGILState_GetThisThreadState (Amazon Linux /usr/lib64/libpython3.7m.so.1.0): - // ADRP X0, .+0x251000 - // LDR X2, [X0,#1960] - // LDR X1, [X2,#1512] - // CBZ X1, .+0xc - // ADD X0, X2, #0x5f0 1. X0's regOffset gets 0x5f0 - // B .+0xfffffffffffb92b4 - - // PyGILState_GetThisThreadState (Debian 11 /usr/bin/python3): - // ADRP X0, #0x907000 - // ADD X2, X0, #0x880 - // ADD X3, X2, #0x10 - // LDR X1, [X2,#0x260] - // CBZ X1, loc_4740BC - // LDR W0, [X3,#0x25C] ; key - // B .pthread_getspecific - - // Storage for load offsets for each Xn register - var regOffset [32]uint64 - retValue := libpf.SymbolValueInvalid - - for offs := 0; offs < len(code); offs += 4 { - inst, err := aa.Decode(code[offs:]) - if err != nil { - return libpf.SymbolValueInvalid - } - if inst.Op == aa.B { - return retValue - } - - // Interested only on commands modifying Xn - dest, ok := ah.Xreg2num(inst.Args[0]) - if !ok { - continue - } - - instOffset := uint64(0) - instRetval := libpf.SymbolValueInvalid - switch inst.Op { - case aa.ADD: - a2, ok := ah.DecodeImmediate(inst.Args[2]) - if !ok { - break - } - instOffset = a2 - instRetval = addrBase + libpf.SymbolValue(a2) - case aa.LDR: - m, ok := inst.Args[1].(aa.MemImmediate) - if !ok { - break - } - src, ok := ah.Xreg2num(m.Base) - if !ok { - break - } - // FIXME: addressing mode not taken into account - // because m.imm is not public, but needed. - instRetval = addrBase + libpf.SymbolValue(regOffset[src]) - } - regOffset[dest] = instOffset - if dest == int(argNumber) { - retValue = instRetval - } - } - - return libpf.SymbolValueInvalid + return decodeStubArgumentWrapperARM64(code, argNumber, symbolValue, addrBase) } diff --git a/interpreter/python/decode_arm64_test.go b/interpreter/python/decode_test.go similarity index 54% rename from interpreter/python/decode_arm64_test.go rename to interpreter/python/decode_test.go index 2f3b4d8b..3a2e8d78 100644 --- a/interpreter/python/decode_arm64_test.go +++ b/interpreter/python/decode_test.go @@ -1,5 +1,3 @@ -//go:build arm64 - /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Apache License 2.0. @@ -16,7 +14,7 @@ import ( ) func TestAnalyzeArm64Stubs(t *testing.T) { - val := decodeStubArgumentWrapper( + val := decodeStubArgumentWrapperARM64( []byte{ 0x40, 0x0a, 0x00, 0x90, 0x01, 0xd4, 0x43, 0xf9, 0x22, 0x60, 0x17, 0x91, 0x40, 0x00, 0x40, 0xf9, @@ -24,11 +22,26 @@ func TestAnalyzeArm64Stubs(t *testing.T) { 0, 0, 0) assert.Equal(t, libpf.SymbolValue(1496), val, "PyEval_ReleaseLock stub test") - val = decodeStubArgumentWrapper( + val = decodeStubArgumentWrapperARM64( []byte{ 0x80, 0x12, 0x00, 0xb0, 0x02, 0xd4, 0x43, 0xf9, 0x41, 0xf4, 0x42, 0xf9, 0x61, 0x00, 0x00, 0xb4, 0x40, 0xc0, 0x17, 0x91, 0xad, 0xe4, 0xfe, 0x17}, 0, 0, 0) assert.Equal(t, libpf.SymbolValue(1520), val, "PyGILState_GetThisThreadState test") + + // Python 3.10.12 on ARM64 Nix + val = decodeStubArgumentWrapperARM64( + []byte{ + 0x40, 0x1a, 0x00, 0xd0, // adrp x0, 0xffffa0eff000 + 0x00, 0xa0, 0x46, 0xf9, // ldr x0, [x0, #3392] + 0x01, 0x28, 0x41, 0xf9, // ldr x1, [x0, #592] + 0x61, 0x00, 0x00, 0xb4, // cbz x1, 0xffffa0bb53c8 + 0x00, 0x5c, 0x42, 0xb9, // ldr w0, [x0, #604] + 0x93, 0x20, 0xff, 0x17, // b 0xffffa0b7d610 + 0x00, 0x00, 0x80, 0xd2, // mov x0, #0x0 + 0xc0, 0x03, 0x5f, 0xd6, // ret + }, + 0, 0, 0) + assert.Equal(t, libpf.SymbolValue(604), val, "PyGILState_GetThisThreadState test") } diff --git a/interpreter/python/python.go b/interpreter/python/python.go index 85ccff25..b7508bd6 100644 --- a/interpreter/python/python.go +++ b/interpreter/python/python.go @@ -7,9 +7,12 @@ package python import ( + "bytes" "debug/elf" + "errors" "fmt" "hash/fnv" + "io" "reflect" "regexp" "strconv" @@ -19,18 +22,20 @@ import ( log "github.com/sirupsen/logrus" + "github.com/elastic/go-freelru" + "github.com/elastic/otel-profiling-agent/host" "github.com/elastic/otel-profiling-agent/interpreter" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/freelru" - npsr "github.com/elastic/otel-profiling-agent/libpf/nopanicslicereader" "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/elastic/otel-profiling-agent/libpf/remotememory" - "github.com/elastic/otel-profiling-agent/libpf/successfailurecounter" "github.com/elastic/otel-profiling-agent/metrics" + npsr "github.com/elastic/otel-profiling-agent/nopanicslicereader" + "github.com/elastic/otel-profiling-agent/remotememory" "github.com/elastic/otel-profiling-agent/reporter" + "github.com/elastic/otel-profiling-agent/successfailurecounter" "github.com/elastic/otel-profiling-agent/support" "github.com/elastic/otel-profiling-agent/tpbase" + "github.com/elastic/otel-profiling-agent/util" ) // #include @@ -44,6 +49,11 @@ var ( libpythonRegex = regexp.MustCompile(`^(?:.*/)?libpython(\d)\.(\d+)[^/]*`) ) +// pythonVer builds a version number from readable numbers +func pythonVer(major, minor int) uint16 { + return uint16(major)*0x100 + uint16(minor) +} + // nolint:lll type pythonData struct { version uint16 @@ -94,10 +104,11 @@ type pythonData struct { Frame uint `name:"frame"` } PyFrameObject struct { - Back uint `name:"f_back"` - Code uint `name:"f_code"` - LastI uint `name:"f_lasti"` - IsEntry uint `name:"f_is_entry"` + Back uint `name:"f_back"` + Code uint `name:"f_code"` + LastI uint `name:"f_lasti"` + EntryMember uint // field depends on python version + EntryVal uint // value depends on python version } // https://github.com/python/cpython/blob/deaf509e8fc6e0363bd6f26d52ad42f976ec42f2/Include/cpython/pystate.h#L38 PyCFrame struct { @@ -112,7 +123,7 @@ func (d *pythonData) String() string { return fmt.Sprintf("Python %d.%d", d.version>>8, d.version&0xff) } -func (d *pythonData) Attach(_ interpreter.EbpfHandler, _ libpf.PID, bias libpf.Address, +func (d *pythonData) Attach(_ interpreter.EbpfHandler, _ util.PID, bias libpf.Address, rm remotememory.RemoteMemory) (interpreter.Instance, error) { addrToCodeObject, err := freelru.New[libpf.Address, *pythonCodeObject](interpreter.LruFunctionCacheSize, @@ -128,10 +139,10 @@ func (d *pythonData) Attach(_ interpreter.EbpfHandler, _ libpf.PID, bias libpf.A addrToCodeObject: addrToCodeObject, } - switch d.version { - case 0x030b: + switch { + case d.version >= pythonVer(3, 11): i.getFuncOffset = walkLocationTable - case 0x030a: + case d.version == pythonVer(3, 10): i.getFuncOffset = walkLineTable default: i.getFuncOffset = mapByteCodeIndexToLine @@ -176,53 +187,48 @@ type pythonCodeObject struct { bciSeen libpf.Set[uint32] } -// readSignedVarint returns a variable length encoded signed integer from a location table entry. -func readSignedVarint(lt []byte) int { - uval := readVarint(lt) - if (uval & 1) != 0 { - return int(uval>>1) * -1 - } - return int(uval >> 1) -} - // readVarint returns a variable length encoded unsigned integer from a location table entry. -func readVarint(lt []byte) uint { - lenLT := len(lt) - i := 0 - nextVal := lt[i] - i++ - val := uint(nextVal & 63) - shift := 0 - for (nextVal & 64) != 0 { - if i >= lenLT { +func readVarint(r io.ByteReader) uint32 { + val := uint32(0) + b := byte(0x40) + for shift := 0; b&0x40 != 0; shift += 6 { + var err error + b, err = r.ReadByte() + if err != nil || b&0x80 != 0 { return 0 } - nextVal = lt[i] - i++ - shift += 6 - val |= uint(nextVal&63) << shift + val |= uint32(b&0x3f) << shift } return val } +// readSignedVarint returns a variable length encoded signed integer from a location table entry. +func readSignedVarint(r io.ByteReader) int32 { + uval := readVarint(r) + if uval&1 != 0 { + return -int32(uval >> 1) + } + return int32(uval >> 1) +} + // walkLocationTable implements the algorithm to read entries from the location table. // This was introduced in Python 3.11. // nolint:lll // https://github.com/python/cpython/blob/deaf509e8fc6e0363bd6f26d52ad42f976ec42f2/Objects/locations.md -func walkLocationTable(m *pythonCodeObject, addrq uint32) uint32 { - if addrq == 0 { - return 0 - } - lineTable := m.lineTable - lenLineTable := len(lineTable) - var i, steps int - var firstByte, code uint8 - var line int - for i = 0; i < lenLineTable; { - // firstByte encodes initial information about the table entry and how to handle it. - firstByte = lineTable[i] +func walkLocationTable(m *pythonCodeObject, bci uint32) uint32 { + r := bytes.NewReader(m.lineTable) + curI := uint32(0) + line := int32(0) + for curI <= bci { + firstByte, err := r.ReadByte() + if err != nil || firstByte&0x80 == 0 { + log.Debugf("first byte: sync lost (%x) or error: %v", + firstByte, err) + return 0 + } - code = (firstByte >> 3) & 15 + code := (firstByte >> 3) & 15 + curI += uint32(firstByte&7) + 1 // Handle the 16 possible different codes known as _PyCodeLocationInfoKind. // nolint:lll @@ -230,48 +236,33 @@ func walkLocationTable(m *pythonCodeObject, addrq uint32) uint32 { switch code { case 0, 1, 2, 3, 4, 5, 6, 7, 8, 9: // PY_CODE_LOCATION_INFO_SHORT does not hold line information. - steps = 1 + _, _ = r.ReadByte() case 10, 11, 12: - // PY_CODE_LOCATION_INFO_ONE_LINE embeds the line information in the code. - steps = 2 - line += int(code - 10) + // PY_CODE_LOCATION_INFO_ONE_LINE embeds the line information in the code + // follows two bytes containing new columns. + line += int32(code - 10) + _, _ = r.ReadByte() + _, _ = r.ReadByte() case 13: // PY_CODE_LOCATION_INFO_NO_COLUMNS - steps = 1 - if i+1 >= lenLineTable { - return 0 - } - diff := readSignedVarint(lineTable[i+1:]) - line += diff + line += readSignedVarint(r) case 14: // PY_CODE_LOCATION_INFO_LONG - steps = 4 - if i+1 >= lenLineTable { - return 0 - } - diff := readSignedVarint(lineTable[i+1:]) - line += diff + line += readSignedVarint(r) + _ = readVarint(r) + _ = readVarint(r) + _ = readVarint(r) case 15: // PY_CODE_LOCATION_INFO_NONE does not hold line information - steps = 0 + line = -1 default: log.Debugf("Unexpected PyCodeLocationInfoKind %d", code) return 0 } - - // Calculate position of the next table entry. - // One is added for the firstByte of the current entry and steps represents its - // variable length. - i += steps + 1 - - if line >= int(addrq) { - return uint32(line) - } } if line < 0 { - return 0 + line = 0 } - return uint32(line) } @@ -329,7 +320,7 @@ func mapByteCodeIndexToLine(m *pythonCodeObject, bci uint32) uint32 { return lineno } -func (m *pythonCodeObject) symbolize(symbolizer interpreter.Symbolizer, bci uint32, +func (m *pythonCodeObject) symbolize(symbolReporter reporter.SymbolReporter, bci uint32, getFuncOffset getFuncOffsetFunc, trace *libpf.Trace) error { trace.AppendFrame(libpf.PythonFrame, m.fileID, libpf.AddressOrLineno(bci)) @@ -338,11 +329,11 @@ func (m *pythonCodeObject) symbolize(symbolizer interpreter.Symbolizer, bci uint return nil } - var lineNo libpf.SourceLineno + var lineNo util.SourceLineno functionOffset := getFuncOffset(m, bci) - lineNo = libpf.SourceLineno(m.firstLineNo + functionOffset) + lineNo = util.SourceLineno(m.firstLineNo + functionOffset) - symbolizer.FrameMetadata(m.fileID, + symbolReporter.FrameMetadata(m.fileID, libpf.AddressOrLineno(bci), lineNo, functionOffset, m.name, m.sourceFileName) @@ -351,10 +342,10 @@ func (m *pythonCodeObject) symbolize(symbolizer interpreter.Symbolizer, bci uint // Until the reporting API gets a way to notify failures, just assume it worked. m.bciSeen[bci] = libpf.Void{} - log.Debugf("[%d] [%x] %v+%v at %v:%v", len(trace.FrameTypes), + log.Debugf("[%d] [%x] %v+%v at %v:%v (bci %d)", len(trace.FrameTypes), m.fileID, m.name, functionOffset, - m.sourceFileName, lineNo) + m.sourceFileName, lineNo, bci) return nil } @@ -388,7 +379,7 @@ type pythonInstance struct { var _ interpreter.Instance = &pythonInstance{} func (p *pythonInstance) GetAndResetMetrics() ([]metrics.Metric, error) { - addrToCodeObjectStats := p.addrToCodeObject.GetAndResetStatistics() + addrToCodeObjectStats := p.addrToCodeObject.ResetMetrics() return []metrics.Metric{ { @@ -401,24 +392,24 @@ func (p *pythonInstance) GetAndResetMetrics() ([]metrics.Metric, error) { }, { ID: metrics.IDPythonAddrToCodeObjectHit, - Value: metrics.MetricValue(addrToCodeObjectStats.Hit), + Value: metrics.MetricValue(addrToCodeObjectStats.Hits), }, { ID: metrics.IDPythonAddrToCodeObjectMiss, - Value: metrics.MetricValue(addrToCodeObjectStats.Miss), + Value: metrics.MetricValue(addrToCodeObjectStats.Misses), }, { ID: metrics.IDPythonAddrToCodeObjectAdd, - Value: metrics.MetricValue(addrToCodeObjectStats.Added), + Value: metrics.MetricValue(addrToCodeObjectStats.Inserts), }, { ID: metrics.IDPythonAddrToCodeObjectDel, - Value: metrics.MetricValue(addrToCodeObjectStats.Deleted), + Value: metrics.MetricValue(addrToCodeObjectStats.Removals), }, }, nil } -func (p *pythonInstance) UpdateTSDInfo(ebpf interpreter.EbpfHandler, pid libpf.PID, +func (p *pythonInstance) UpdateTSDInfo(ebpf interpreter.EbpfHandler, pid util.PID, tsdInfo tpbase.TSDInfo) error { d := p.d vm := &d.vmStructs @@ -437,11 +428,13 @@ func (p *pythonInstance) UpdateTSDInfo(ebpf interpreter.EbpfHandler, pid libpf.P PyFrameObject_f_back: C.u8(vm.PyFrameObject.Back), PyFrameObject_f_code: C.u8(vm.PyFrameObject.Code), PyFrameObject_f_lasti: C.u8(vm.PyFrameObject.LastI), - PyFrameObject_f_is_entry: C.u8(vm.PyFrameObject.IsEntry), + PyFrameObject_entry_member: C.u8(vm.PyFrameObject.EntryMember), + PyFrameObject_entry_val: C.u8(vm.PyFrameObject.EntryVal), PyCodeObject_co_argcount: C.u8(vm.PyCodeObject.ArgCount), PyCodeObject_co_kwonlyargcount: C.u8(vm.PyCodeObject.KwOnlyArgCount), PyCodeObject_co_flags: C.u8(vm.PyCodeObject.Flags), PyCodeObject_co_firstlineno: C.u8(vm.PyCodeObject.FirstLineno), + PyCodeObject_sizeof: C.u8(vm.PyCodeObject.Sizeof), } err := ebpf.UpdateProcData(libpf.Python, pid, unsafe.Pointer(&cdata)) @@ -453,7 +446,7 @@ func (p *pythonInstance) UpdateTSDInfo(ebpf interpreter.EbpfHandler, pid libpf.P return err } -func (p *pythonInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error { +func (p *pythonInstance) Detach(ebpf interpreter.EbpfHandler, pid util.PID) error { if !p.procInfoInserted { return nil } @@ -499,7 +492,7 @@ func frozenNameToFileName(sourceFileName string) (string, error) { func (p *pythonInstance) getCodeObject(addr libpf.Address, ebpfChecksum uint32) (*pythonCodeObject, error) { if addr == 0 { - return nil, fmt.Errorf("failed to read code object: null pointer") + return nil, errors.New("failed to read code object: null pointer") } if value, ok := p.addrToCodeObject.Get(addr); ok { m := value @@ -522,7 +515,7 @@ func (p *pythonInstance) getCodeObject(addr libpf.Address, data := libpf.Address(vms.PyASCIIObject.Data) var lineInfoPtr libpf.Address - if p.d.version < 0x030a { + if p.d.version < pythonVer(3, 10) { lineInfoPtr = npsr.Ptr(cobj, vms.PyCodeObject.Lnotab) } else { lineInfoPtr = npsr.Ptr(cobj, vms.PyCodeObject.Linetable) @@ -535,7 +528,7 @@ func (p *pythonInstance) getCodeObject(addr libpf.Address, if name == "" { name = p.rm.String(data + npsr.Ptr(cobj, vms.PyCodeObject.Name)) } - if !libpf.IsValidString(name) { + if !util.IsValidString(name) { log.Debugf("Extracted invalid Python method/function name at 0x%x '%v'", addr, []byte(name)) return nil, fmt.Errorf("extracted invalid Python method/function name from address 0x%x", @@ -550,7 +543,7 @@ func (p *pythonInstance) getCodeObject(addr libpf.Address, if err != nil { sourceFileName = sourcePath } - if !libpf.IsValidString(sourceFileName) { + if !util.IsValidString(sourceFileName) { log.Debugf("Extracted invalid Python source file name at 0x%x '%v'", addr, []byte(sourceFileName)) return nil, fmt.Errorf("extracted invalid Python source file name from address 0x%x", @@ -565,7 +558,7 @@ func (p *pythonInstance) getCodeObject(addr libpf.Address, } lineTableSize := p.rm.Uint64(lineInfoPtr + libpf.Address(vms.PyVarObject.ObSize)) - if lineTableSize >= 0x10000 || (p.d.version < 0x30b && lineTableSize&1 != 0) { + if lineTableSize >= 0x10000 || (p.d.version < pythonVer(3, 11) && lineTableSize&1 != 0) { return nil, fmt.Errorf("invalid line table size (%v)", lineTableSize) } lineTable := make([]byte, lineTableSize) @@ -748,9 +741,10 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr var pyruntimeAddr, autoTLSKey libpf.SymbolValue major, _ := strconv.Atoi(matches[1]) minor, _ := strconv.Atoi(matches[2]) - version := uint16(major*0x100 + minor) + version := pythonVer(major, minor) - const minVer, maxVer = 0x306, 0x30b + minVer := pythonVer(3, 6) + maxVer := pythonVer(3, 12) if version < minVer || version > maxVer { return nil, fmt.Errorf("unsupported Python %d.%d (need >= %d.%d and <= %d.%d)", major, minor, @@ -758,7 +752,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr (maxVer>>8)&0xff, maxVer&0xff) } - if version >= 0x307 { + if version >= pythonVer(3, 7) { if pyruntimeAddr, err = ef.LookupSymbolAddress("_PyRuntime"); err != nil { return nil, fmt.Errorf("_PyRuntime not defined: %v", err) } @@ -767,9 +761,9 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr // Calls first: PyThread_tss_get(autoTSSKey) autoTLSKey = decodeStub(ef, pyruntimeAddr, "PyGILState_GetThisThreadState", 0) if autoTLSKey == libpf.SymbolValueInvalid { - return nil, fmt.Errorf("unable to resolve autoTLSKey") + return nil, errors.New("unable to resolve autoTLSKey") } - if version >= 0x307 && autoTLSKey%8 == 0 { + if version >= pythonVer(3, 7) && autoTLSKey%8 == 0 { // On Python 3.7+, the call is to PyThread_tss_get, but can get optimized to // call directly pthread_getspecific. So we might be finding the address // for "Py_tss_t" or "pthread_key_t" depending on call target. @@ -790,9 +784,6 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr // 0b72b23fb0c v3.9 2020-03-12 _PyEval_EvalFrameDefault(PyThreadState*,PyFrameObject*,int) // 3cebf938727 v3.6 2016-09-05 _PyEval_EvalFrameDefault(PyFrameObject*,int) // 49fd7fa4431 v3.0 2006-04-21 PyEval_EvalFrameEx(PyFrameObject*,int) - // - // Try the two known symbols, and fall back to .text section in case the symbol - // was not exported for some strange reason. interpRanges, err := info.GetSymbolAsRanges("_PyEval_EvalFrameDefault") if err != nil { if interpRanges, err = info.GetSymbolAsRanges("PyEval_EvalFrameEx"); err != nil { @@ -817,21 +808,32 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr vms.PyVarObject.ObSize = 16 vms.PyThreadState.Frame = 24 - if version >= 0x30b { + switch version { + case pythonVer(3, 11): // Starting with 3.11 we no longer can extract needed information from // PyFrameObject. In addition PyFrameObject was replaced with _PyInterpreterFrame. // The following offsets come from _PyInterpreterFrame but we continue to use // PyFrameObject as the structure name, since the struct elements serve the same // function as before. vms.PyFrameObject.Code = 32 - vms.PyFrameObject.LastI = 56 // f_lasti got renamed to prev_instr - vms.PyFrameObject.Back = 48 // f_back got renamed to previous - vms.PyFrameObject.IsEntry = 68 - + vms.PyFrameObject.LastI = 56 // _Py_CODEUNIT *prev_instr + vms.PyFrameObject.Back = 48 // struct _PyInterpreterFrame *previous + vms.PyFrameObject.EntryMember = 68 // bool is_entry + vms.PyFrameObject.EntryVal = 1 // true, from stdbool.h // frame got removed in PyThreadState but we can use cframe instead. vms.PyThreadState.Frame = 56 - vms.PyCFrame.CurrentFrame = 8 + case pythonVer(3, 12): + // Entry frame detection changed due to the shim frame + // https://github.com/python/cpython/commit/1e197e63e21f77b102ff2601a549dda4b6439455 + vms.PyFrameObject.Code = 0 + vms.PyFrameObject.LastI = 56 // _Py_CODEUNIT *prev_instr + vms.PyFrameObject.Back = 8 // struct _PyInterpreterFrame *previous + vms.PyFrameObject.EntryMember = 70 // char owner + vms.PyFrameObject.EntryVal = 3 // enum _frameowner, FRAME_OWNED_BY_CSTACK + vms.PyThreadState.Frame = 56 + vms.PyCFrame.CurrentFrame = 0 + vms.PyASCIIObject.Data = 40 } // Read the introspection data from objects types that have it diff --git a/interpreter/python/python_test.go b/interpreter/python/python_test.go index bfc3c890..66c3c322 100644 --- a/interpreter/python/python_test.go +++ b/interpreter/python/python_test.go @@ -9,6 +9,8 @@ package python import ( "regexp" "testing" + + "github.com/stretchr/testify/assert" ) func TestFrozenNameToFileName(t *testing.T) { @@ -72,9 +74,8 @@ func TestPythonRegexs(t *testing.T) { for regex, strings := range shouldMatch { for _, s := range strings { - if !regex.MatchString(s) { - t.Fatalf("regex %s should match %s", regex.String(), s) - } + assert.Truef(t, regex.MatchString(s), + "%s should match: %v", regex.String(), s) } } @@ -89,9 +90,8 @@ func TestPythonRegexs(t *testing.T) { for regex, strings := range shouldNotMatch { for _, s := range strings { - if regex.MatchString(s) { - t.Fatalf("regex %s should not match %s", regex.String(), s) - } + assert.Falsef(t, regex.MatchString(s), + "%v should not match: %v", regex.String(), s) } } } diff --git a/interpreter/ruby/ruby.go b/interpreter/ruby/ruby.go index 4f31b938..fcf9050f 100644 --- a/interpreter/ruby/ruby.go +++ b/interpreter/ruby/ruby.go @@ -8,6 +8,7 @@ package ruby import ( "encoding/binary" + "errors" "fmt" "hash/fnv" "math/bits" @@ -20,17 +21,19 @@ import ( log "github.com/sirupsen/logrus" + "github.com/elastic/go-freelru" + "github.com/elastic/otel-profiling-agent/host" "github.com/elastic/otel-profiling-agent/interpreter" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/freelru" "github.com/elastic/otel-profiling-agent/libpf/hash" "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/elastic/otel-profiling-agent/libpf/remotememory" - "github.com/elastic/otel-profiling-agent/libpf/successfailurecounter" "github.com/elastic/otel-profiling-agent/metrics" + "github.com/elastic/otel-profiling-agent/remotememory" "github.com/elastic/otel-profiling-agent/reporter" + "github.com/elastic/otel-profiling-agent/successfailurecounter" "github.com/elastic/otel-profiling-agent/support" + "github.com/elastic/otel-profiling-agent/util" ) // #include "../../support/ebpf/types.h" @@ -175,7 +178,11 @@ type rubyData struct { } } -func (r *rubyData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libpf.Address, +func rubyVersion(major, minor, release uint32) uint32 { + return major*0x10000 + minor*0x100 + release +} + +func (r *rubyData) Attach(ebpf interpreter.EbpfHandler, pid util.PID, bias libpf.Address, rm remotememory.RemoteMemory) (interpreter.Instance, error) { cdata := C.RubyProcInfo{ version: C.u32(r.version), @@ -282,7 +289,7 @@ type rubyInstance struct { maxSize atomic.Uint32 } -func (r *rubyInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error { +func (r *rubyInstance) Detach(ebpf interpreter.EbpfHandler, pid util.PID) error { return ebpf.DeleteProcData(libpf.Ruby, pid) } @@ -423,7 +430,7 @@ func (r *rubyInstance) getObsoleteRubyLineNo(iseqBody libpf.Address, ptr := r.rm.Ptr(iseqBody + libpf.Address(vms.iseq_constant_body.insn_info_body)) syncPoolData := r.memPool.Get().(*[]byte) if syncPoolData == nil { - return 0, fmt.Errorf("failed to get memory from sync pool") + return 0, errors.New("failed to get memory from sync pool") } if uint32(len(*syncPoolData)) < size*sizeOfEntry { // make sure the data we want to write into blob fits in @@ -519,16 +526,18 @@ func (r *rubyInstance) getRubyLineNo(iseqBody libpf.Address, pc uint64) (uint32, // For our better understanding and future improvement we track the maximum value we get for // size and report it. - libpf.AtomicUpdateMaxUint32(&r.maxSize, size) + util.AtomicUpdateMaxUint32(&r.maxSize, size) // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/iseq.c#L1678 if size == 0 { - return 0, fmt.Errorf("failed to read size") - } else if size == 1 { + return 0, errors.New("failed to read size") + } + if size == 1 { offsetBody := vms.iseq_constant_body.insn_info_body lineNo := binary.LittleEndian.Uint32(blob[offsetBody : offsetBody+4]) return lineNo, nil - } else if size > rubyInsnInfoSizeLimit { + } + if size > rubyInsnInfoSizeLimit { // When reading the value for size we don't have a way to validate this returned // value. To make sure we don't accept any arbitrary number we set here a limit of // 1MB. @@ -563,7 +572,7 @@ func (r *rubyInstance) getRubyLineNo(iseqBody libpf.Address, pc uint64) (uint32, if succIndexTable == 0 { // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/iseq.c#L1686 - return 0, fmt.Errorf("failed to get table with line information") + return 0, errors.New("failed to get table with line information") } // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/iseq.c#L3500-L3517 @@ -574,7 +583,7 @@ func (r *rubyInstance) getRubyLineNo(iseqBody libpf.Address, pc uint64) (uint32, immPart := r.rm.Uint64(libpf.Address(succIndexTable) + libpf.Address(i*int(vms.size_of_value))) if immPart == 0 { - return 0, fmt.Errorf("failed to read immPart") + return 0, errors.New("failed to read immPart") } tableIndex = immBlockRankGet(immPart, uint32(j)) } else { @@ -585,7 +594,7 @@ func (r *rubyInstance) getRubyLineNo(iseqBody libpf.Address, pc uint64) (uint32, rank := r.rm.Uint32(libpf.Address(succIndexTable) + libpf.Address(vms.succ_index_table_struct.succ_part) + blockOffset) if rank == 0 { - return 0, fmt.Errorf("failed to read rank") + return 0, errors.New("failed to read rank") } blockBitIndex := uint32((pos - uint64(vms.size_of_immediate_table)) % 512) @@ -596,7 +605,7 @@ func (r *rubyInstance) getRubyLineNo(iseqBody libpf.Address, pc uint64) (uint32, libpf.Address(vms.succ_index_table_struct.succ_part+ vms.succ_index_table_struct.small_block_ranks)) if smallBlockRanks == 0 { - return 0, fmt.Errorf("failed to read smallBlockRanks") + return 0, errors.New("failed to read smallBlockRanks") } smallBlockPopcount := smallBlockRankGet(smallBlockRanks, smallBlockIndex) @@ -605,7 +614,7 @@ func (r *rubyInstance) getRubyLineNo(iseqBody libpf.Address, pc uint64) (uint32, libpf.Address(vms.succ_index_table_struct.succ_part+ vms.succ_index_table_struct.block_bits) + smallBlockOffset) if blockBits == 0 { - return 0, fmt.Errorf("failed to read blockBits") + return 0, errors.New("failed to read blockBits") } popCnt := rubyPopcount64((blockBits << (63 - blockBitIndex%64))) @@ -616,13 +625,13 @@ func (r *rubyInstance) getRubyLineNo(iseqBody libpf.Address, pc uint64) (uint32, offsetBody := vms.iseq_constant_body.insn_info_body lineNoAddr := binary.LittleEndian.Uint64(blob[offsetBody : offsetBody+8]) if lineNoAddr == 0 { - return 0, fmt.Errorf("failed to read lineNoAddr") + return 0, errors.New("failed to read lineNoAddr") } lineNo := r.rm.Uint32(libpf.Address(lineNoAddr) + libpf.Address(tableIndex*uint32(vms.iseq_insn_info_entry.size_of_iseq_insn_info_entry))) if lineNo == 0 { - return 0, fmt.Errorf("failed to read lineNo") + return 0, errors.New("failed to read lineNo") } return lineNo, nil } @@ -669,7 +678,7 @@ func (r *rubyInstance) Symbolize(symbolReporter reporter.SymbolReporter, if err != nil { return err } - if !libpf.IsValidString(sourceFileName) { + if !util.IsValidString(sourceFileName) { log.Debugf("Extracted invalid Ruby source file name at 0x%x '%v'", iseqBody, []byte(sourceFileName)) return fmt.Errorf("extracted invalid Ruby source file name from address 0x%x", @@ -682,7 +691,7 @@ func (r *rubyInstance) Symbolize(symbolReporter reporter.SymbolReporter, if err != nil { return err } - if !libpf.IsValidString(functionName) { + if !util.IsValidString(functionName) { log.Debugf("Extracted invalid Ruby method name at 0x%x '%v'", iseqBody, []byte(functionName)) return fmt.Errorf("extracted invalid Ruby method name from address 0x%x", @@ -716,7 +725,7 @@ func (r *rubyInstance) Symbolize(symbolReporter reporter.SymbolReporter, // particular line. So we report 0 for this to our backend. symbolReporter.FrameMetadata( fileID, - libpf.AddressOrLineno(lineNo), libpf.SourceLineno(lineNo), 0, + libpf.AddressOrLineno(lineNo), util.SourceLineno(lineNo), 0, functionName, sourceFileName) log.Debugf("[%d] [%x] %v+%v at %v:%v", len(trace.FrameTypes), @@ -729,8 +738,8 @@ func (r *rubyInstance) Symbolize(symbolReporter reporter.SymbolReporter, } func (r *rubyInstance) GetAndResetMetrics() ([]metrics.Metric, error) { - rubyIseqBodyPCStats := r.iseqBodyPCToFunction.GetAndResetStatistics() - addrToStringStats := r.addrToString.GetAndResetStatistics() + rubyIseqBodyPCStats := r.iseqBodyPCToFunction.ResetMetrics() + addrToStringStats := r.addrToString.ResetMetrics() return []metrics.Metric{ { @@ -743,35 +752,35 @@ func (r *rubyInstance) GetAndResetMetrics() ([]metrics.Metric, error) { }, { ID: metrics.IDRubyIseqBodyPCHit, - Value: metrics.MetricValue(rubyIseqBodyPCStats.Hit), + Value: metrics.MetricValue(rubyIseqBodyPCStats.Hits), }, { ID: metrics.IDRubyIseqBodyPCMiss, - Value: metrics.MetricValue(rubyIseqBodyPCStats.Miss), + Value: metrics.MetricValue(rubyIseqBodyPCStats.Misses), }, { ID: metrics.IDRubyIseqBodyPCAdd, - Value: metrics.MetricValue(rubyIseqBodyPCStats.Added), + Value: metrics.MetricValue(rubyIseqBodyPCStats.Inserts), }, { ID: metrics.IDRubyIseqBodyPCDel, - Value: metrics.MetricValue(rubyIseqBodyPCStats.Deleted), + Value: metrics.MetricValue(rubyIseqBodyPCStats.Removals), }, { ID: metrics.IDRubyAddrToStringHit, - Value: metrics.MetricValue(addrToStringStats.Hit), + Value: metrics.MetricValue(addrToStringStats.Hits), }, { ID: metrics.IDRubyAddrToStringMiss, - Value: metrics.MetricValue(addrToStringStats.Miss), + Value: metrics.MetricValue(addrToStringStats.Misses), }, { ID: metrics.IDRubyAddrToStringAdd, - Value: metrics.MetricValue(addrToStringStats.Added), + Value: metrics.MetricValue(addrToStringStats.Inserts), }, { ID: metrics.IDRubyAddrToStringDel, - Value: metrics.MetricValue(addrToStringStats.Deleted), + Value: metrics.MetricValue(addrToStringStats.Removals), }, { ID: metrics.IDRubyMaxSize, @@ -800,7 +809,7 @@ func determineRubyVersion(ef *pfelf.File) (uint32, error) { minor, _ := strconv.Atoi(matches[2]) release, _ := strconv.Atoi(matches[3]) - return uint32(major*0x10000 + minor*0x100 + release), nil + return rubyVersion(uint32(major), uint32(minor), uint32(release)), nil } func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { @@ -824,7 +833,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr // Reason for maximum supported version 3.2.x: // - this is currently the newest stable version - const minVer, maxVer = 0x20500, 0x30300 + minVer, maxVer := rubyVersion(2, 5, 0), rubyVersion(3, 3, 0) if version < minVer || version >= maxVer { return nil, fmt.Errorf("unsupported Ruby %d.%d.%d (need >= %d.%d.%d and <= %d.%d.%d)", (version>>16)&0xff, (version>>8)&0xff, version&0xff, @@ -840,7 +849,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr // [0] https://github.com/ruby/ruby/commit/837fd5e494731d7d44786f29e7d6e8c27029806f // [1] https://github.com/ruby/ruby/commit/79df14c04b452411b9d17e26a398e491bca1a811 currentCtxSymbol := libpf.SymbolName("ruby_single_main_ractor") - if version < 0x30000 { + if version < rubyVersion(3, 0, 0) { currentCtxSymbol = "ruby_current_execution_context_ptr" } currentCtxPtr, err := ef.LookupSymbolAddress(currentCtxSymbol) @@ -852,7 +861,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr // ruby_run_node which is the main executor function since Ruby v1.9.0 // https://github.com/ruby/ruby/blob/587e6800086764a1b7c959976acef33e230dccc2/main.c#L47 symbolName := libpf.SymbolName("rb_vm_exec") - if version < 0x20600 { + if version < rubyVersion(2, 6, 0) { symbolName = libpf.SymbolName("ruby_exec_node") } interpRanges, err := info.GetSymbolAsRanges(symbolName) @@ -877,51 +886,52 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr vms.control_frame_struct.pc = 0 vms.control_frame_struct.iseq = 16 vms.control_frame_struct.ep = 32 - if version < 0x20600 { + switch { + case version < rubyVersion(2, 6, 0): vms.control_frame_struct.size_of_control_frame_struct = 48 - } else if version < 0x30100 { + case version < rubyVersion(3, 1, 0): // With Ruby 2.6 the field bp was added to rb_control_frame_t // https://github.com/ruby/ruby/commit/ed935aa5be0e5e6b8d53c3e7d76a9ce395dfa18b vms.control_frame_struct.size_of_control_frame_struct = 56 - } else { + default: // 3.1 adds new jit_return field at the end. // https://github.com/ruby/ruby/commit/9d8cc01b758f9385bd4c806f3daff9719e07faa0 vms.control_frame_struct.size_of_control_frame_struct = 64 } - vms.iseq_struct.body = 16 vms.iseq_constant_body.iseq_type = 0 vms.iseq_constant_body.size = 4 vms.iseq_constant_body.encoded = 8 vms.iseq_constant_body.location = 64 - if version < 0x20600 { + switch { + case version < rubyVersion(2, 6, 0): vms.iseq_constant_body.insn_info_body = 112 vms.iseq_constant_body.insn_info_size = 200 vms.iseq_constant_body.succ_index_table = 144 vms.iseq_constant_body.size_of_iseq_constant_body = 288 - } else if version < 0x30200 { + case version < rubyVersion(3, 2, 0): vms.iseq_constant_body.insn_info_body = 120 vms.iseq_constant_body.insn_info_size = 136 vms.iseq_constant_body.succ_index_table = 144 vms.iseq_constant_body.size_of_iseq_constant_body = 312 - } else { + default: vms.iseq_constant_body.insn_info_body = 112 vms.iseq_constant_body.insn_info_size = 128 vms.iseq_constant_body.succ_index_table = 136 vms.iseq_constant_body.size_of_iseq_constant_body = 320 } - vms.iseq_location_struct.pathobj = 0 vms.iseq_location_struct.base_label = 8 - if version < 0x20600 { + switch { + case version < rubyVersion(2, 6, 0): vms.iseq_insn_info_entry.position = 0 vms.iseq_insn_info_entry.size_of_position = 4 vms.iseq_insn_info_entry.line_no = 4 vms.iseq_insn_info_entry.size_of_line_no = 4 vms.iseq_insn_info_entry.size_of_iseq_insn_info_entry = 12 - } else if version < 0x30100 { + case version < rubyVersion(3, 1, 0): // The position field was removed from this struct with // https://github.com/ruby/ruby/commit/295838e6eb1d063c64f7cde5bbbd13c7768908fd vms.iseq_insn_info_entry.position = 0 @@ -929,7 +939,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr vms.iseq_insn_info_entry.line_no = 0 vms.iseq_insn_info_entry.size_of_line_no = 4 vms.iseq_insn_info_entry.size_of_iseq_insn_info_entry = 8 - } else { + default: // https://github.com/ruby/ruby/commit/0a36cab1b53646062026c3181117fad73802baf4 vms.iseq_insn_info_entry.position = 0 vms.iseq_insn_info_entry.size_of_position = 0 @@ -937,8 +947,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr vms.iseq_insn_info_entry.size_of_line_no = 4 vms.iseq_insn_info_entry.size_of_iseq_insn_info_entry = 12 } - - if version < 0x30200 { + if version < rubyVersion(3, 2, 0) { vms.rstring_struct.as_ary = 16 } else { vms.rstring_struct.as_ary = 24 @@ -956,7 +965,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr vms.size_of_value = 8 - if version >= 0x30000 { + if version >= rubyVersion(3, 0, 0) { if runtime.GOARCH == "amd64" { vms.rb_ractor_struct.running_ec = 0x208 } else { diff --git a/interpreter/types.go b/interpreter/types.go index 4a2805eb..aecad274 100644 --- a/interpreter/types.go +++ b/interpreter/types.go @@ -12,12 +12,13 @@ import ( "github.com/elastic/otel-profiling-agent/host" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/process" - "github.com/elastic/otel-profiling-agent/libpf/remotememory" "github.com/elastic/otel-profiling-agent/lpm" "github.com/elastic/otel-profiling-agent/metrics" + "github.com/elastic/otel-profiling-agent/process" + "github.com/elastic/otel-profiling-agent/remotememory" "github.com/elastic/otel-profiling-agent/reporter" "github.com/elastic/otel-profiling-agent/tpbase" + "github.com/elastic/otel-profiling-agent/util" ) const ( @@ -84,28 +85,25 @@ var ( // need to hard code different well known offsets in the xxData. It allows then to still // share the Data and Instance code between these versions. -// Symbolizer is the interface to call back for frame symbolization information -type Symbolizer = reporter.SymbolReporter - // EbpfHandler provides the functionality for interpreters to interact with eBPF maps. type EbpfHandler interface { // UpdateInterpreterOffsets adds the given offsetRanges to the eBPF map interpreter_offsets. UpdateInterpreterOffsets(ebpfProgIndex uint16, fileID host.FileID, - offsetRanges []libpf.Range) error + offsetRanges []util.Range) error // UpdateProcData adds the given interpreter data to the named eBPF map. - UpdateProcData(typ libpf.InterpType, pid libpf.PID, data unsafe.Pointer) error + UpdateProcData(typ libpf.InterpreterType, pid util.PID, data unsafe.Pointer) error // DeleteProcData removes any data from the named eBPF map. - DeleteProcData(typ libpf.InterpType, pid libpf.PID) error + DeleteProcData(typ libpf.InterpreterType, pid util.PID) error // UpdatePidInterpreterMapping updates the eBPF map pid_page_to_mapping_info // to call given interpreter unwinder. - UpdatePidInterpreterMapping(libpf.PID, lpm.Prefix, uint8, host.FileID, uint64) error + UpdatePidInterpreterMapping(util.PID, lpm.Prefix, uint8, host.FileID, uint64) error // DeletePidInterpreterMapping removes the element specified by pid, prefix // rom the eBPF map pid_page_to_mapping_info. - DeletePidInterpreterMapping(libpf.PID, lpm.Prefix) error + DeletePidInterpreterMapping(util.PID, lpm.Prefix) error } // Loader is a function to detect and load data from given interpreter ELF file. @@ -123,7 +121,7 @@ type Loader func(ebpf EbpfHandler, info *LoaderInfo) (Data, error) type Data interface { // Attach checks if the given dso is supported, and loads the information // of it to the ebpf maps. - Attach(ebpf EbpfHandler, pid libpf.PID, bias libpf.Address, rm remotememory.RemoteMemory) ( + Attach(ebpf EbpfHandler, pid util.PID, bias libpf.Address, rm remotememory.RemoteMemory) ( Instance, error) } @@ -131,7 +129,7 @@ type Data interface { type Instance interface { // Detach removes any information from the ebpf maps. The pid is given as argument so // simple interpreters can use the global Data also as the Instance implementation. - Detach(ebpf EbpfHandler, pid libpf.PID) error + Detach(ebpf EbpfHandler, pid util.PID) error // SynchronizeMappings is called when the processmanager has reread process memory // mappings. Interpreters not needing to process these events can simply ignore them @@ -141,11 +139,11 @@ type Instance interface { // UpdateTSDInfo is called when the process C-library Thread Specific Data related // introspection data has been updated. - UpdateTSDInfo(ebpf EbpfHandler, pid libpf.PID, info tpbase.TSDInfo) error + UpdateTSDInfo(ebpf EbpfHandler, pid util.PID, info tpbase.TSDInfo) error // Symbolize requests symbolization of the given frame, and dispatches this symbolization - // to the collection agent using Symbolizer interface. The frame's contents (frame type, - // file ID and line number) are appended to trace. + // to the collection agent. The frame's contents (frame type, file ID and line number) + // are appended to newTrace. Symbolize(symbolReporter reporter.SymbolReporter, frame *host.Frame, trace *libpf.Trace) error diff --git a/libpf/address.go b/libpf/address.go new file mode 100644 index 00000000..6e0fc0e2 --- /dev/null +++ b/libpf/address.go @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package libpf + +import "github.com/elastic/otel-profiling-agent/libpf/hash" + +// Address represents an address, or offset within a process +type Address uintptr + +// Hash32 returns a 32 bits hash of the input. +// It's main purpose is to be used as key for caching. +func (adr Address) Hash32() uint32 { + return uint32(adr.Hash()) +} + +// Hash returns a 64 bits hash of the input. +func (adr Address) Hash() uint64 { + return hash.Uint64(uint64(adr)) +} diff --git a/debug/log/doc.go b/libpf/apm.go similarity index 56% rename from debug/log/doc.go rename to libpf/apm.go index 6a33ca73..14f86930 100644 --- a/debug/log/doc.go +++ b/libpf/apm.go @@ -4,8 +4,10 @@ * See the file "LICENSE" for details. */ -/* -Package log is a drop-in wrapper around the logrus library. -It provides access to the same features, but also adds some debugging capabilities. -*/ -package log +package libpf + +type APMSpanID [8]byte +type APMTraceID [16]byte +type APMTransactionID = APMSpanID + +var InvalidAPMSpanID = APMSpanID{0, 0, 0, 0, 0, 0, 0, 0} diff --git a/libpf/basehash/hash128_test.go b/libpf/basehash/hash128_test.go index 82bab411..2d8e7f08 100644 --- a/libpf/basehash/hash128_test.go +++ b/libpf/basehash/hash128_test.go @@ -7,32 +7,33 @@ package basehash import ( - "errors" "fmt" + "math" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFromBytes(t *testing.T) { _, err := New128FromBytes(nil) - assert.Error(t, err) + require.Error(t, err) b := []byte{} _, err = New128FromBytes(b) - assert.Error(t, err) + require.Error(t, err) b = []byte{1} _, err = New128FromBytes(b) - assert.Error(t, err) + require.Error(t, err) b = []byte{0, 1, 2, 3, 4, 5, 6, 7} _, err = New128FromBytes(b) - assert.Error(t, err) + require.Error(t, err) b = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} hash, err := New128FromBytes(b) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, New128(0x01020304050607, 0x08090A0B0C0D0E0F), hash) } @@ -75,28 +76,41 @@ func TestIsZero(t *testing.T) { } func TestBytes(t *testing.T) { - hash := New128(0, 0) - assert.Equal(t, hash.Bytes(), []byte{ - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0}) - - hash = New128(0xDEC0DE, 0xC0FFEE) - assert.Equal(t, hash.Bytes(), []byte{ - 0, 0, 0, 0, 0, 0xDE, 0xC0, 0xDE, - 0, 0, 0, 0, 0, 0xC0, 0xFF, 0xEE}) - - hash = New128(0, 0xC0FFEE) - assert.Equal(t, hash.Bytes(), []byte{ - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0xC0, 0xFF, 0xEE}) - - maxUint64 := ^uint64(0) - hash = New128(maxUint64, maxUint64) - assert.Equal(t, hash.Bytes(), []byte{ - 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF}) + testCases := []struct { + name string + hash Hash128 + expected []byte + }{ + { + name: "Zero hash", + hash: New128(0, 0), + expected: make([]byte, 16), + }, + { + name: "Non-zero hash", + hash: New128(0xDEC0DE, 0xC0FFEE), + expected: []byte{0, 0, 0, 0, 0, 0xDE, 0xC0, 0xDE, 0, 0, 0, 0, 0, 0xC0, 0xFF, 0xEE}, + }, + { + name: "Non-zero low bits", + hash: New128(0, 0xC0FFEE), + expected: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xC0, 0xFF, 0xEE}, + }, + { + name: "Max uint64", + hash: New128(math.MaxUint64, math.MaxUint64), + expected: []byte{ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.hash.Bytes()) + }) + } } func TestPutBytes16(t *testing.T) { @@ -104,11 +118,14 @@ func TestPutBytes16(t *testing.T) { hash := New128(0x0011223344556677, 0x8899AABBCCDDEEFF) hash.PutBytes16(&b) - assert.Equal(t, hash.Bytes(), []byte{ + expected := []byte{ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, - 0xCC, 0xDD, 0xEE, 0xFF}) + 0xCC, 0xDD, 0xEE, 0xFF, + } + + assert.Equal(t, expected, hash.Bytes()) } func TestHash128Format(t *testing.T) { @@ -136,9 +153,7 @@ func TestHash128Format(t *testing.T) { test := test t.Run(name, func(t *testing.T) { output := fmt.Sprintf(test.formater, h) - if output != test.expected { - t.Fatalf("Expected '%s' but got '%s'", test.expected, output) - } + assert.Equal(t, test.expected, output) }) } } @@ -167,13 +182,8 @@ func TestNew128FromString(t *testing.T) { tc := tc t.Run(name, func(t *testing.T) { got, err := New128FromString(tc.stringRepresentation) - if !errors.Is(err, tc.err) { - t.Fatalf("Expected '%v' but got '%v'", tc.err, err) - } - if !got.Equal(tc.expected) { - t.Fatalf("Expected %v from '%s' but got %v", tc.expected, - tc.stringRepresentation, got) - } + require.ErrorIs(t, err, tc.err) + assert.Equal(t, tc.expected, got) }) } } diff --git a/libpf/basehash/hash64_test.go b/libpf/basehash/hash64_test.go index 167265ab..d8205b3c 100644 --- a/libpf/basehash/hash64_test.go +++ b/libpf/basehash/hash64_test.go @@ -9,31 +9,25 @@ package basehash import ( "fmt" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestBaseHash64(t *testing.T) { origHash := Hash64(5550100) - var err error - var data []byte // Test Sprintf marshaled := fmt.Sprintf("%x", origHash) expected := "54b014" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } + assert.Equal(t, expected, marshaled) // Test (Un)MarshalJSON - if data, err = origHash.MarshalJSON(); err != nil { - t.Fatalf("Failed to marshal baseHash64: %v", err) - } + data, err := origHash.MarshalJSON() + require.NoError(t, err) var newHash Hash64 - if err = newHash.UnmarshalJSON(data); err != nil { - t.Fatalf("Failed to unmarshal baseHash64: %v", err) - } - - if newHash != origHash { - t.Fatalf("New baseHash64 is different to original. Expected %v, got %v", origHash, newHash) - } + err = newHash.UnmarshalJSON(data) + require.NoError(t, err) + assert.Equal(t, origHash, newHash) } diff --git a/libpf/convenience.go b/libpf/convenience.go index a76d254f..5e4f505a 100644 --- a/libpf/convenience.go +++ b/libpf/convenience.go @@ -8,67 +8,16 @@ package libpf import ( "context" - "errors" "fmt" - "hash/fnv" - "io" "math/rand" "os" "reflect" - "strconv" - "strings" - "sync/atomic" "time" - "unicode" - "unicode/utf8" "unsafe" log "github.com/sirupsen/logrus" - - sha256 "github.com/minio/sha256-simd" ) -// HashString turns a string into a 64-bit hash. -func HashString(s string) uint64 { - h := fnv.New64a() - if _, err := h.Write([]byte(s)); err != nil { - log.Fatalf("Failed to write '%v' to hash: %v", s, err) - } - - return h.Sum64() -} - -// HashStrings turns a list of strings into a 128-bit hash. -func HashStrings(strs ...string) []byte { - h := fnv.New128a() - for _, s := range strs { - if _, err := h.Write([]byte(s)); err != nil { - log.Fatalf("Failed to write '%v' to hash: %v", s, err) - } - } - return h.Sum(nil) -} - -// HexToUint64 is a convenience function to extract a hex string to a uint64 and -// not worry about errors. Essentially a "mustConvertHexToUint64". -func HexToUint64(str string) uint64 { - v, err := strconv.ParseUint(str, 16, 64) - if err != nil { - log.Fatalf("Failure to hex-convert %s to uint64: %v", str, err) - } - return v -} - -// DecToUint64 is a convenience function to extract a decimal string to a uint64 -// and not worry about errors. Essentially a "mustConvertDecToUint64". -func DecToUint64(str string) uint64 { - v, err := strconv.ParseUint(str, 10, 64) - if err != nil { - log.Fatalf("Failure to dec-convert %s to uint64: %v", str, err) - } - return v -} - // WriteTempFile writes a data buffer to a temporary file on the filesystem. It // is the callers responsibility to clean up that file again. The function returns // the filename if successful. @@ -115,79 +64,6 @@ func AddJitter(baseDuration time.Duration, jitter float64) time.Duration { return time.Duration((1 + jitter - 2*jitter*rand.Float64()) * float64(baseDuration)) } -func ComputeFileSHA256(filePath string) (string, error) { - f, err := os.Open(filePath) - if err != nil { - return "", err - } - defer f.Close() - h := sha256.New() - if _, err = io.Copy(h, f); err != nil { - return "", err - } - return fmt.Sprintf("%x", h.Sum(nil)), nil -} - -// IsValidString checks if string is UTF-8-encoded and only contains expected characters. -func IsValidString(s string) bool { - if s == "" { - return false - } - if !utf8.ValidString(s) { - return false - } - for _, r := range s { - if !unicode.IsPrint(r) { - return false - } - } - return true -} - -// GetURLWithoutQueryParams returns an URL with all query parameters removed -// For example, http://hello.com/abc?a=1&b=2 becomes http://hello.com/abc -func GetURLWithoutQueryParams(url string) string { - return strings.Split(url, "?")[0] -} - -// NextPowerOfTwo returns the next highest power of 2 for a given value v or v, -// if v is a power of 2. -func NextPowerOfTwo(v uint32) uint32 { - v-- - v |= v >> 1 - v |= v >> 2 - v |= v >> 4 - v |= v >> 8 - v |= v >> 16 - v++ - return v -} - -// AtomicUpdateMaxUint32 updates the value in store using atomic memory primitives. newValue will -// only be placed in store if newValue is larger than the current value in store. -// To avoid inconsistency parallel updates to store should be avoided. -func AtomicUpdateMaxUint32(store *atomic.Uint32, newValue uint32) { - for { - // Load the current value - oldValue := store.Load() - if newValue <= oldValue { - // No update needed. - break - } - if store.CompareAndSwap(oldValue, newValue) { - // The value was atomically updated. - break - } - // The value changed between load and update attempt. - // Retry with the new value. - } -} - -// VersionUint returns a single integer composed of major, minor, patch. -func VersionUint(major, minor, patch uint32) uint32 { - return (major << 16) + (minor << 8) + patch -} - // SliceFrom converts a Go struct pointer or slice to []byte to read data into func SliceFrom(data any) []byte { var s []byte @@ -210,25 +86,3 @@ func SliceFrom(data any) []byte { } return s } - -// CheckError tries to match err with an error in the passed slice and returns -// true if a match is found. -func CheckError(err error, errList ...error) bool { - for _, e := range errList { - if errors.Is(err, e) { - return true - } - } - return false -} - -// CheckCanceled tries to match the first error with context canceled/deadline exceeded -// and returns it. If no match is found, the second error is returned. -func CheckCanceled(err1, err2 error) error { - if CheckError(err1, - context.Canceled, - context.DeadlineExceeded) { - return err1 - } - return err2 -} diff --git a/libpf/convenience_test.go b/libpf/convenience_test.go index 70d3a0be..cc433e9f 100644 --- a/libpf/convenience_test.go +++ b/libpf/convenience_test.go @@ -8,15 +8,11 @@ package libpf import ( "testing" -) -func TestMin(t *testing.T) { - a := 3 - b := 2 - if c := min(a, b); c != b { - t.Fatalf("Failed to return expected minimum.") - } -} + "github.com/elastic/otel-profiling-agent/util" + + "github.com/stretchr/testify/assert" +) func TestHexTo(t *testing.T) { tests := map[string]struct { @@ -31,9 +27,7 @@ func TestHexTo(t *testing.T) { name := name testcase := testcase t.Run(name, func(t *testing.T) { - if result := HexToUint64(name); result != testcase.result { - t.Fatalf("Unexpected return. Expected %d, got %d", testcase.result, result) - } + assert.Equal(t, testcase.result, util.HexToUint64(name)) }) } } @@ -51,9 +45,7 @@ func TestDecTo(t *testing.T) { name := name testcase := testcase t.Run(name, func(t *testing.T) { - if result := DecToUint64(name); result != testcase.result { - t.Fatalf("Unexpected return. Expected %d, got %d", testcase.result, result) - } + assert.Equal(t, testcase.result, util.DecToUint64(name)) }) } } @@ -83,9 +75,7 @@ func TestIsValidString(t *testing.T) { name := name testcase := testcase t.Run(name, func(t *testing.T) { - if testcase.expected != IsValidString(string(testcase.input)) { - t.Fatalf("Expected return %v for '%v'", testcase.expected, testcase.input) - } + assert.Equal(t, testcase.expected, util.IsValidString(string(testcase.input))) }) } } diff --git a/libpf/fileid.go b/libpf/fileid.go new file mode 100644 index 00000000..acc675f3 --- /dev/null +++ b/libpf/fileid.go @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package libpf + +import ( + "bytes" + "encoding" + "encoding/base64" + "encoding/binary" + "fmt" + "hash/fnv" + "io" + "math" + "os" + + "github.com/elastic/otel-profiling-agent/libpf/basehash" + sha256 "github.com/minio/sha256-simd" +) + +// FileID is used for unique identifiers for files +type FileID struct { + basehash.Hash128 +} + +// UnsymbolizedFileID is used as 128-bit FileID when symbolization fails. +var UnsymbolizedFileID = NewFileID(math.MaxUint64, math.MaxUint64) + +// UnknownKernelFileID is used as 128-bit FileID when the host agent isn't able to derive a FileID +// for a kernel frame. +var UnknownKernelFileID = NewFileID(math.MaxUint64-2, math.MaxUint64-2) + +func NewFileID(hi, lo uint64) FileID { + return FileID{basehash.New128(hi, lo)} +} + +// FileIDFromBytes parses a byte slice into the internal data representation for a file ID. +func FileIDFromBytes(b []byte) (FileID, error) { + // We need to check for nil since byte slice fields in protobuf messages can be optional. + // Until improved message validation and deserialization is added, this check will prevent + // panics. + if b == nil { + return FileID{}, nil + } + h, err := basehash.New128FromBytes(b) + if err != nil { + return FileID{}, err + } + return FileID{h}, nil +} + +// FileIDFromString parses a hexadecimal notation of a file ID into the internal data +// representation. +func FileIDFromString(s string) (FileID, error) { + hash128, err := basehash.New128FromString(s) + if err != nil { + return FileID{}, err + } + return FileID{hash128}, nil +} + +// FileIDFromBase64 converts a base64url encoded file ID into its binary representation. +// We store binary fields as keywords as base64 URL encoded strings. +// But when retrieving binary fields, ES sends them as base64 STD encoded strings. +func FileIDFromBase64(s string) (FileID, error) { + data, err := base64.RawURLEncoding.DecodeString(s) // allows - and _ in input + if err != nil { + // ES uses StdEncoding when marshaling binary fields + data, err = base64.RawStdEncoding.DecodeString(s) // allows + and / in input + if err != nil { + return FileID{}, fmt.Errorf("failed to decode to fileID %s: %v", s, err) + } + } + if len(data) != 16 { + return FileID{}, fmt.Errorf("unexpected input size (expected 16 bytes): %d", + len(data)) + } + return FileIDFromBytes(data) +} + +// Hash32 returns a 32 bits hash of the input. +// It's main purpose is to be used as key for caching. +func (f FileID) Hash32() uint32 { + return uint32(f.Hi()) +} + +func (f FileID) Equal(other FileID) bool { + return f.Hash128.Equal(other.Hash128) +} + +func (f FileID) Less(other FileID) bool { + return f.Hash128.Less(other.Hash128) +} + +// Compare returns an integer comparing two hashes lexicographically. +// The result will be 0 if f == other, -1 if f < other, and +1 if f > other. +func (f FileID) Compare(other FileID) int { + return f.Hash128.Compare(other.Hash128) +} + +// Swapped creates a new FileID with swapped high and low part. This function is its own inverse, +// so it can be used for the opposite operation. This is mostly used to connect Linux kernel +// module and its debug file build IDs. This provides 2 properties: +// - FileIDs must be different between kernel files and their debug files. +// - A kernel FileID (debug and non-debug) must only depend on its GNU BuildID (see +// FileIDFromKernelBuildID), and can always be computed in the Host Agent or during indexing +// without external information. +func (f FileID) Swapped() FileID { + // Reverse high and low. + return NewFileID(f.Lo(), f.Hi()) +} + +// Compile-time interface checks +var _ encoding.TextUnmarshaler = (*FileID)(nil) +var _ encoding.TextMarshaler = (*FileID)(nil) + +// FileIDFromExecutableReader hashes portions of the contents of the reader in order to +// generate a system-independent identifier. The file is expected to be an executable +// file (ELF or PE) where the header and footer has enough data to make the file unique. +// +// *** WARNING *** +// ANY CHANGE IN BEHAVIOR CAN EASILY BREAK OUR INFRASTRUCTURE, POSSIBLY MAKING THE ENTIRETY +// OF THE DEBUG INDEX OR FRAME METADATA WORTHLESS (BREAKING BACKWARDS COMPATIBILITY). +func FileIDFromExecutableReader(reader io.ReadSeeker) (FileID, error) { + h := sha256.New() + + // Hash algorithm: SHA256 of the following: + // 1) 4 KiB header: + // ELF: should cover the program headers, and usually the GNU Build ID (if present) + // plus other sections. + // PE/dotnet: section headers, and typically also the build GUID. + // 2) 4 KiB trailer: ELF: in practice, should cover the ELF section headers, as well as the + // contents of the debug link and other sections. + // 3) File length (8 bytes, big-endian). Just for paranoia: ELF files can be appended to + // without restrictions, so it feels a bit too easy to produce valid ELF files that would + // produce identical hashes using only 1) and 2). + + // 1) Hash header + _, err := io.Copy(h, io.LimitReader(reader, 4096)) + if err != nil { + return FileID{}, fmt.Errorf("failed to hash file header: %v", err) + } + + var size int64 + size, err = reader.Seek(0, io.SeekEnd) + if err != nil { + return FileID{}, fmt.Errorf("failed to seek end of file: %v", err) + } + + // 2) Hash trailer + // This will double-hash some data if the file is < 8192 bytes large. Oh well - better keep + // it simple since the logic is customer-facing. + tailBytes := min(size, 4096) + _, err = reader.Seek(-tailBytes, io.SeekEnd) + if err != nil { + return FileID{}, fmt.Errorf("failed to seek file trailer: %v", err) + } + + _, err = io.Copy(h, reader) + if err != nil { + return FileID{}, fmt.Errorf("failed to hash file trailer: %v", err) + } + + // 3) Hash length + lengthArray := make([]byte, 8) + binary.BigEndian.PutUint64(lengthArray, uint64(size)) + _, err = io.Copy(h, bytes.NewReader(lengthArray)) + if err != nil { + return FileID{}, fmt.Errorf("failed to hash file length: %v", err) + } + + return FileIDFromBytes(h.Sum(nil)[0:16]) +} + +// FileIDFromExecutableFile opens an executable file and calculates the FileID for it. +// The caller is responsible pre-validate it as an executable file and that the algorithm +// described in FileIDFromExecutableReader is suitable. +func FileIDFromExecutableFile(fileName string) (FileID, error) { + f, err := os.Open(fileName) + if err != nil { + return FileID{}, err + } + defer f.Close() + + return FileIDFromExecutableReader(f) +} + +// FileIDFromKernelBuildID returns the FileID of a kernel image or module, which consists +// of a hash of its GNU BuildID in hex string form. +// The hashing step is to ensure that the FileID remains an opaque concept to the end user. +func FileIDFromKernelBuildID(buildID string) FileID { + h := fnv.New128a() + _, _ = h.Write([]byte(buildID)) + // Cannot fail, ignore error. + fileID, _ := FileIDFromBytes(h.Sum(nil)) + return fileID +} diff --git a/libpf/fileid_test.go b/libpf/fileid_test.go new file mode 100644 index 00000000..9349d5ea --- /dev/null +++ b/libpf/fileid_test.go @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package libpf + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFileIDSprintf(t *testing.T) { + origID, err := FileIDFromString("600DCAFE4A110000F2BF38C493F5FB92") + require.NoError(t, err) + + marshaled := fmt.Sprintf("%d", origID) + // nolint:goconst + expected := "{6921411395851452416 17491761894677412754}" + assert.Equal(t, expected, marshaled) + + marshaled = fmt.Sprintf("%s", origID) + expected = "{%!s(uint64=6921411395851452416) %!s(uint64=17491761894677412754)}" + assert.Equal(t, expected, marshaled) + + marshaled = fmt.Sprintf("%v", origID) + expected = "{6921411395851452416 17491761894677412754}" + assert.Equal(t, expected, marshaled) + + marshaled = fmt.Sprintf("%#v", origID) + expected = "0x600dcafe4a110000f2bf38c493f5fb92" + assert.Equal(t, expected, marshaled) + + fileID := NewFileID(5705163814651576546, 12305932466601883523) + + marshaled = fmt.Sprintf("%x", fileID) + expected = "4f2cd0431db840e2aac77460f5c07783" + assert.Equal(t, expected, marshaled) + + marshaled = fmt.Sprintf("%X", fileID) + expected = "4F2CD0431DB840E2AAC77460F5C07783" + assert.Equal(t, expected, marshaled) + + marshaled = fmt.Sprintf("%#x", fileID) + expected = "0x4f2cd0431db840e2aac77460f5c07783" + assert.Equal(t, expected, marshaled) + + marshaled = fmt.Sprintf("%#X", fileID) + expected = "0x4F2CD0431DB840E2AAC77460F5C07783" + assert.Equal(t, expected, marshaled) +} + +func TestFileIDMarshal(t *testing.T) { + origID, err := FileIDFromString("600DCAFE4A110000F2BF38C493F5FB92") + require.NoError(t, err) + + // Test (Un)MarshalJSON + var data []byte + data, err = origID.MarshalJSON() + require.NoError(t, err) + + marshaled := string(data) + expected := "\"600dcafe4a110000f2bf38c493f5fb92\"" + assert.Equal(t, expected, marshaled) + + var jsonID FileID + err = jsonID.UnmarshalJSON(data) + require.NoError(t, err) + assert.Equal(t, jsonID, origID) + + // Test (Un)MarshalText + data, err = origID.MarshalText() + require.NoError(t, err) + + marshaled = string(data) + expected = "600dcafe4a110000f2bf38c493f5fb92" + assert.Equal(t, expected, marshaled) + + var textID FileID + err = textID.UnmarshalText(data) + require.NoError(t, err) + assert.Equal(t, origID, textID) +} + +func TestInvalidFileIDs(t *testing.T) { + // 15 characters + _, err := FileIDFromString("600DCAFE4A11000") + require.Error(t, err) + + // Non-hex characters + _, err = FileIDFromString("600DCAFE4A11000G") + require.Error(t, err) +} + +func TestFileIDFromBase64(t *testing.T) { + expected := NewFileID(0x12345678124397ff, 0x87654321877484a8) + fileIDURLEncoded := "EjRWeBJDl_-HZUMhh3SEqA" + fileIDStdEncoded := "EjRWeBJDl/+HZUMhh3SEqA" + + actual, err := FileIDFromBase64(fileIDURLEncoded) + require.NoError(t, err) + assert.Equal(t, expected, actual) + + actual, err = FileIDFromBase64(fileIDStdEncoded) + require.NoError(t, err) + assert.Equal(t, expected, actual) +} + +func TestFileIDBase64(t *testing.T) { + expected := "EjRWeBJDl_WHZUMhh3SEng" + fileID := NewFileID(0x12345678124397f5, 0x876543218774849e) + + assert.Equal(t, expected, fileID.Base64()) +} + +func TestFileIDFromExecutableReader(t *testing.T) { + tests := map[string]struct { + data []byte + id FileID + }{ + "ELF file": { + data: []byte{0x7F, 'E', 'L', 'F', 0x00, 0x01, 0x2, 0x3, 0x4}, + id: NewFileID(0xcaf6e5907166ac76, 0xeef618e5f7f59cd9), + }, + } + + for name, testcase := range tests { + name := name + testcase := testcase + t.Run(name, func(t *testing.T) { + fileID, err := FileIDFromExecutableReader(bytes.NewReader(testcase.data)) + require.NoError(t, err, "Failed to calculate executable ID") + assert.Equal(t, fileID, testcase.id) + }) + } +} + +func TestFileIDFromKernelBuildID(t *testing.T) { + buildID := "f8e1cf0f60558098edaec164ac7749df" + fileID := FileIDFromKernelBuildID(buildID) + expectedFileID, _ := FileIDFromString("026a2d6a60ee6b4eb8ec85adf2e76f4d") + assert.Equal(t, expectedFileID, fileID) +} + +func TestFileIDSwapped(t *testing.T) { + fileID, _ := FileIDFromString("026a2d6a60ee6b4eb8ec85adf2e76f4d") + toggled := fileID.Swapped() + expectedFileID, _ := FileIDFromString("b8ec85adf2e76f4d026a2d6a60ee6b4e") + assert.Equal(t, expectedFileID, toggled) +} diff --git a/libpf/frameid.go b/libpf/frameid.go index 2b36c387..ce2d3dc4 100644 --- a/libpf/frameid.go +++ b/libpf/frameid.go @@ -33,7 +33,7 @@ func NewFrameID(fileID FileID, addressOrLineno AddressOrLineno) FrameID { } } -// NewFrameIDFromString creates a new FrameID from its JSON string representation. +// NewFrameIDFromString creates a new FrameID from its base64 string representation. func NewFrameIDFromString(frameEncoded string) (FrameID, error) { var frameID FrameID diff --git a/libpf/frametype.go b/libpf/frametype.go new file mode 100644 index 00000000..56ba8927 --- /dev/null +++ b/libpf/frametype.go @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package libpf + +import ( + "fmt" + + "github.com/elastic/otel-profiling-agent/support" +) + +// FrameType defines the type of frame. This usually corresponds to the interpreter type that +// emitted it, but can additionally contain meta-information like error frames. +// +// A frame type can represent one of the following things: +// +// - A successfully unwound frame. This is represented simply as the `InterpreterType` ID. +// - A partial (non-critical failure), indicated by ORing the `InterpreterType` ID with +// the error bit. +// - A fatal failure that caused further unwinding to be aborted. This is indicated using the +// special value support.FrameMarkerAbort (0xFF). It thus also contains the error bit, but +// does not fit into the `InterpreterType` enum. +type FrameType int + +// Convenience shorthands to create various frame types. +// +// Code should not compare against the constants below directly, but instead use the provided +// methods to query the required information (IsError, Interpreter, ...) to improve forward +// compatibility and clarify intentions. +const ( + // unknownFrame indicates a frame of an unknown interpreter. + // If this appears, it's likely a bug somewhere. + unknownFrame FrameType = support.FrameMarkerUnknown + // PHPFrame identifies PHP interpreter frames. + PHPFrame FrameType = support.FrameMarkerPHP + // PHPJITFrame identifies PHP JIT interpreter frames. + PHPJITFrame FrameType = support.FrameMarkerPHPJIT + // PythonFrame identifies the Python interpreter frames. + PythonFrame FrameType = support.FrameMarkerPython + // NativeFrame identifies native frames. + NativeFrame FrameType = support.FrameMarkerNative + // KernelFrame identifies kernel frames. + KernelFrame FrameType = support.FrameMarkerKernel + // HotSpotFrame identifies Java HotSpot VM frames. + HotSpotFrame FrameType = support.FrameMarkerHotSpot + // RubyFrame identifies the Ruby interpreter frames. + RubyFrame FrameType = support.FrameMarkerRuby + // PerlFrame identifies the Perl interpreter frames. + PerlFrame FrameType = support.FrameMarkerPerl + // V8Frame identifies the V8 interpreter frames. + V8Frame FrameType = support.FrameMarkerV8 + // DotnetFrame identifies the Dotnet interpreter frames. + DotnetFrame FrameType = support.FrameMarkerDotnet + // AbortFrame identifies frames that report that further unwinding was aborted due to an error. + AbortFrame FrameType = support.FrameMarkerAbort +) + +const ( + abortFrameName = "abort-marker" +) + +func FrameTypeFromString(name string) FrameType { + if name == abortFrameName { + return AbortFrame + } + return InterpreterTypeFromString(name).Frame() +} + +// Interpreter returns the interpreter that produced the frame. +func (ty FrameType) Interpreter() InterpreterType { + switch ty { + case support.FrameMarkerAbort, support.FrameMarkerUnknown: + return UnknownInterp + default: + return InterpreterType(ty &^ support.FrameMarkerErrorBit) + } +} + +// IsInterpType checks whether the frame type belongs to the given interpreter. +func (ty FrameType) IsInterpType(ity InterpreterType) bool { + return ity == ty.Interpreter() +} + +// Error adds the error bit into the frame type. +func (ty FrameType) Error() FrameType { + return ty | support.FrameMarkerErrorBit +} + +// IsError checks whether the frame is an error frame. +func (ty FrameType) IsError() bool { + return ty&support.FrameMarkerErrorBit != 0 +} + +// String implements the Stringer interface. +func (ty FrameType) String() string { + switch ty { + case support.FrameMarkerAbort: + return abortFrameName + default: + interp := ty.Interpreter() + if ty.IsError() { + return fmt.Sprintf("%s-error", interp) + } + return interp.String() + } +} diff --git a/libpf/frametype_test.go b/libpf/frametype_test.go new file mode 100644 index 00000000..c9c48fe4 --- /dev/null +++ b/libpf/frametype_test.go @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package libpf + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFrameTypeFromString(t *testing.T) { + // Simple check whether all FrameType values can be converted to string and back. + for _, ft := range []FrameType{ + unknownFrame, PHPFrame, PythonFrame, NativeFrame, KernelFrame, HotSpotFrame, RubyFrame, + PerlFrame, V8Frame, DotnetFrame, AbortFrame} { + name := ft.String() + result := FrameTypeFromString(name) + require.Equal(t, ft, result) + } +} diff --git a/libpf/freelru/lru.go b/libpf/freelru/lru.go deleted file mode 100644 index 1356a219..00000000 --- a/libpf/freelru/lru.go +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -// Package freelru is a wrapper around go-freelru.LRU with additional statistics embedded and can -// be used as a drop in replacement. -package freelru - -import ( - "sync/atomic" - - lru "github.com/elastic/go-freelru" -) - -// LRU is a wrapper around go-freelru.LRU with additional statistics embedded. -type LRU[K comparable, V any] struct { - lru lru.LRU[K, V] - - // Internal statistics - hit atomic.Uint64 - miss atomic.Uint64 - added atomic.Uint64 - deleted atomic.Uint64 -} - -type Statistics struct { - // Number of times for a hit of a cache entry. - Hit uint64 - // Number of times for a miss of a cache entry. - Miss uint64 - // Number of elements that were added to the cache. - Added uint64 - // Number of elements that were deleted from the cache. - Deleted uint64 -} - -func New[K comparable, V any](capacity uint32, hash lru.HashKeyCallback[K]) (*LRU[K, V], error) { - cache, err := lru.New[K, V](capacity, hash) - if err != nil { - return nil, err - } - return &LRU[K, V]{ - lru: *cache, - }, nil -} - -func (c *LRU[K, V]) Add(key K, value V) (evicted bool) { - evicted = c.lru.Add(key, value) - if evicted { - c.deleted.Add(1) - } - c.added.Add(1) - return evicted -} - -func (c *LRU[K, V]) Contains(key K) (ok bool) { - return c.lru.Contains(key) -} - -func (c *LRU[K, V]) Get(key K) (value V, ok bool) { - value, ok = c.lru.Get(key) - if ok { - c.hit.Add(1) - } else { - c.miss.Add(1) - } - return value, ok -} - -func (c *LRU[K, V]) Purge() { - size := c.lru.Len() - c.deleted.Add(uint64(size)) - c.lru.Purge() -} - -func (c *LRU[K, V]) Remove(key K) (present bool) { - present = c.lru.Remove(key) - if present { - c.deleted.Add(1) - } - return present -} - -// GetAndResetStatistics returns the internal statistics for this LRU and resets all values to 0. -func (c *LRU[K, V]) GetAndResetStatistics() Statistics { - return Statistics{ - Hit: c.hit.Swap(0), - Miss: c.miss.Swap(0), - Added: c.added.Swap(0), - Deleted: c.deleted.Swap(0), - } -} diff --git a/libpf/hash/hash_test.go b/libpf/hash/hash_test.go index ceff9c4f..05d4cd88 100644 --- a/libpf/hash/hash_test.go +++ b/libpf/hash/hash_test.go @@ -9,6 +9,8 @@ package hash import ( "math" "testing" + + "github.com/stretchr/testify/assert" ) func TestUint64(t *testing.T) { @@ -28,9 +30,7 @@ func TestUint64(t *testing.T) { testcase := testcase t.Run(name, func(t *testing.T) { result := Uint64(testcase.input) - if result != testcase.expect { - t.Fatalf("Unexpected hash. Expected %d, got %d", testcase.expect, result) - } + assert.Equal(t, testcase.expect, result) }) } } @@ -51,9 +51,7 @@ func TestUint32(t *testing.T) { testcase := testcase t.Run(name, func(t *testing.T) { result := Uint32(testcase.input) - if result != testcase.expect { - t.Fatalf("Unexpected hash. Expected %d, got %d", testcase.expect, result) - } + assert.Equal(t, testcase.expect, result) }) } } diff --git a/libpf/interpretertype.go b/libpf/interpretertype.go new file mode 100644 index 00000000..b3eafdfe --- /dev/null +++ b/libpf/interpretertype.go @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package libpf + +import "github.com/elastic/otel-profiling-agent/support" + +// InterpreterType variables can hold one of the interpreter type values defined below. +type InterpreterType int + +const ( + // UnknownInterp signifies that the interpreter is unknown. + UnknownInterp InterpreterType = support.FrameMarkerUnknown + // PHP identifies the PHP interpreter. + PHP InterpreterType = support.FrameMarkerPHP + // PHPJIT identifies PHP JIT processes. + PHPJIT InterpreterType = support.FrameMarkerPHPJIT + // Python identifies the Python interpreter. + Python InterpreterType = support.FrameMarkerPython + // Native identifies native code. + Native InterpreterType = support.FrameMarkerNative + // Kernel identifies kernel code. + Kernel InterpreterType = support.FrameMarkerKernel + // HotSpot identifies the Java HotSpot VM. + HotSpot InterpreterType = support.FrameMarkerHotSpot + // Ruby identifies the Ruby interpreter. + Ruby InterpreterType = support.FrameMarkerRuby + // Perl identifies the Perl interpreter. + Perl InterpreterType = support.FrameMarkerPerl + // V8 identifies the V8 interpreter. + V8 InterpreterType = support.FrameMarkerV8 + // Dotnet identifies the Dotnet interpreter. + Dotnet InterpreterType = support.FrameMarkerDotnet +) + +// Pseudo-interpreters without a corresponding frame type. +const ( + // pseudoInterpreterStart marks the start of the pseudo interpreter ID space. + pseudoInterpreterStart InterpreterType = 0x100 + + // APMInt identifies the pseudo-interpreter for the APM integration. + APMInt InterpreterType = 0x100 +) + +// Frame converts the interpreter type into the corresponding frame type. +func (i InterpreterType) Frame() FrameType { + if i >= pseudoInterpreterStart { + return unknownFrame + } + + return FrameType(i) +} + +var interpreterTypeToString = map[InterpreterType]string{ + UnknownInterp: "unknown", + PHP: "php", + PHPJIT: "phpjit", + Python: "python", + Native: "native", + Kernel: "kernel", + HotSpot: "jvm", + Ruby: "ruby", + Perl: "perl", + V8: "v8", + Dotnet: "dotnet", + APMInt: "apm-integration", +} + +var stringToInterpreterType = make(map[string]InterpreterType, len(interpreterTypeToString)) + +func init() { + for k, v := range interpreterTypeToString { + stringToInterpreterType[v] = k + } +} + +func InterpreterTypeFromString(name string) InterpreterType { + if result, ok := stringToInterpreterType[name]; ok { + return result + } + return UnknownInterp +} + +// String converts the frame type int to the related string value to be displayed in the UI. +func (i InterpreterType) String() string { + if result, ok := interpreterTypeToString[i]; ok { + return result + } + // nolint:goconst + return "" +} diff --git a/libpf/libpf.go b/libpf/libpf.go index ffe36602..e5b20e79 100644 --- a/libpf/libpf.go +++ b/libpf/libpf.go @@ -7,22 +7,12 @@ package libpf import ( - "encoding" - "encoding/base64" "encoding/json" "fmt" - "hash/crc32" - "io" "math" - "os" "time" - _ "unsafe" // required to use //go:linkname for runtime.nanotime - "golang.org/x/sys/unix" - - "github.com/elastic/otel-profiling-agent/libpf/basehash" - "github.com/elastic/otel-profiling-agent/libpf/hash" - "github.com/elastic/otel-profiling-agent/support" + "github.com/elastic/otel-profiling-agent/util" ) // UnixTime32 is another type to represent seconds since epoch. @@ -35,8 +25,8 @@ import ( // To restore some semblance of type safety, we declare a type alias here. type UnixTime32 uint32 -func (t *UnixTime32) MarshalJSON() ([]byte, error) { - return time.Unix(int64(*t), 0).UTC().MarshalJSON() +func (t UnixTime32) MarshalJSON() ([]byte, error) { + return time.Unix(int64(t), 0).UTC().MarshalJSON() } // Compile-time interface checks @@ -47,535 +37,59 @@ func NowAsUInt32() uint32 { return uint32(time.Now().Unix()) } -// PID represent Unix Process ID (pid_t) -type PID int32 - -func (p PID) Hash32() uint32 { - return uint32(p) -} - -// FileID is used for unique identifiers for files -type FileID struct { - basehash.Hash128 -} - -// UnsymbolizedFileID is used as 128-bit FileID when symbolization fails. -var UnsymbolizedFileID = NewFileID(math.MaxUint64, math.MaxUint64) - -// UnknownKernelFileID is used as 128-bit FileID when the host agent isn't able to derive a FileID -// for a kernel frame. -var UnknownKernelFileID = NewFileID(math.MaxUint64-2, math.MaxUint64-2) - -func NewFileID(hi, lo uint64) FileID { - return FileID{basehash.New128(hi, lo)} -} - -// FileIDFromBytes parses a byte slice into the internal data representation for a file ID. -func FileIDFromBytes(b []byte) (FileID, error) { - // We need to check for nil since byte slice fields in protobuf messages can be optional. - // Until improved message validation and deserialization is added, this check will prevent - // panics. - if b == nil { - return FileID{}, nil - } - h, err := basehash.New128FromBytes(b) - if err != nil { - return FileID{}, err - } - return FileID{h}, nil -} - -// FileIDFromString parses a hexadecimal notation of a file ID into the internal data -// representation. -func FileIDFromString(s string) (FileID, error) { - hash128, err := basehash.New128FromString(s) - if err != nil { - return FileID{}, err - } - return FileID{hash128}, nil -} - -// FileIDFromBase64 converts a base64url encoded file ID into its binary representation. -// We store binary fields as keywords as base64 URL encoded strings. -// But when retrieving binary fields, ES sends them as base64 STD encoded strings. -func FileIDFromBase64(s string) (FileID, error) { - bytes, err := base64.RawURLEncoding.DecodeString(s) // allows - and _ in input - if err != nil { - // ES uses StdEncoding when marshaling binary fields - bytes, err = base64.RawStdEncoding.DecodeString(s) // allows + and / in input - if err != nil { - return FileID{}, fmt.Errorf("failed to decode to fileID %s: %v", s, err) - } - } - if len(bytes) != 16 { - return FileID{}, fmt.Errorf("unexpected input size (expected 16 exeBytes): %d", - len(bytes)) - } - - return FileIDFromBytes(bytes) -} - -// Hash32 returns a 32 bits hash of the input. -// It's main purpose is to be used as key for caching. -func (f FileID) Hash32() uint32 { - return uint32(f.Hi()) -} - -func (f FileID) Equal(other FileID) bool { - return f.Hash128.Equal(other.Hash128) -} - -func (f FileID) Less(other FileID) bool { - return f.Hash128.Less(other.Hash128) -} - -// Compare returns an integer comparing two hashes lexicographically. -// The result will be 0 if f == other, -1 if f < other, and +1 if f > other. -func (f FileID) Compare(other FileID) int { - return f.Hash128.Compare(other.Hash128) -} - -// Compile-time interface checks -var _ encoding.TextUnmarshaler = (*FileID)(nil) -var _ encoding.TextMarshaler = (*FileID)(nil) - -// PackageID is used for unique identifiers for packages -type PackageID struct { - basehash.Hash128 -} - -// PackageIDFromBytes parses a byte slice into the internal data representation for a PackageID. -func PackageIDFromBytes(b []byte) (PackageID, error) { - h, err := basehash.New128FromBytes(b) - if err != nil { - return PackageID{}, err - } - return PackageID{h}, nil -} - -// Equal returns true if both PackageIDs are equal. -func (h PackageID) Equal(other PackageID) bool { - return h.Hash128.Equal(other.Hash128) -} - -// String returns the string representation for the package ID. -func (h PackageID) String() string { - return h.StringNoQuotes() -} +// UnixTime64 represents nanoseconds or (reduced precision) seconds since epoch. +type UnixTime64 uint64 -// PackageIDFromString returns a PackageID from its string representation. -func PackageIDFromString(str string) (PackageID, error) { - hash128, err := basehash.New128FromString(str) - if err != nil { - return PackageID{}, err +func (t UnixTime64) MarshalJSON() ([]byte, error) { + if t > math.MaxUint32 { + // Nanoseconds, ES does not support 'epoch_nanoseconds' so + // we have to pass it a value formatted as 'strict_date_optional_time_nanos'. + out := []byte(fmt.Sprintf("%q", + time.Unix(0, int64(t)).UTC().Format(time.RFC3339Nano))) + return out, nil } - return PackageID{hash128}, nil -} - -// TraceHash represents the unique hash of a trace -type TraceHash struct { - basehash.Hash128 -} - -func NewTraceHash(hi, lo uint64) TraceHash { - return TraceHash{basehash.New128(hi, lo)} -} -// TraceHashFromBytes parses a byte slice of a trace hash into the internal data representation. -func TraceHashFromBytes(b []byte) (TraceHash, error) { - h, err := basehash.New128FromBytes(b) - if err != nil { - return TraceHash{}, err - } - return TraceHash{h}, nil + // Reduced precision seconds-since-the-epoch, ES 'epoch_second' formatter will match these. + out := []byte(fmt.Sprintf("%d", t)) + return out, nil } -// TraceHashFromString parses a hexadecimal notation of a trace hash into the internal data -// representation. -func TraceHashFromString(s string) (TraceHash, error) { - hash128, err := basehash.New128FromString(s) - if err != nil { - return TraceHash{}, err +// Unix returns the value as seconds since epoch. +func (t UnixTime64) Unix() int64 { + if t > math.MaxUint32 { + // Nanoseconds, convert to seconds-since-the-epoch + return time.Unix(0, int64(t)).Unix() } - return TraceHash{hash128}, nil -} - -func (h TraceHash) Equal(other TraceHash) bool { - return h.Hash128.Equal(other.Hash128) -} - -func (h TraceHash) Less(other TraceHash) bool { - return h.Hash128.Less(other.Hash128) -} - -// EncodeTo encodes the hash into the base64 encoded representation -// and stores it in the provided destination byte array. -// The length of the destination must be at least EncodedLen(). -func (h TraceHash) EncodeTo(dst []byte) { - base64.RawURLEncoding.Encode(dst, h.Bytes()) -} -// EncodedLen returns the length of the hash's base64 representation. -func (TraceHash) EncodedLen() int { - // TraceHash is 16 bytes long, the base64 representation is one base64 byte per 6 bits. - return ((16)*8)/6 + 1 -} - -// Hash32 returns a 32 bits hash of the input. -// It's main purpose is to be used for LRU caching. -func (h TraceHash) Hash32() uint32 { - return uint32(h.Lo()) + return int64(t) } // Compile-time interface checks -var _ encoding.TextUnmarshaler = (*TraceHash)(nil) -var _ encoding.TextMarshaler = (*TraceHash)(nil) +var _ json.Marshaler = (*UnixTime64)(nil) // AddressOrLineno represents a line number in an interpreted file or an offset into -// a native file. TODO(thomasdullien): check with regards to JSON marshaling/demarshaling. +// a native file. type AddressOrLineno uint64 -// Address represents an address, or offset within a process -type Address uint64 - -// Hash32 returns a 32 bits hash of the input. -// It's main purpose is to be used as key for caching. -func (adr Address) Hash32() uint32 { - return uint32(adr.Hash()) -} - -func (adr Address) Hash() uint64 { - return hash.Uint64(uint64(adr)) -} - -// InterpVersion represents the version of an interpreter -type InterpVersion string - -// SourceLineno represents a line number within a source file. It is intended to be used for the -// source line numbers associated with offsets in native code, or for source line numbers in -// interpreted code. -type SourceLineno uint64 - -// InterpType variables can hold one of the interpreter type values defined below. -type InterpType int - -const ( - // UnknownInterp signifies that the interpreter is unknown. - UnknownInterp InterpType = support.FrameMarkerUnknown - // PHP identifies the PHP interpreter. - PHP InterpType = support.FrameMarkerPHP - // PHPJIT identifies PHP JIT processes. - PHPJIT InterpType = support.FrameMarkerPHPJIT - // Python identifies the Python interpreter. - Python InterpType = support.FrameMarkerPython - // Native identifies native code. - Native InterpType = support.FrameMarkerNative - // Kernel identifies kernel code. - Kernel InterpType = support.FrameMarkerKernel - // HotSpot identifies the Java HotSpot VM. - HotSpot InterpType = support.FrameMarkerHotSpot - // Ruby identifies the Ruby interpreter. - Ruby InterpType = support.FrameMarkerRuby - // Perl identifies the Perl interpreter. - Perl InterpType = support.FrameMarkerPerl - // V8 identifies the V8 interpreter. - V8 InterpType = support.FrameMarkerV8 -) - -// Frame converts the interpreter type into the corresponding frame type. -func (i InterpType) Frame() FrameType { - return FrameType(i) -} - -var interpTypeToString = map[InterpType]string{ - UnknownInterp: "unknown", - PHP: "php", - PHPJIT: "phpjit", - Python: "python", - Native: "native", - Kernel: "kernel", - HotSpot: "jvm", - Ruby: "ruby", - Perl: "perl", - V8: "v8", -} - -// String converts the frame type int to the related string value to be displayed in the UI. -func (i InterpType) String() string { - if result, ok := interpTypeToString[i]; ok { - return result - } - // nolint:goconst - return "" -} - -// FrameType defines the type of frame. This usually corresponds to the interpreter type that -// emitted it, but can additionally contain meta-information like error frames. -// -// A frame type can represent one of the following things: -// -// - A successfully unwound frame. This is represented simply as the `InterpType` ID. -// - A partial (non-critical failure), indicated by ORing the `InterpType` ID with the error bit. -// - A fatal failure that caused further unwinding to be aborted. This is indicated using the -// special value support.FrameMarkerAbort (0xFF). It thus also contains the error bit, but -// does not fit into the `InterpType` enum. -type FrameType int - -// Convenience shorthands to create various frame types. -// -// Code should not compare against the constants below directly, but instead use the provided -// methods to query the required information (IsError, Interpreter, ...) to improve forward -// compatibility and clarify intentions. -const ( - // UnknownFrame indicates a frame of an unknown interpreter. - // If this appears, it's likely a bug somewhere. - UnknownFrame FrameType = support.FrameMarkerUnknown - // PHPFrame identifies PHP interpreter frames. - PHPFrame FrameType = support.FrameMarkerPHP - // PHPJITFrame identifies PHP JIT interpreter frames. - PHPJITFrame FrameType = support.FrameMarkerPHPJIT - // PythonFrame identifies the Python interpreter frames. - PythonFrame FrameType = support.FrameMarkerPython - // NativeFrame identifies native frames. - NativeFrame FrameType = support.FrameMarkerNative - // KernelFrame identifies kernel frames. - KernelFrame FrameType = support.FrameMarkerKernel - // HotSpotFrame identifies Java HotSpot VM frames. - HotSpotFrame FrameType = support.FrameMarkerHotSpot - // RubyFrame identifies the Ruby interpreter frames. - RubyFrame FrameType = support.FrameMarkerRuby - // PerlFrame identifies the Perl interpreter frames. - PerlFrame FrameType = support.FrameMarkerPerl - // V8Frame identifies the V8 interpreter frames. - V8Frame FrameType = support.FrameMarkerV8 - // AbortFrame identifies frames that report that further unwinding was aborted due to an error. - AbortFrame FrameType = support.FrameMarkerAbort -) - -// Interpreter returns the interpreter that produced the frame. -func (ty FrameType) Interpreter() InterpType { - switch ty { - case support.FrameMarkerAbort, support.FrameMarkerUnknown: - return UnknownInterp - default: - return InterpType(ty &^ support.FrameMarkerErrorBit) - } -} - -// IsInterpType checks whether the frame type belongs to the given interpreter. -func (ty FrameType) IsInterpType(ity InterpType) bool { - return ity == ty.Interpreter() -} - -// Error adds the error bit into the frame type. -func (ty FrameType) Error() FrameType { - return ty | support.FrameMarkerErrorBit -} - -// IsError checks whether the frame is an error frame. -func (ty FrameType) IsError() bool { - return ty&support.FrameMarkerErrorBit != 0 -} - -// String implements the Stringer interface. -func (ty FrameType) String() string { - switch ty { - case support.FrameMarkerAbort: - return "abort-marker" - default: - interp := ty.Interpreter() - if ty.IsError() { - return fmt.Sprintf("%s-error", interp) - } - return interp.String() - } -} - -// The different types of packages that we process -type PackageType int32 - -func (t PackageType) String() string { - if res, ok := packageTypeToString[t]; ok { - return res - } - // nolint:goconst - return "" -} - -const ( - PackageTypeDeb = iota - PackageTypeRPM - PackageTypeCustomSymbols - PackageTypeAPK -) - -var packageTypeToString = map[PackageType]string{ - PackageTypeDeb: "deb", - PackageTypeRPM: "rpm", - PackageTypeCustomSymbols: "custom", - PackageTypeAPK: "apk", -} - -// The different types of source package objects that we process -type SourcePackageType int32 - -const ( - SourcePackageTypeDeb = iota - SourcePackageTypeRPM -) - -const ( - CodeIndexingPackageTypeDeb = "deb" - CodeIndexingPackageTypeRpm = "rpm" - CodeIndexingPackageTypeCustom = "custom" - CodeIndexingPackageTypeApk = "apk" -) - -type CodeIndexingMessage struct { - SourcePackageName string `json:"sourcePackageName"` - SourcePackageVersion string `json:"sourcePackageVersion"` - MirrorName string `json:"mirrorName"` - ForceRetry bool `json:"forceRetry"` -} - -// LocalFSPackageID is a fake package identifier, indicating that a particular file was not part of -// a package, but was extracted directly from a local filesystem. -var LocalFSPackageID = PackageID{ - basehash.New128(0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF), -} - -// The different types of packages that we process -type FileType int32 - -const ( - FileTypeNative = iota - FileTypePython -) - -// Trace represents a stack trace. Each tuple (Files[i], Linenos[i]) represents a -// stack frame via the file ID and line number at the offset i in the trace. The -// information for the most recently called function is at offset 0. -type Trace struct { - Files []FileID - Linenos []AddressOrLineno - FrameTypes []FrameType - Hash TraceHash -} - -// AppendFrame appends a frame to the columnar frame array. -func (trace *Trace) AppendFrame(ty FrameType, file FileID, addrOrLine AddressOrLineno) { - trace.FrameTypes = append(trace.FrameTypes, ty) - trace.Files = append(trace.Files, file) - trace.Linenos = append(trace.Linenos, addrOrLine) -} - type TraceAndCounts struct { - Hash TraceHash - Timestamp UnixTime32 - Count uint16 - Comm string - PodName string - ContainerName string + Hash TraceHash + Timestamp UnixTime64 + Count uint16 + Comm string + PodName string + ContainerName string + APMServiceName string } type FrameMetadata struct { FileID FileID AddressOrLine AddressOrLineno - LineNumber SourceLineno + LineNumber util.SourceLineno FunctionOffset uint32 FunctionName string Filename string } -// StackFrame represents a stack frame - an ID for the file it belongs to, an -// address (in case it is a binary file) or a line number (in case it is a source -// file), and a type that says what type of frame this is (Python, PHP, native, -// more languages in the future). -// type StackFrame struct { -// file FileID -// addressOrLine AddressOrLineno -// frameType InterpType -// } - -// ComputeFileCRC32 computes the CRC32 hash of a file -func ComputeFileCRC32(filePath string) (int32, error) { - f, err := os.Open(filePath) - if err != nil { - return 0, fmt.Errorf("unable to compute CRC32 for %v: %v", filePath, err) - } - defer f.Close() - - h := crc32.NewIEEE() - - _, err = io.Copy(h, f) - if err != nil { - return 0, fmt.Errorf("unable to compute CRC32 for %v: %v (failed copy)", filePath, err) - } - - return int32(h.Sum32()), nil -} - -// OnDiskFileIdentifier can be used as unique identifier for a file. -// It is a structure to identify a particular file on disk by -// deviceID and inode number. -type OnDiskFileIdentifier struct { - DeviceID uint64 // dev_t as reported by stat. - InodeNum uint64 // ino_t should fit into 64 bits -} - -func (odfi OnDiskFileIdentifier) Hash32() uint32 { - return uint32(hash.Uint64(odfi.InodeNum) + odfi.DeviceID) -} - -// GetOnDiskFileIdentifier builds a unique identifier of a given filename -// based on the information we can extract from stat. -func GetOnDiskFileIdentifier(filename string) (OnDiskFileIdentifier, error) { - var st unix.Stat_t - err := unix.Stat(filename, &st) - if err != nil { - // Putting filename into the error makes it escape to the heap. - // Since this is a common path, we try to avoid it. - // Currently, the only caller discards the error string anyway. - return OnDiskFileIdentifier{}, fmt.Errorf("failed to stat: %v", - err) - } - return OnDiskFileIdentifier{ - DeviceID: st.Dev, - InodeNum: st.Ino}, - nil -} - -// TimeToInt64 converts a time.Time to an int64. It preserves the "zero-ness" across the -// conversion, which means a zero Time is converted to 0. -func TimeToInt64(t time.Time) int64 { - if t.IsZero() { - // t.UnixNano() is undefined if t.IsZero() is true. - return 0 - } - return t.UnixNano() -} - -// Int64ToTime converts an int64 to a time.Time. It preserves the "zero-ness" across the -// conversion, which means 0 is converted to a zero time.Time (instead of the Unix epoch). -func Int64ToTime(t int64) time.Time { - if t == 0 { - return time.Time{} - } - return time.Unix(0, t) -} - -// KTime stores a time value, retrieved from a monotonic clock, in nanoseconds -type KTime int64 - -// GetKTime gets the current time in same nanosecond format as bpf_ktime_get_ns() eBPF call -// This relies runtime.nanotime to use CLOCK_MONOTONIC. If this changes, this needs to -// be adjusted accordingly. Using this internal is superior in performance, as it is able -// to use the vDSO to query the time without syscall. -// -//go:noescape -//go:linkname GetKTime runtime.nanotime -func GetKTime() KTime - // Void allows to use maps as sets without memory allocation for the values. // From the "Go Programming Language": // @@ -585,9 +99,3 @@ func GetKTime() KTime // that only the keys are significant, but the space saving is marginal and the syntax more // cumbersome, so we generally avoid it. type Void struct{} - -// Range describes a range with Start and End values. -type Range struct { - Start uint64 - End uint64 -} diff --git a/libpf/libpf_test.go b/libpf/libpf_test.go index 5476ab0c..1641c7ae 100644 --- a/libpf/libpf_test.go +++ b/libpf/libpf_test.go @@ -8,274 +8,18 @@ package libpf import ( "fmt" + "strconv" "testing" - assert "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestFileIDSprintf(t *testing.T) { - var origID FileID - var err error - - if origID, err = FileIDFromString("600DCAFE4A110000F2BF38C493F5FB92"); err != nil { - t.Fatalf("Failed to build FileID from string: %v", err) - } - - marshaled := fmt.Sprintf("%d", origID) - // nolint:goconst - expected := "{6921411395851452416 17491761894677412754}" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - marshaled = fmt.Sprintf("%s", origID) - expected = "{%!s(uint64=6921411395851452416) %!s(uint64=17491761894677412754)}" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - marshaled = fmt.Sprintf("%v", origID) - expected = "{6921411395851452416 17491761894677412754}" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - marshaled = fmt.Sprintf("%#v", origID) - expected = "0x600dcafe4a110000f2bf38c493f5fb92" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - fileID := NewFileID(5705163814651576546, 12305932466601883523) - - marshaled = fmt.Sprintf("%x", fileID) - expected = "4f2cd0431db840e2aac77460f5c07783" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - marshaled = fmt.Sprintf("%X", fileID) - expected = "4F2CD0431DB840E2AAC77460F5C07783" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - marshaled = fmt.Sprintf("%#x", fileID) - expected = "0x4f2cd0431db840e2aac77460f5c07783" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - marshaled = fmt.Sprintf("%#X", fileID) - expected = "0x4F2CD0431DB840E2AAC77460F5C07783" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } -} - -func TestFileIDMarshal(t *testing.T) { - var origID FileID - var err error - - if origID, err = FileIDFromString("600DCAFE4A110000F2BF38C493F5FB92"); err != nil { - t.Fatalf("Failed to build FileID from string: %v", err) - } - - // Test (Un)MarshalJSON - var data []byte - if data, err = origID.MarshalJSON(); err != nil { - t.Fatalf("Failed to marshal FileID: %v", err) - } - - marshaled := string(data) - expected := "\"600dcafe4a110000f2bf38c493f5fb92\"" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - var jsonID FileID - if err = jsonID.UnmarshalJSON(data); err != nil { - t.Fatalf("Failed to unmarshal FileID: %v", err) - } - - if jsonID != origID { - t.Fatalf("new FileID is different to original one. Expected %d, got %d", origID, jsonID) - } - - // Test (Un)MarshalText - if data, err = origID.MarshalText(); err != nil { - t.Fatalf("Failed to marshal FileID: %v", err) - } - - marshaled = string(data) - expected = "600dcafe4a110000f2bf38c493f5fb92" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - var textID FileID - if err = textID.UnmarshalText(data); err != nil { - t.Fatalf("Failed to unmarshal FileID: %v", err) - } - - if textID != origID { - t.Fatalf("new FileID is different to original one. Expected %d, got %d", origID, textID) - } -} - -func TestInvalidFileIDs(t *testing.T) { - // 15 characters - if _, err := FileIDFromString("600DCAFE4A11000"); err == nil { - t.Fatalf("Expected an error") - } - // Non-hex characters - if _, err := FileIDFromString("600DCAFE4A11000G"); err == nil { - t.Fatalf("Expected an error") - } -} - -func TestFileIDFromBase64(t *testing.T) { - expected := NewFileID(0x12345678124397ff, 0x87654321877484a8) - fileIDURLEncoded := "EjRWeBJDl_-HZUMhh3SEqA" - fileIDStdEncoded := "EjRWeBJDl/+HZUMhh3SEqA" - - actual, err := FileIDFromBase64(fileIDURLEncoded) - assert.Nil(t, err) - assert.Equal(t, expected, actual) - - actual, err = FileIDFromBase64(fileIDStdEncoded) - assert.Nil(t, err) - assert.Equal(t, expected, actual) -} - -func TestFileIDBase64(t *testing.T) { - expected := "EjRWeBJDl_WHZUMhh3SEng" - fileID := NewFileID(0x12345678124397f5, 0x876543218774849e) - - assert.Equal(t, fileID.Base64(), expected) -} - -func TestTraceHashSprintf(t *testing.T) { - origHash := NewTraceHash(0x0001C03F8D6B8520, 0xEDEAEEA9460BEEBB) - - marshaled := fmt.Sprintf("%d", origHash) - // nolint:goconst - expected := "{492854164817184 17143777342331285179}" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - marshaled = fmt.Sprintf("%s", origHash) - expected = "{%!s(uint64=492854164817184) %!s(uint64=17143777342331285179)}" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - marshaled = fmt.Sprintf("%v", origHash) - // nolint:goconst - expected = "{492854164817184 17143777342331285179}" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - marshaled = fmt.Sprintf("%#v", origHash) - expected = "0x1c03f8d6b8520edeaeea9460beebb" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - // Values were chosen to test non-zero-padded output - traceHash := NewTraceHash(42, 100) - - marshaled = fmt.Sprintf("%x", traceHash) - expected = "2a0000000000000064" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - marshaled = fmt.Sprintf("%X", traceHash) - expected = "2A0000000000000064" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - marshaled = fmt.Sprintf("%#x", traceHash) - expected = "0x2a0000000000000064" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - marshaled = fmt.Sprintf("%#X", traceHash) - expected = "0x2A0000000000000064" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } -} - -func TestTraceHashMarshal(t *testing.T) { - origHash := NewTraceHash(0x600DF00D, 0xF00D600D) - var err error - - // Test (Un)MarshalJSON - var data []byte - if data, err = origHash.MarshalJSON(); err != nil { - t.Fatalf("Failed to marshal TraceHash: %v", err) - } - - marshaled := string(data) - expected := "\"00000000600df00d00000000f00d600d\"" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - var jsonHash TraceHash - if err = jsonHash.UnmarshalJSON(data); err != nil { - t.Fatalf("Failed to unmarshal TraceHash: %v", err) - } - - if origHash != jsonHash { - t.Fatalf("new TraceHash is different to original one") - } - - // Test (Un)MarshalText - if data, err = origHash.MarshalText(); err != nil { - t.Fatalf("Failed to marshal TraceHash: %v", err) - } - - marshaled = string(data) - expected = "00000000600df00d00000000f00d600d" - if marshaled != expected { - t.Fatalf("Expected marshaled value %s, got %s", expected, marshaled) - } - - var textHash TraceHash - if err = textHash.UnmarshalText(data); err != nil { - t.Fatalf("Failed to unmarshal TraceHash: %v", err) - } - - if origHash != textHash { - t.Fatalf("new TraceHash is different to original one. Expected %s, got %s", - origHash, textHash) - } -} - -func TestCRC32(t *testing.T) { - crc32, err := ComputeFileCRC32("testdata/crc32_test_data") - if err != nil { - t.Fatal(err) - } - - expectedValue := uint32(0x526B888) - if uint32(crc32) != expectedValue { - t.Fatalf("expected CRC32 value 0x%x, got 0x%x", expectedValue, crc32) - } -} - func TestTraceType(t *testing.T) { tests := []struct { ty FrameType isErr bool - interp InterpType + interp InterpreterType str string }{ { @@ -304,3 +48,35 @@ func TestTraceType(t *testing.T) { assert.Equal(t, test.str, test.ty.String()) } } + +func TestUnixTime64_MarshalJSON(t *testing.T) { + tests := []struct { + name string + time UnixTime64 + want []byte + }{ + { + name: "zero", + time: UnixTime64(0), + want: []byte(strconv.Itoa(0)), + }, + { + name: "non-zero, seconds since the epoch", + time: UnixTime64(1710349106), + want: []byte(strconv.Itoa(1710349106)), + }, + { + name: "non-zero, nanoseconds since the epoch", + time: UnixTime64(1710349106864964685), + want: []byte(fmt.Sprintf("%q", "2024-03-13T16:58:26.864964685Z")), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + b, err := test.time.MarshalJSON() + require.NoError(t, err) + assert.Equal(t, test.want, b) + }) + } +} diff --git a/libpf/lpm/lpm.go b/libpf/lpm/lpm.go deleted file mode 100644 index 41e29845..00000000 --- a/libpf/lpm/lpm.go +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -// lpm package provides helpers for calculating prefix lists from ranges -package lpm - -import ( - "fmt" - "math/bits" -) - -// Prefix stores the Key and its according Length for a LPM entry. -type Prefix struct { - Key uint64 - Length uint32 -} - -// getRightmostSetBit returns a value that has exactly one bit, the rightmost bit of the given x. -func getRightmostSetBit(x uint64) uint64 { - return (x & (-x)) -} - -// CalculatePrefixList calculates and returns a set of keys that cover the interval for the given -// range from start to end, with the 'end' not being included. -// Longest-Prefix-Matching (LPM) tries structure their keys according to the most significant bits. -// This also means a prefix defines how many of the significant bits are checked for a lookup in -// this trie. The `keys` and `keyBits` returned by this algorithm reflect this. While the list of -// `keys` holds the smallest number of keys that are needed to cover the given interval from `start` -// to `end`. And `keyBits` holds the information how many most significant bits are set for a -// particular `key`. -// -// The following algorithm divides the interval from start to end into a number of non overlapping -// `keys`. Where each `key` covers a range with a length that is specified with `keyBits` and where -// only a single bit is set in `keyBits`. In the LPM trie structure the `keyBits` define the minimum -// length of the prefix to look up this element with a key. -// -// Example for an interval from 10 to 22: -// ............. -// ^ ^ -// 10 20 -// -// In the first round of the loop the binary representation of 10 is 0b1010. So rmb will result in -// 2 (0b10). The sum of both is smaller than 22, so 10 will be the first key (a) and the loop will -// continue. -// aa........... -// ^ ^ -// 10 20 -// -// Then the sum of 12 (0b1100) with a rmb of 4 (0b100) will result in 16 and is still smaller than -// 22. -// aabbbb....... -// ^ ^ -// 10 20 -// -// The sum of the previous key and its keyBits result in the next key (c) 16 (0b10000). Its rmb is -// also 16 (0b10000) and therefore the sum is larger than 22. So to not exceed the given end of the -// interval rmb needs to be divided by two and becomes 8 (0b1000). As the sum of 16 and 8 still is -// larger than 22, 8 needs to be divided by two again and becomes 4 (0b100). -// aabbbbcccc... -// ^ ^ -// 10 20 -// -// The next key (d) is 20 (0b10100) and its rmb 4 (0b100). As the sum of both is larger than 22 -// the rmb needs to be divided by two again so it becomes 2 (0b10). And so we have the last key -// to cover the range. -// aabbbbccccdd. -// ^ ^ -// 10 20 -// -// So to cover the range from 10 to 22 four different keys, 10, 12, 16 and 20 are needed. -func CalculatePrefixList(start, end uint64) ([]Prefix, error) { - if end <= start { - return nil, fmt.Errorf("can't build LPM prefixes from end (%d) <= start (%d)", - end, start) - } - - // Calculate the exact size of list. - listSize := 0 - for currentVal := start; currentVal < end; currentVal += calculateRmb(currentVal, end) { - listSize++ - } - - list := make([]Prefix, listSize) - - idx := 0 - for currentVal := start; currentVal < end; idx++ { - rmb := calculateRmb(currentVal, end) - list[idx].Key = currentVal - list[idx].Length = uint32(1 + bits.LeadingZeros64(rmb)) - currentVal += rmb - } - - return list, nil -} - -func calculateRmb(currentVal, end uint64) uint64 { - rmb := getRightmostSetBit(currentVal) - for currentVal+rmb > end { - rmb >>= 1 - } - return rmb -} diff --git a/libpf/lpm/lpm_test.go b/libpf/lpm/lpm_test.go deleted file mode 100644 index a6c38ed4..00000000 --- a/libpf/lpm/lpm_test.go +++ /dev/null @@ -1,84 +0,0 @@ -//go:build !integration -// +build !integration - -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -package lpm - -import ( - "testing" - - "github.com/google/go-cmp/cmp" -) - -func TestGetRightmostSetBit(t *testing.T) { - tests := map[string]struct { - input uint64 - expected uint64 - }{ - "1": {input: 0b1, expected: 0b1}, - "2": {input: 0b10, expected: 0b10}, - "3": {input: 0b11, expected: 0b1}, - "160": {input: 0b10100000, expected: 0b100000}, - } - - for name, test := range tests { - name := name - test := test - t.Run(name, func(t *testing.T) { - output := getRightmostSetBit(test.input) - if output != test.expected { - t.Fatalf("Expected %d (0b%b) but got %d (0b%b)", - test.expected, test.expected, output, output) - } - }) - } -} - -func TestCalculatePrefixList(t *testing.T) { - tests := map[string]struct { - start uint64 - end uint64 - err bool - expect []Prefix - }{ - "4k to 0": {start: 4096, end: 0, err: true}, - "10 to 22": {start: 0b1010, end: 0b10110, - expect: []Prefix{{0b1010, 63}, {0b1100, 62}, {0b10000, 62}, - {0b10100, 63}}}, - "4k to 16k": {start: 4096, end: 16384, - expect: []Prefix{{0x1000, 52}, {0x2000, 51}}}, - "0x55ff3f68a000 to 0x55ff3f740000": {start: 0x55ff3f68a000, end: 0x55ff3f740000, - expect: []Prefix{{0x55ff3f68a000, 51}, {0x55ff3f68c000, 50}, - {0x55ff3f690000, 48}, {0x55ff3f6a0000, 47}, - {0x55ff3f6c0000, 46}, {0x55ff3f700000, 46}}}, - "0x7f5b6ef4f000 to 0x7f5b6ef5d000": {start: 0x7f5b6ef4f000, end: 0x7f5b6ef5d000, - expect: []Prefix{{0x7f5b6ef4f000, 52}, {0x7f5b6ef50000, 49}, - {0x7f5b6ef58000, 50}, {0x7f5b6ef5c000, 52}}}, - } - - for name, test := range tests { - name := name - test := test - t.Run(name, func(t *testing.T) { - prefixes, err := CalculatePrefixList(test.start, test.end) - if err != nil { - if test.err { - // We received and expected an error. So we can return here. - return - } - t.Fatalf("Unexpected error: %v", err) - } - if test.err { - t.Fatalf("Expected an error but got none") - } - if diff := cmp.Diff(test.expect, prefixes); diff != "" { - t.Fatalf("CalculatePrefixList() mismatching prefixes (-want +got):\n%s", diff) - } - }) - } -} diff --git a/libpf/nativeunwind/intervalcache.go b/libpf/nativeunwind/intervalcache.go deleted file mode 100644 index be198422..00000000 --- a/libpf/nativeunwind/intervalcache.go +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -package nativeunwind - -import ( - "github.com/elastic/otel-profiling-agent/host" - "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" -) - -// IntervalCache defines an interface that allows one to save and load interval data for use in the -// unwinding of native stacks. It should be implemented by types that want to provide caching to -// `GetIntervalStructures`. -type IntervalCache interface { - // HasIntervals returns true if interval data exists in the cache for a file with the provided - // ID, or false otherwise. - HasIntervals(exeID host.FileID) bool - // GetIntervalData loads the interval data from the cache that is associated with `exeID` - // into `interval`. - GetIntervalData(exeID host.FileID, interval *stackdeltatypes.IntervalData) error - // SaveIntervalData stores the provided `interval` that is associated with `exeID` - // in the cache. - SaveIntervalData(exeID host.FileID, interval *stackdeltatypes.IntervalData) error - // GetCurrentCacheSize returns the current size of the cache in bytes. Or an error - // otherwise. - GetCurrentCacheSize() (uint64, error) - // GetAndResetHitMissCounters returns the current hit and miss counters of the cache - // and resets them to 0. - GetAndResetHitMissCounters() (hit, miss uint64) -} diff --git a/libpf/nativeunwind/localintervalcache/localintervalcache.go b/libpf/nativeunwind/localintervalcache/localintervalcache.go deleted file mode 100644 index 2e011558..00000000 --- a/libpf/nativeunwind/localintervalcache/localintervalcache.go +++ /dev/null @@ -1,429 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -package localintervalcache - -import ( - "compress/gzip" - "container/list" - "encoding/gob" - "errors" - "fmt" - "io" - "io/fs" - "os" - "path" - "path/filepath" - "sort" - "sync" - "sync/atomic" - "syscall" - "time" - - log "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" - - "github.com/elastic/otel-profiling-agent/config" - "github.com/elastic/otel-profiling-agent/host" - "github.com/elastic/otel-profiling-agent/libpf/nativeunwind" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" -) - -// cacheElementExtension defines the file extension used for elements in the cache. -const cacheElementExtension = "gz" - -// errElementTooLarge indicates that the element is larger than the max cache size. -var errElementTooLarge = errors.New("element too large for cache") - -// cacheDirPathSuffix returns the subdirectory within `config.CacheDirectory()` that will be used -// as the data directory for the interval cache. It contains the ABI version of the cache. -func cacheDirPathSuffix() string { - return fmt.Sprintf("otel-profiling-agent/interval_cache/%v", sdtypes.ABI) -} - -// entryInfo holds the size and lru list entry for a cache element. -type entryInfo struct { - size uint64 - lruEntry *list.Element -} - -// Cache implements the `nativeunwind.IntervalCache` interface. It stores its cache data in a local -// sub-directory of `CacheDirectory`. -// The cache evicts data based on a LRU policy, with usage order preserved across HA restarts. -// If the cache grows larger than maxSize bytes elements will be removed from the cache before -// adding new ones, starting by the element with the oldest access time. To keep the order of the -// LRU cache across restarts, the population of the LRU is based on the access time information -// of existing elements. -type Cache struct { - hitCounter atomic.Uint64 - missCounter atomic.Uint64 - - cacheDir string - // maxSize represents the configured maximum size of the cache. - maxSize uint64 - - // A mutex to synchronize access to internal fields entries and lru to avoid race conditions. - mu sync.RWMutex - // entries maps the name of elements in the cache to their size and element in the lru list. - entries map[string]entryInfo - // lru holds a list of elements in the cache ordered by their last access time. - lru *list.List -} - -// Compile time check that the Cache implements the IntervalCache interface -var _ nativeunwind.IntervalCache = &Cache{} - -// We define 2 pools to offload the GC from allocating and freeing gzip writers and readers. -// The pools will be used to write/read files of the intervalcache during encoding/decoding. -var ( - compressors = sync.Pool{ - New: func() any { - return gzip.NewWriter(io.Discard) - }, - } - - decompressors = sync.Pool{ - New: func() any { - return &gzip.Reader{} - }, - } -) - -// elementData holds the access time from the file system information and size information -// for an element. -type elementData struct { - atime time.Time - name string - size uint64 -} - -// New creates a new Cache using `path.Join(config.CacheDirectory(), cacheDirPathSuffix())` as the -// data directory for the cache. If that directory does not exist it will be created. However, -// `CacheDirectory` itself must already exist. -func New(maxSize uint64) (*Cache, error) { - cacheDir := path.Join(config.CacheDirectory(), cacheDirPathSuffix()) - if _, err := os.Stat(cacheDir); os.IsNotExist(err) { - if err := os.MkdirAll(cacheDir, os.ModePerm); err != nil { - return nil, fmt.Errorf("failed to create interval cache directory (%s): %s", cacheDir, - err) - } - } - - // Directory exists. Make sure we can read from and write to it. - if err := unix.Access(cacheDir, unix.R_OK|unix.W_OK); err != nil { - return nil, fmt.Errorf("interval cache directory (%s) exists but we can't read or write it", - cacheDir) - } - - // Delete cache entries from obsolete ABI versions. - if err := deleteObsoletedABICaches(cacheDir); err != nil { - return nil, err - } - - var elements []elementData - - // Elements in the localintervalcache are persistent on the file system. So we add the already - // existing elements to elements, so we can sort them based on the access time and put them - // into the cache. - err := filepath.WalkDir(cacheDir, func(path string, info fs.DirEntry, err error) error { - if err != nil { - return err - } - if !info.IsDir() { - entry, errInfo := info.Info() - if errInfo != nil { - log.Debugf("Did not get file info from '%s': %v", path, errInfo) - // We return nil here instead of the error to continue walking - // entries in cacheDir. - return nil - } - stat := entry.Sys().(*syscall.Stat_t) - atime := time.Unix(stat.Atim.Sec, stat.Atim.Nsec) - elements = append(elements, elementData{ - name: info.Name(), - size: uint64(entry.Size()), - atime: atime, - }) - } - return err - }) - if err != nil { - return nil, fmt.Errorf("failed to get preexisting cache elements: %v", err) - } - - // Sort all elements based on their access time from oldest to newest. - sort.SliceStable(elements, func(i, j int) bool { - return elements[i].atime.Before(elements[j].atime) - }) - - entries := make(map[string]entryInfo) - lru := list.New() - - // Put the information about preexisting elements into the cache. As elements - // is sorted based on the access time from oldest to newest we add the next element - // before the last added element into the lru. - for _, e := range elements { - lruEntry := lru.PushFront(e.name) - entries[e.name] = entryInfo{ - size: e.size, - lruEntry: lruEntry, - } - } - - return &Cache{ - maxSize: maxSize, - cacheDir: cacheDir, - entries: entries, - lru: lru}, nil -} - -// GetCurrentCacheSize returns the current size of all elements in the cache. -func (c *Cache) GetCurrentCacheSize() (uint64, error) { - c.mu.RLock() - defer c.mu.RUnlock() - - var size uint64 - for _, entry := range c.entries { - size += entry.size - } - return size, nil -} - -// getCacheFile constructs the path in the cache for the interval data associated -// with the provided executable ID. -func (c *Cache) getPathForCacheFile(exeID host.FileID) string { - return fmt.Sprintf("%s/%s.%s", c.cacheDir, exeID.StringNoQuotes(), cacheElementExtension) -} - -// HasIntervals returns true if interval data exists in the cache for a file with the provided -// ID, or false otherwise. -func (c *Cache) HasIntervals(exeID host.FileID) bool { - c.mu.RLock() - defer c.mu.RUnlock() - - _, ok := c.entries[exeID.StringNoQuotes()+"."+cacheElementExtension] - if !ok { - c.missCounter.Add(1) - return false - } - c.hitCounter.Add(1) - return true -} - -func gzipWriterGet(out io.Writer) *gzip.Writer { - w := compressors.Get().(*gzip.Writer) - w.Reset(out) - return w -} - -func gzipWriterPut(w *gzip.Writer) error { - if err := w.Flush(); err != nil { - return err - } - compressors.Put(w) - return nil -} - -func gzipReaderGet(in io.Reader) (*gzip.Reader, error) { - w := decompressors.Get().(*gzip.Reader) - if err := w.Reset(in); err != nil { - return nil, err - } - return w, nil -} - -func gzipReaderPut(r *gzip.Reader) { - decompressors.Put(r) -} - -// decompressAndDecode provides the ability to decompress and decode data that has been written to -// a file with `compressAndEncode`. The `destination` must be passed by reference. -func (c *Cache) decompressAndDecode(inPath string, destination any) error { - reader, err := os.Open(inPath) - if err != nil { - return fmt.Errorf("failed to open %s: %s", inPath, err) - } - defer reader.Close() - - zr, err := gzipReaderGet(reader) - if err != nil { - return fmt.Errorf("failed to create new gzip reader on %s: %s", inPath, err) - } - defer gzipReaderPut(zr) - - decoder := gob.NewDecoder(zr) - err = decoder.Decode(destination) - if err != nil { - return fmt.Errorf("failed to decompress and decode data from %s: %s", inPath, err) - } - - return nil -} - -// encodeAndCompress provides the ability to encode a generic data type, compress it, and write -// it to the provided output path. -func (c *Cache) encodeAndCompress(outPath string, source any) error { - // Open a file, create it if not existent - out, err := os.OpenFile(outPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) - if err != nil { - return fmt.Errorf("failed to open local interval cache file at %s: %v", outPath, err) - } - defer out.Close() - zw := gzipWriterGet(out) - - // Encode and compress the data, write the data to the cache file - encoder := gob.NewEncoder(zw) - if err := encoder.Encode(source); err != nil { - return fmt.Errorf("failed to encode and compress data: %s", err) - } - - return gzipWriterPut(zw) -} - -// GetIntervalData loads the interval data from the cache that is associated with `exeID` -// into `interval`. -func (c *Cache) GetIntervalData(exeID host.FileID, interval *sdtypes.IntervalData) error { - // Load the data and check for errors before updating the IntervalStructures, to avoid - // half-initializing it. - var data sdtypes.IntervalData - cacheElementPath := c.getPathForCacheFile(exeID) - if err := c.decompressAndDecode(cacheElementPath, &data); err != nil { - return fmt.Errorf("failed to load stack delta ranges: %s", err) - } - *interval = data - - c.mu.Lock() - // Update the last access information for this element. - entryName := filepath.Base(cacheElementPath) - entry := c.entries[entryName] - c.lru.MoveToFront(entry.lruEntry) - c.mu.Unlock() - - // Update the access and modification time for the element on the file system with the - // current time. So in case of a restart of our host agent we can order the elements in - // the cache correctly. - - // Use non-nil argument to Utime to mitigate GO bug for ARM64 Linux - curTime := &syscall.Timeval{} - if err := syscall.Gettimeofday(curTime); err != nil { - return fmt.Errorf("failed to get current time: %s", err) - } - - fileTime := &unix.Utimbuf{ - Actime: curTime.Sec, - Modtime: curTime.Sec, - } - if err := unix.Utime(cacheElementPath, fileTime); err != nil { - // We just log the error here instead of returning it as for further processing - // the relevant interval data is available. - // Not being able to update the access and modification time might indicate a - // problem with the file system. As a result on a restart of our host agent - // the order of elements in the cache might not be correct. - log.Errorf("Failed to update access time for '%s': %v", cacheElementPath, err) - } - - return nil -} - -// SaveIntervalData stores the provided `interval` that is associated with `exeID` -// in the cache. -func (c *Cache) SaveIntervalData(exeID host.FileID, interval *sdtypes.IntervalData) error { - cacheElement := c.getPathForCacheFile(exeID) - if err := c.encodeAndCompress(cacheElement, interval); err != nil { - return fmt.Errorf("failed to save stack delta ranges: %s", err) - } - info, err := os.Stat(cacheElement) - if err != nil { - return err - } - - cacheElementSize := uint64(info.Size()) - if cacheElementSize > c.maxSize { - if err = os.RemoveAll(cacheElement); err != nil { - return fmt.Errorf("failed to delete '%s': %v", cacheElement, err) - } - return fmt.Errorf("too large interval data for 0x%x (%d bytes): %w", - exeID, cacheElementSize, errElementTooLarge) - } - - // In this implementation of the cache GetCurrentCacheSize never returns an error - currentSize, _ := c.GetCurrentCacheSize() - - c.mu.Lock() - defer c.mu.Unlock() - if c.maxSize < currentSize+cacheElementSize { - if err = c.evictEntries(currentSize + cacheElementSize - c.maxSize); err != nil { - return err - } - } - - entryName := info.Name() - lruEntry := c.lru.PushFront(entryName) - c.entries[info.Name()] = entryInfo{ - size: cacheElementSize, - lruEntry: lruEntry, - } - return nil -} - -// GetAndResetHitMissCounters retrieves the current hit and miss counters and -// resets them to 0. -func (c *Cache) GetAndResetHitMissCounters() (hit, miss uint64) { - hit = c.hitCounter.Swap(0) - miss = c.missCounter.Swap(0) - return hit, miss -} - -// deleteObsoletedABICaches deletes all data that is related to obsolete ABI versions. -func deleteObsoletedABICaches(cacheDir string) error { - cacheBase := filepath.Dir(cacheDir) - - for i := 0; i < sdtypes.ABI; i++ { - oldABICachePath := fmt.Sprintf("%s/%d", cacheBase, i) - if _, err := os.Stat(oldABICachePath); err != nil { - if os.IsNotExist(err) { - continue - } - return err - } - if err := os.RemoveAll(oldABICachePath); err != nil { - return err - } - } - return nil -} - -// evictEntries deletes elements from the cache. It will delete elements with the oldest modTime -// information until the sum of deleted bytes is at toBeDeletedBytes. -// The caller is responsible to hold the lock on the cache to avoid race conditions. -func (c *Cache) evictEntries(toBeDeletedBytes uint64) error { - // sumDeletedBytes holds the number of bytes that are already deleted - // from this cache. - var sumDeletedBytes uint64 - - for { - if toBeDeletedBytes <= sumDeletedBytes { - return nil - } - oldestEntry := c.lru.Back() - if oldestEntry == nil { - return fmt.Errorf("cache is now empty - %d bytes were requested to be deleted, "+ - "but there were only %d bytes in the cache", toBeDeletedBytes, sumDeletedBytes) - } - entryName := oldestEntry.Value.(string) - - // Remove element from the filesystem. - if err := os.RemoveAll(path.Join(c.cacheDir, entryName)); err != nil { - return fmt.Errorf("failed to delete %s: %v", - path.Join(c.cacheDir, entryName), err) - } - - // Remove information about the element from the cache. - c.lru.Remove(oldestEntry) - sumDeletedBytes += c.entries[entryName].size - delete(c.entries, entryName) - } -} diff --git a/libpf/nativeunwind/localintervalcache/localintervalcache_test.go b/libpf/nativeunwind/localintervalcache/localintervalcache_test.go deleted file mode 100644 index 4472b5e2..00000000 --- a/libpf/nativeunwind/localintervalcache/localintervalcache_test.go +++ /dev/null @@ -1,587 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -package localintervalcache - -import ( - "errors" - "fmt" - "math/rand" - "os" - "path" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/stretchr/testify/assert" - - "github.com/elastic/otel-profiling-agent/config" - "github.com/elastic/otel-profiling-agent/host" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" -) - -// preTestSetup defines a type for a setup function that can be run prior to a particular test -// executing. It is used below to allow table-drive tests to modify CacheDirectory to point to -// different cache directories, as required. -type preTestSetup func(t *testing.T) - -func TestNewIntervalCache(t *testing.T) { - // nolint:gosec - seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) - - // A top level directory to hold other directories created during this test - testTopLevel, err := os.MkdirTemp("", "*_TestNewIntervalCache") - if err != nil { - t.Fatalf("Failed to create temporary directory: %v", err) - } - defer os.RemoveAll(testTopLevel) - - tests := map[string]struct { - // setupFunc is a function that will be called prior to running a test that will - // set up a cache directory in a new CacheDirectory, in whatever manner the test - // requires. It will then modify the configuration to set the config.cacheDirectory - // equal to the new CacheDirectory. - setupFunc preTestSetup - // hasError should be true if a test expects an error from New, or false - // otherwise. - hasError bool - // expectedSize holds the expected size of the cache in bytes. - expectedSize uint64 - }{ - // Successful creation when there is no pre-existing cache directory - "CorrectCacheDirectoryNoCache": { - setupFunc: func(t *testing.T) { - // A directory to use as CacheDirectory that does not have a cache already - cacheDirectoryNoCache := path.Join(testTopLevel, - fmt.Sprintf("%x", seededRand.Uint32())) - if err := os.Mkdir(cacheDirectoryNoCache, os.ModePerm); err != nil { - t.Fatalf("Failed to create directory (%s): %s", cacheDirectoryNoCache, err) - } - - err := config.SetConfiguration(&config.Config{ - ProjectID: 42, - CacheDirectory: cacheDirectoryNoCache, - SecretToken: "secret"}) - if err != nil { - t.Fatalf("failed to set temporary config: %s", err) - } - }, - hasError: false, - }, - // Successful creation when there is a pre-existing cache - "CorrectCacheDirectoryWithCache": { - setupFunc: func(t *testing.T) { - // A directory to use as CacheDirectory that has an accessible cache - cacheDirectoryWithCache := path.Join(testTopLevel, - fmt.Sprintf("%x", seededRand.Uint32())) - cacheDir := path.Join(cacheDirectoryWithCache, cacheDirPathSuffix()) - if err := os.MkdirAll(cacheDir, os.ModePerm); err != nil { - t.Fatalf("Failed to create directory (%s): %s", cacheDir, err) - } - - err := config.SetConfiguration(&config.Config{ - ProjectID: 42, - CacheDirectory: cacheDirectoryWithCache, - SecretToken: "secret"}) - if err != nil { - t.Fatalf("failed to set temporary config: %s", err) - } - }, - hasError: false, - }, - // Successful creation of a cache with pre-existing elements - "Use pre-exiting elements": { - setupFunc: func(t *testing.T) { - // A directory to use as CacheDirectory that has an accessible cache - cacheDirectoryWithCache := path.Join(testTopLevel, - fmt.Sprintf("%x", seededRand.Uint32())) - cacheDir := path.Join(cacheDirectoryWithCache, cacheDirPathSuffix()) - if err := os.MkdirAll(cacheDir, os.ModePerm); err != nil { - t.Fatalf("Failed to create directory (%s): %s", cacheDir, err) - } - - err := config.SetConfiguration(&config.Config{ - ProjectID: 42, - CacheDirectory: cacheDirectoryWithCache, - SecretToken: "secret"}) - if err != nil { - t.Fatalf("failed to set temporary config: %s", err) - } - populateCache(t, cacheDir, 100) - }, - expectedSize: 100 * 10, - hasError: false, - }, - } - - for name, tc := range tests { - name := name - tc := tc - t.Run(name, func(t *testing.T) { - tc.setupFunc(t) - expectedCacheDir := path.Join(config.CacheDirectory(), cacheDirPathSuffix()) - cacheDirExistsBeforeTest := true - if _, err := os.Stat(expectedCacheDir); os.IsNotExist(err) { - cacheDirExistsBeforeTest = false - } - - intervalCache, err := New(100) - if tc.hasError { - if err == nil { - t.Errorf("Expected an error but didn't get one") - } - - if intervalCache != nil { - t.Errorf("Expected nil IntervalCache") - } - - if _, err = os.Stat(expectedCacheDir); err == nil && !cacheDirExistsBeforeTest { - t.Errorf("Cache directory (%s) should not be created on failure", - expectedCacheDir) - } - return - } - - if err != nil { - t.Errorf("%s", err) - } - - if intervalCache == nil { - t.Fatalf("Expected an IntervalCache but got nil") - return - } - - if intervalCache.cacheDir != expectedCacheDir { - t.Errorf("Expected cache dir '%s' but got '%s'", - expectedCacheDir, intervalCache.cacheDir) - } - - if _, err = os.Stat(intervalCache.cacheDir); err != nil { - t.Errorf("Tried to stat cache dir (%s) and got error: %s", - intervalCache.cacheDir, err) - } - - size, err := intervalCache.GetCurrentCacheSize() - if err != nil { - t.Fatalf("Failed to get size of cache: %v", err) - } - if size != tc.expectedSize { - t.Fatalf("Expected a size of %d but got %d", tc.expectedSize, size) - } - }) - } -} - -func TestDeleteObsoletedABICaches(t *testing.T) { - // A top level directory to hold other directories created during this test - testTopLevel := setupDirAndConf(t, "*_TestEviction") - defer os.RemoveAll(testTopLevel) - cacheDir := path.Join(testTopLevel, cacheDirPathSuffix()) - if err := os.MkdirAll(cacheDir, os.ModePerm); err != nil { - t.Fatalf("Failed to create directory (%s): %s", cacheDir, err) - } - - // Prepopulate the cache with 100 elements where each element - // has a size of 10 bytes. - populateCache(t, cacheDir, 100) - - // Create an obsolete cache and populate it. - cacheDirBase := filepath.Dir(cacheDir) - obsoleteCacheDir := path.Join(cacheDirBase, fmt.Sprintf("%d", sdtypes.ABI-1)) - if err := os.MkdirAll(obsoleteCacheDir, os.ModePerm); err != nil { - t.Fatalf("Failed to create directory (%s): %s", obsoleteCacheDir, err) - } - // Prepopulate the cache with 100 elements where each element - // has a size of 10 bytes. - populateCache(t, obsoleteCacheDir, 100) - - cache, err := New(100 * 10) - if err != nil { - t.Fatalf("failed to create cache for test: %v", err) - } - - _, err = cache.GetCurrentCacheSize() - if err != nil { - t.Fatalf("Failed to get current size: %v", err) - } - - if _, err = os.Stat(obsoleteCacheDir); os.IsNotExist(err) { - // The obsolete cache directory no longer exists. We - // received the expected error and can return here. - return - } - t.Fatalf("Expected obsolete cache directory to no longer exist but got %v", err) -} - -// TestEvictionFullCache tests with a cache that exceeds the maximum size that a newly -// added element is added to the tail of the LRU and after this element got accessed it -// is moved to the front of the LRU. -func TestEvictionFullCache(t *testing.T) { - // A top level directory to hold other directories created during this test - testTopLevel := setupDirAndConf(t, "*_TestEviction") - defer os.RemoveAll(testTopLevel) - cacheDir := path.Join(testTopLevel, cacheDirPathSuffix()) - if err := os.MkdirAll(cacheDir, os.ModePerm); err != nil { - t.Fatalf("Failed to create directory (%s): %s", cacheDir, err) - } - - // Prepopulate the cache with 202 elements where each element - // has a size of 10 bytes. - populateCache(t, cacheDir, 202) - - maxCacheSize := uint64(200 * 10) - - cache, err := New(maxCacheSize) - if err != nil { - t.Fatalf("failed to create cache for test: %v", err) - } - - currentCacheSize, err := cache.GetCurrentCacheSize() - if err != nil { - t.Fatalf("Failed to get current size: %v", err) - } - t.Logf("current cache size before adding new elements: %d", currentCacheSize) - - // Create a new element that will be added to the cache. - // nolint:gosec - id := rand.Uint64() - idString := cache.getPathForCacheFile(host.FileID(id)) - exeID1, intervalData1 := testArtifacts(id) - - // Add the new element to the full cache. - if err = cache.SaveIntervalData(exeID1, intervalData1); err != nil { - t.Fatalf("Failed to add new element to cache: %v", err) - } - currentCacheSize, err = cache.GetCurrentCacheSize() - if err != nil { - t.Fatalf("Failed to get current size: %v", err) - } - if currentCacheSize > maxCacheSize { - t.Fatalf("current cache size (%d) is larger than max cache size (%d)", - currentCacheSize, maxCacheSize) - } - - // Make sure the newly added element was added to the front of the LRU. - currentFirstElement := (cache.lru.Front().Value).(string) - if !strings.Contains(idString, currentFirstElement) { - t.Fatalf("Newly inserted element is not first element of lru") - } - - // Create a new element that will be added to the cache. - // nolint:gosec - id2 := rand.Uint64() - id2String := cache.getPathForCacheFile(host.FileID(id2)) - exeID2, intervalData2 := testArtifacts(id2) - - // Add the new element to the cache. - if err = cache.SaveIntervalData(exeID2, intervalData2); err != nil { - t.Fatalf("Failed to add new element to cache: %v", err) - } - - // Make sure the newly added element was added to the front of the LRU. - currentFirstElement = (cache.lru.Front().Value).(string) - if !strings.Contains(id2String, currentFirstElement) { - t.Fatalf("Newly inserted element is not first element of lru") - } - - result := new(sdtypes.IntervalData) - if err := cache.GetIntervalData(exeID1, result); err != nil { - t.Fatalf("Failed to get interval data: %v", err) - } - - // Make sure that the last accessed element is the first element of the LRU. - currentFirstElement = (cache.lru.Front().Value).(string) - if !strings.Contains(idString, currentFirstElement) { - t.Fatalf("Newly inserted element is not newest recently used element of lru " + - "after call to GetIntervalData()") - } -} - -// populateCache creates m fake elements within dir. Each element will have a size of 10 bytes. -func populateCache(t *testing.T, dir string, m int) { - t.Helper() - // nolint:gosec - seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) - - dummyContent := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} - for i := 0; i < m; i++ { - fileName := dir + fmt.Sprintf("/%x.gz", seededRand.Uint32()) - f, err := os.Create(fileName) - if err != nil { - t.Fatalf("Failed to create '%s': %v", fileName, err) - } - n, err := f.Write(dummyContent) - if err != nil || n != 10 { - t.Fatalf("Failed to write to '%s': %v", fileName, err) - } - f.Close() - } -} - -func TestCacheHasIntervals(t *testing.T) { - // nolint:gosec - seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) - // A top level directory to hold other directories created during this test - testTopLevel, err := os.MkdirTemp("", "TestCacheHasIntervals_*") - if err != nil { - t.Fatalf("Failed to create temporary directory: %v", err) - } - defer os.RemoveAll(testTopLevel) - - exeID1 := host.FileID(1) - exeID2 := host.FileID(2) - - tests := map[string]struct { - // setupFunc is a function that will be called prior to running a test that will - // set up a cache directory in a new CacheDirectory, in whatever manner the test - // requires. It will then modify the configuration to set the config.cacheDirectory - // equal to the new CacheDirectory. - setupFunc preTestSetup - // exeID specifies the ID of the executable that we wish to check the cache for - exeID host.FileID - // hasIntervals indicates the result we expect from calling intervalCache.HasIntervals - hasIntervals bool - }{ - // Check the case where we have interval data - "hasIntervals": { - exeID: exeID1, - hasIntervals: true, - setupFunc: func(t *testing.T) { - // A directory to use as CacheDirectory that has an accessible cache - validCacheDirectory := path.Join(testTopLevel, - fmt.Sprintf("%x", seededRand.Uint32())) - if err := os.Mkdir(validCacheDirectory, os.ModePerm); err != nil { - t.Fatalf("Failed to create directory (%s): %s", validCacheDirectory, err) - } - - err := config.SetConfiguration(&config.Config{ - ProjectID: 42, - CacheDirectory: validCacheDirectory, - SecretToken: "secret"}) - if err != nil { - t.Fatalf("failed to set temporary config: %s", err) - } - - validIC, err := New(100) - if err != nil { - t.Fatalf("failed to create new interval cache") - } - - // Create a valid cache entry for exeID1 - cacheFile := validIC.getPathForCacheFile(exeID1) - emptyFile, err := os.Create(cacheFile) - if err != nil { - t.Fatalf("Failed to create cache file (%s): %s", cacheFile, err) - } - emptyFile.Close() - }}, - // Check the case where we don't have interval data - "doesNotHaveIntervals": { - exeID: exeID2, - hasIntervals: false, - setupFunc: func(t *testing.T) { - // A directory to use as CacheDirectory that has an accessible cache - validCacheDirectory := path.Join(testTopLevel, - fmt.Sprintf("%x", seededRand.Uint32())) - if err := os.Mkdir(validCacheDirectory, os.ModePerm); err != nil { - t.Fatalf("Failed to create directory (%s): %s", validCacheDirectory, err) - } - - err := config.SetConfiguration(&config.Config{ - ProjectID: 42, - CacheDirectory: validCacheDirectory, - SecretToken: "secret"}) - if err != nil { - t.Fatalf("failed to set temporary config: %s", err) - } - }}, - // Check the case where the cache directory is not accessible - "brokenCacheDir": { - exeID: exeID1, - hasIntervals: false, - setupFunc: func(t *testing.T) { - // A directory in which the cache dir is unreadable - cacheDirectoryWithBrokenCacheDir := path.Join(testTopLevel, fmt.Sprintf("%x", - seededRand.Uint32())) - if err := os.Mkdir(cacheDirectoryWithBrokenCacheDir, os.ModePerm); err != nil { - t.Fatalf("Failed to create directory (%s): %s", - cacheDirectoryWithBrokenCacheDir, err) - } - - err := config.SetConfiguration(&config.Config{ - ProjectID: 42, - CacheDirectory: cacheDirectoryWithBrokenCacheDir, - SecretToken: "secret"}) - if err != nil { - t.Fatalf("failed to set temporary config: %s", err) - } - - icWithBrokenCache, err := New(100) - if err != nil { - t.Fatalf("failed to create interval cache: %s", err) - } - if err = os.Remove(icWithBrokenCache.cacheDir); err != nil { - t.Fatalf("Failed to remove %s: %s", icWithBrokenCache.cacheDir, err) - } - }}} - - for name, tc := range tests { - name := name - tc := tc - t.Run(name, func(t *testing.T) { - tc.setupFunc(t) - ic, err := New(100) - if err != nil { - t.Fatalf("failed to create interval cache: %s", err) - } - hasIntervals := ic.HasIntervals(tc.exeID) - - if tc.hasIntervals != hasIntervals { - t.Errorf("Expected %v but got %v", tc.hasIntervals, hasIntervals) - } - }) - } -} - -func TestSaveAndGetIntervalData(t *testing.T) { - tmpDir := setupDirAndConf(t, "TestSaveAndGetIntervaldata_*") - defer os.RemoveAll(tmpDir) - - tests := map[string]struct { - // maxCacheSize defines the maximum size of the test cache. - maxCacheSize uint64 - // saveErr defines an expected error if any for a call to SaveIntervalData. - saveErr error - }{ - "too small cache": {maxCacheSize: 100, saveErr: errElementTooLarge}, - "regular cache": {maxCacheSize: 1000}, - } - - exeID, intervalData := testArtifacts(1) - - for name, test := range tests { - name := name - test := test - t.Run(name, func(t *testing.T) { - icWithData, err := New(test.maxCacheSize) - if err != nil { - t.Fatalf("Failed to create IntervalCache: %s", err) - } - - if err := icWithData.SaveIntervalData(exeID, intervalData); err != nil { - if test.saveErr != nil && errors.Is(err, test.saveErr) { - // We received the expected error and can return here. - return - } - t.Fatalf("Failed to save interval data: %v", err) - } - if test.saveErr != nil { - t.Fatalf("Expected '%s' but got none", test.saveErr) - } - - result := new(sdtypes.IntervalData) - - if err := icWithData.GetIntervalData(exeID, result); err != nil { - t.Fatalf("Failed to get interval data: %v", err) - } - - if diff := cmp.Diff(intervalData, result); diff != "" { - t.Errorf("GetIntervaldata() mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func BenchmarkCache_SaveIntervalData(b *testing.B) { - b.StopTimer() - b.ReportAllocs() - tmpDir := setupDirAndConf(b, "BenchmarkSaveIntervalData_*") - defer os.RemoveAll(tmpDir) - intervalCache, err := New(1000) - if err != nil { - b.Fatalf("Failed to create IntervalCache: %s", err) - } - - exe, data := testArtifacts(1) - - for i := 0; i < b.N; i++ { - b.StartTimer() - err := intervalCache.SaveIntervalData(exe, data) - b.StopTimer() - if err != nil { - b.Fatalf("SaveIntervalData error: %v", err) - } - assert.Nil(b, os.Remove(intervalCache.getPathForCacheFile(exe))) - } -} - -func BenchmarkCache_GetIntervalData(b *testing.B) { - b.StopTimer() - b.ReportAllocs() - tmpDir := setupDirAndConf(b, "BenchmarkGetIntervalData_*") - defer os.RemoveAll(tmpDir) - intervalCache, err := New(1000) - if err != nil { - b.Fatalf("Failed to create IntervalCache: %s", err) - } - - exe, data := testArtifacts(1) - if err := intervalCache.SaveIntervalData(exe, data); err != nil { - b.Fatalf("error storing cache: %v", err) - } - - for i := 0; i < b.N; i++ { - var result sdtypes.IntervalData - b.StartTimer() - err := intervalCache.GetIntervalData(exe, &result) - b.StopTimer() - if err != nil { - b.Fatalf("GetIntervalData error: %v", err) - } - assert.Equal(b, data, &result) - } -} - -func deltaSP(sp int32) sdtypes.UnwindInfo { - return sdtypes.UnwindInfo{Opcode: sdtypes.UnwindOpcodeBaseSP, Param: sp} -} - -func testArtifacts(id uint64) (host.FileID, *sdtypes.IntervalData) { - exeID := host.FileID(id) - - intervalData := &sdtypes.IntervalData{ - Deltas: []sdtypes.StackDelta{ - {Address: 0 + id, Info: deltaSP(16)}, - {Address: 100 + id, Info: deltaSP(3)}, - {Address: 110 + id, Info: deltaSP(64)}, - {Address: 190 + id, Info: deltaSP(48)}, - {Address: 200 + id, Info: deltaSP(16)}, - }, - } - return exeID, intervalData -} - -// setupDirAndConf creates a temporary directory and sets the host-agent -// configuration with the test directory to avoid collisions during tests. -// Returns the path of the directory to be used for testing: -// the caller is responsible to delete this directory. -func setupDirAndConf(tb testing.TB, pattern string) string { - // A top level directory to hold test artifacts - testTopLevel, err := os.MkdirTemp("", pattern) - if err != nil { - tb.Fatalf("Failed to create temporary directory: %v", err) - } - - if err = config.SetConfiguration(&config.Config{ - ProjectID: 42, - CacheDirectory: testTopLevel, - SecretToken: "secret"}); err != nil { - tb.Fatalf("failed to set temporary config: %s", err) - } - return testTopLevel -} diff --git a/libpf/nativeunwind/localstackdeltaprovider/localstackdeltaprovider.go b/libpf/nativeunwind/localstackdeltaprovider/localstackdeltaprovider.go deleted file mode 100644 index 6c37c78e..00000000 --- a/libpf/nativeunwind/localstackdeltaprovider/localstackdeltaprovider.go +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -package localstackdeltaprovider - -import ( - "fmt" - "sync/atomic" - - "github.com/elastic/otel-profiling-agent/host" - "github.com/elastic/otel-profiling-agent/libpf/nativeunwind" - "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/elfunwindinfo" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" - "github.com/elastic/otel-profiling-agent/libpf/pfelf" - log "github.com/sirupsen/logrus" -) - -// LocalStackDeltaProvider extracts stack deltas from executables available -// on the local filesystem. -type LocalStackDeltaProvider struct { - // Metrics - hitCount atomic.Uint64 - missCount atomic.Uint64 - extractionErrorCount atomic.Uint64 - - // cache provides access to a cache of interval data that is preserved across runs of - // the agent, so that we only need to process an executable to extract intervals the - // first time a run of the agent sees the executable. - cache nativeunwind.IntervalCache -} - -// Compile time check that the LocalStackDeltaProvider implements its interface correctly. -var _ nativeunwind.StackDeltaProvider = (*LocalStackDeltaProvider)(nil) - -// New creates a local stack delta provider that uses the given cache to provide -// stack deltas for executables. -func New(cache nativeunwind.IntervalCache) *LocalStackDeltaProvider { - return &LocalStackDeltaProvider{ - cache: cache, - } -} - -// GetIntervalStructuresForFile builds the stack delta information for a single executable. -func (provider *LocalStackDeltaProvider) GetIntervalStructuresForFile(fileID host.FileID, - elfRef *pfelf.Reference, interval *sdtypes.IntervalData) error { - // Return cached data if it's available - if provider.cache.HasIntervals(fileID) { - var err error - if err = provider.cache.GetIntervalData(fileID, interval); err == nil { - provider.hitCount.Add(1) - return nil - } - provider.missCount.Add(1) - log.Debugf("Failed to get stack delta for %s from cache: %v", - elfRef.FileName(), err) - } - - err := elfunwindinfo.ExtractELF(elfRef, interval) - if err != nil { - provider.extractionErrorCount.Add(1) - return fmt.Errorf("failed to extract stack deltas from %s: %v", - elfRef.FileName(), err) - } - - return provider.cache.SaveIntervalData(fileID, interval) -} - -func (provider *LocalStackDeltaProvider) GetAndResetStatistics() nativeunwind.Statistics { - return nativeunwind.Statistics{ - Hit: provider.hitCount.Swap(0), - Miss: provider.missCount.Swap(0), - ExtractionErrors: provider.extractionErrorCount.Swap(0), - } -} diff --git a/libpf/nativeunwind/localstackdeltaprovider/localstackdeltaprovider_test.go b/libpf/nativeunwind/localstackdeltaprovider/localstackdeltaprovider_test.go deleted file mode 100644 index 20c38d33..00000000 --- a/libpf/nativeunwind/localstackdeltaprovider/localstackdeltaprovider_test.go +++ /dev/null @@ -1,431 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -package localstackdeltaprovider - -import ( - "bytes" - "compress/gzip" - "encoding/base64" - "fmt" - "io" - "os" - "testing" - - "github.com/google/go-cmp/cmp" - - "github.com/elastic/otel-profiling-agent/config" - "github.com/elastic/otel-profiling-agent/host" - "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/nativeunwind" - "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/localintervalcache" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" - "github.com/elastic/otel-profiling-agent/libpf/pfelf" -) - -// /usr/lib/vlc/plugins/video_filter/libinvert_plugin.so gzip'ed and base64'ed. -// nolint:lll -var usrLibVlcPluginsVideoFilterLibinvertPluginSo = `H4sICN6c+VwAA2xpYmludmVydF9wbHVnaW4uc28A7Vt9cBvHdb/DBwlKJA4ImYa2Vfuc0A0ZSiAp -UQo5tmocSUkH+SBREqVItiQQBA4kLIiAgQMlyopLBxRFBIZDu46tdjoZTdI2qjMTy53WI9FxBxL9 -IXXGKUWNbbqpE6pTJ6RVR3RVi1JsC31vbw8EbsTYnclM+weXg3373r7f27fvdvf2eHt/tk5ab2BZ -RktG5k8Z5I7bVN5J5QMNORWQNTElkN/O3EZ0TczC6VxRIWWoXcSZ83g9fdxaSPNxpD2eynX03wyF -NB+HLgyvUPnhtYV0gOoP63AGijtGccfWFlLGUEgtlDXRXxOV62kVU0i1GLa/r/ix7Lxb5fVUYgqp -htsCOC3EXyRp4d5K21soLgcNhVQbKYipYHC8MMyGTduZo/8x8f6Wv3tO2PzxSmnrSy+HWn5ZcYSh -9UuZ+fgz7BGMFXEb5aXwu/g399aMWJvedD3/vXt+n7/8LeRx+H3pFvIfLSB/ZQE5u4D9v1pA/4kF -5NsXkC9fwP56+H31FvIPiJ1SJnKnymvj6idUPrJM5V+lF+TnVN6p02c8nu794V5PTPFGFY+H8bg6 -3B6/HJW7gzFFjna4W0PhXrnD2xWS1bpb13h8B72eQLDXGwoekpm+kM8TCMejPp9ng6y09kTD+71t -cswXDUaUYLiXiQR9Sjwqe1rDkf72aDgiR5WgHMuJt8oh2RuTmf3yfl+kn1iTwt2Eyr1KtN/jWeWp -99QHoFlw27fP4+vZ5wl4g6E8FR+Yjga7e5Sc8nxdKOiTe2NyriYU7IJKXzgqO2JhRzPyPiytwVKw -tw/c80RC8e5gL0iZDZKrpdWz0rHSsTpXbmjMFRvnrxFOCSMtm2AEqX9GpoKdn1/x24MlWLuSyp55 -6tkisspSviIYLENLLnodtXmnXb92u0pHdHIblWsLdO56U37qfpXinJtf3RlmOk9uzpPP5slL8uTX -8+RL8uQnqbyYmV8CMZ3Kkxvz5Jk8ef794lyePH/9Gs+TF+fJJ/PkFmYxLabFtJgW02JaTItpMf1v -k5j4T4uYMr9XB8UjGcWQHRcTr1rGcvXZ1YegKnvPYci5u5xQQr4Hq2amspDuiSKPW8yZccI/jDxu -CWcyhO9CHreCMycJ/yDyuAWcOU74rcjjVnRmJKcfGMn5l17rRd/SZsSJzdeVL4O7f0LdLclOcXcN -oN4YpaDfQPRXfwNJzU3xzE2jmJwVz0zfL7KvixduKhVg4GOHasCSnQpwd7XN4wfWfgRVTLxuu5hY -+zYWxeT7SqmYWjsBzPQe8HC6B7LXzeeBZ/eM6dqfeRQqAw7urkHi/shYYJ7B+KU2RNbu2r6jXdmz -duuGlnZlBxJB2bxWAKqI28TkJ8k3pn99M5tF9xpStiMTSm1KbEs3vokm7o2I8WXQR3xObsiIyZc7 -IdIPjc23cQpj+NBY4lx2jPRL+JawQ9gudGzfBle6WjxyjRv8oyXYKaWqEtrKPnfMgFxblQ3a4wYt -WJdqq6p8vAm8FFIn2uHKcoM/hUeCIx9yTw4WYfXhqsq2tDsb4Poy8DvvOnPJLAHGtin9DDcOPkjp -H59EkuyosrjTJ7Ec0PxbB+iKddwLh6tKXSlTlStldfuyLvYNqfk6l/gSmBdSoarK+f5Ivrc2pXfb -Mq7mMW6IK0HjPU6peSZWJqb7nTAeuO/cDShXMnOVC3faIKsW0xGna24cijt5yNrrhVdweG5OlxoD -3BW43lemXWdmioBOwm+cs9e40rtYzu61CYlLRjG9xcnZd/GcfUu9mPiE5YaWQ4TcR85zg9fAiitx -g+UGf6CWDIoEDfRhq4/xwis46K+2ceH19ZhbxfTJAdL1K7Pwux5o465kMDvH2aO2Ns6+hLMf4oG2 -1IvppwYodocN8728K/EaKyZedyIXmMW8NyMmzjrRwDhmk5hNYTYNNraOQ/ZwBrIHpyDrmgWX7tRs -IA4t9I1j/tjUVTQZoCZmMbsOuAwGB7BRNHVoCo1IzZe5oRUQvPlegk2T+PjrjCp8th5GG9CfMkDF -x8+iONdfdBS84+x/b4Nazv48Q8goT8jTBAr+DLXTwpOdtPBshBZ+4MTJlIvbfN/Bw+8gDOhfOlX6 -3U6VPq2BAyMkasdJz0+Sng/Mx2y+6xiuEezyAIbwOEbgJEYIB9BejFUfBvOxDB07k1r/AAZj51AG -Qw4YEqyjF2HiutlJV2Iq40ofdErpZU0SOy6yN6TmSW7IAyPVnW61ieldNqn5jMD9+RlX8y+AvLbO -MgOTz4wLVyQr1fxSTNy0ckNzJpybrzVMSDXvuZIfuBL/PutKH4K17IpJTLzBJn5TzA2eIot4Fgbl -U2rJoByA3KjsgdyktEJuVmogL1LsVwUuXD5qUwflBc5OBMvLgZnh7MvtME5sV9dxYVM5BuYMRMKU -k5US2XmQleZkFUR2AWQVOdkyInsXZMtysioiuwSyKiKTmidUb7FyOamcaUMHkFfKbSj4CASKncAP -l/MouQGSw3YeXfaX14PPlzi7316P/O5yJ/DvcvbddifySnk78B+hhXbkD5d3An8D8Z3Id5RH1AB0 -2CNi4oMBdOhIGMaNKzkNl24Wl4qN6WWsq3maG9wIY3od9xIsEo+wwtxbbcO1YvMsN7QKxAL3UpB1 -p7cY5t5yDu9jpeZxbugOVd5vcKW3GOfecQ4/ahBRblTNCEZ3OmgS5sbbhluNEtgfwv/0AeARkysd -NM+97RyOmUSUTzAEsMXsSvcXCXMTbcPbzLjeDZ1WAVuK3GmheG5ieFuR1Jzhhn6o6geLwVELcXRf -MfE0qeoHYS3eUkI8tYCnfXH0sgS8XEK8LAEv+7zo4RLwcKnq4RLwsE9C75aCd6XEu6XgXV8TelYK -npWpnpWCZ31fQ6/KwCsrelUGXvXZQM9lFeYutA0/YMX7n5gcIyu8VPMLkX13XWp3VamQeqxUqH2s -Uqi5LPrecfsuiuwFmDSu5je4o5/dgIuSeBWWndd56V5/lYU7MgoiKeWH2xfcSmxS8tfTPXMgSf5m -+jhQvHnZYAtT/dAeYbewR9greMZIu6lKV/KMa+6COHfRzb4rJm8TU+0WvA/Aos+KcxPu5DkxuQX2 -QhGbOPe2mHzEJqXESgBISRc0tZMHoJjcxQOsWkpeBgSoV4N6vZj8nYqod6fc1wli83U3Lp1fZ/HO -kVG63Kl1DNySpGSxK7XHQix5LK7UdlgJjuHKT6x9y+ZKxSvpvYBYPFAJFnnVIg82qomNDdXu1P56 -19xbUjJcP/MC2VhA/1bhSBYTlwcINDogpR4YJtCNw1Jq3wjRf3hETD16TJx7R0weOialWo8Tgy3H -xVTshAo7AbCTKuwkwE6psFMAy6iwDMDOqbBzABtXYeMAm1RhkwCbUmFTAJtWYdMAm1VhsyC9rkqv -w/LHHXWSmZeB2pnM7+DegPsWGCcNH8IgcSXfExLvwzp3jNxyP2OVfsgNig9yo+KG3KQ0Q25WqiAv -UqwwBleR+z0O1gYYfhW1ZIhWwBr4Ggu199XieL9PYytqYbCqlaBsqiUD36TxpbVk7JZq/PJaMrOW -E15sfo8brP0smwUzjbXYXmMN6HTUsqTFjhq0f7jWgO0drjEIWGWE1jpqjKDWVqsuA201JuCkWjNp -SaoxAxeqLSLthGqKAKXUFqNxpaYYWrVAq/EnZ/7hUwyUEzY+YzBX8O5y9IcwMUak5OTM336K80DC -GdJRZUt8yKZeXIbbuBderGRx/zZqQ+J7W0y9iKXpnf+dzc788TUAvQGTh+xi/xFNwKOBlF79X0a8 -Gr8STv0Y15EdUrpsGm9L6bIzUPFPBrLn/vJvQTQ9ezWbbcgIp3rJrvlXIH7HhBu2svMmqiklG6sA -eQqQ02evopeHq6qnp6G088EZAdocEToaJraLyY8bruFWWExeAS94v5gy3VPNqBvVStz3XkJXyf9/ -sxdh33rk2yzZD6fLYuhtClpJXhktZrXqvSzxeA9UjpawOQ21UlArO7DSoq+sYrEHsPA0XDtdRDy4 -4k5OjbKk5dP4WDPK2ohYTCmglclebLgGGqeLSZWKQOnpOvZWuujzmxCaUYO+5edxi39+1KiXP00i -bv6uAR3DxU6tJHv40yx5Lhhlac/jPQ0ZKW0+SkKtqqXLHsXmTJrG+lEz6aJ5a4HSHgON3ffPci+d -nZvEoVTpF1dVkOsQ/wossTwusdpzD3nuuPzu9KcfwY4t7/kHPL3bQB6PBunznNmFPHnuLHjSXEz/ -F8nhqAv2+kJxv1xH3uoFQ4ocdfQwPm/v1xW+W1b4cFyJxBWevrxj+oJ+OUz16tSXZw4foxYYFyE8 -0eFVHaY1HApHeaIQw5eDrVHZq8hMmxxTouF+zY9W7X0eX91awys9Mr8DjUjCJn6H1Mrvl/1BLx8J -efvlKO+X++QQvlWMwcBV3/X5+XivH6oQCI3uj/HhAGE2bNrOS3IsBnUb5F456g3x7fGuUNDHU+Ry -njrGr3Q08OBpyIsR0PxSO+HZJB9opwFYr3aL9pi9w3gvvovHtWjqt9ks/mek/QosZED52Wz2BNB6 -oD8DehzoFNZ/ks3eB5OOh1tHBGg90EH6kq6Ctsse2sqwB23sHaXFlhFWPSeAr3wj0EYEFay29dbK -jdzSA5YB5v7b7/3Gqqqvani4fTKnQC///Rgu67sRD75p7wN55OEXAtk+FHRabQlDfInV8sDS9VaL -gHNzJ/yeQXug8wK+AGyx2r5naLFWPmlssfJpU4u1+gmzYK0fKhKsTYniNmun8XLREmsTiARrNaiA -KkBarJZ1S43fNFp7tlojhp3WHsHaydSDuVn4lcJNn7zHBsUnDK3WyiHjeiufMG20Oo3PsUusvGCt -FIgNcSn2A9fkE5/M9w9lTSD7608K+7yYFtNiWkyLaTEtpsW0mBbT/9eknfPTzvXlztmyhXwFLWjH -ou+kfKlmiB48LKPsQXoe9DbKa+cL76C8tl++nVLtnOEyXf3HN7NhpJ30sJ92dnCKHvLTzu6N0Hrt -rOA26t9SyldSqp0NPEbP82lnCLUXv9pzkHb27yuUNhUVykfMhX6eoFQ7w6i1d6euP/CoQPqjxfUm -5dupvayufpbyjbT+BuXzzzj+IVPufLkuraHXdz2lOygNUNpH6ZCN+WLJqZK6eCxaFwp21fnlrnh3 -ncN/4FDdwaY1njWNK0LB3vjBFd29cfwPwQr1oOyKLm9MdhBd5ty/dj8f3re25GeHxqpu/CjCSj+5 -tIGRG/0r5dWrG7zNzd9sbGgMBFavWtnoW72ya01X1xpf40q/3NDYtJpagPTz8uW7GEesJ6ZEFW8X -4+gNK7ID2nR0xYMh/4qgnyFcjzfWwzj8/b2x/v0qVaJqjfbvhXzGA3VROeRFRVqKhBTGEewNQg5F -R3cYCop8EPIASEEp7PcqXsYh93gCUe9+2dPjj85zKtTjjUa9/SpCK0ML3v1BHxQIvCsWI554SAe9 -IQWCuC9PQtg/RMJ5lX8Gd6HvDLSk/1YD58U1GMsaXpvfGq2ncm2c65+vq6kPuflrKKTt7Hy7bB5e -m5f11LaG19YTjWrrh5Z0LNPEqHNVw2vzU6PaOqb5r/tcg1nHqHNf47X5r1Enc2v/tdRB63L9NxdS -bT3Sx0/r/16Kb9H6U1RItfUQ8RW3wPcwed9WYNJ9R6Otm1rSX3+fDs/bCmlEp6//XKdXhx+xFVJ9 -vCw6ekCH17470uhkGVOQ9Mvat3V47X6n0RKdvr7/CYrPnWHnC6lTN+D07ad0+IW+11mo/b/Q4Ufu -LqSSrn19PPE7F9wLaOMr9/3Oilvr6+NPXh/m4bX7/8gXxL/MqLHP3c+176MoXuuYSYfT4hhl1P7r -9wPH6lTa8zntv6rD5yZs/e/3X0v/TGW5+UnxlgXw+vXnX24hy8dv/hz8pQXwOym+USfXj5+Cvuel -Jyh+9nPa/x+sJOndADgAAA==` - -// nolint:lll -var usrLibVlcPluginsVideoFilterLibEdgedetectionPluginSo = `H4sICN6c+VwAA2xpYmVkZ2VkZXRlY3Rpb25fcGx1Z2luLnNvAO0bW3Ab1XXXr8iJvXKZQE2AepuK -1qFElhI72E0MXnttr6nyILFDgICyltbWNrLkSqvECTCYOqYVRuCWlkn7Q2aYtintTMO0zYQUqNOE -JG2nbSidAfoY3AfF5lEClCSQJttz7t4r726iwke/Ojoz0rnnec8996G7unfv6Qx1lfA8x6CUu55D -arrGotsofzSYVwFeM1cJ34u4y4luGVcYTsxzYo76RbtyG+3GH/M4sd2O1CdSvgv3ljmx3a4CPrGl -Fh1rdeI9JRbeW+K0K6F2I9RupNWJuRInZuGW0U8z5buxj3NiFu66V4woltsWW7Qb93JOzOxuArsK -7qMDS/d6Wl+hvOwucWI2UtBmIYfjheO61/Rxj2p/vkoJLB7nv3H+2FDTSMtlX+35IUflXm4u/xw/ -jrkiYXtpHM9/e+WSSaH51z2PP3R1oXgPwOeSAu0QL8LXC+j/pgD/6QL8Vwvw+QL1niug/8UC/G8V -4PcV4F9boN4vF9Dvgs/ii/BX8qhfxXG1Fs3GbTXlBy6z6B/TDq+j/JMfd+pz4fDgUDIRThtqygiH -uXBP7+pwVEtpg3ra0FK9qzviyYTWq/bHNUt2cUk4MqKGB/SEGtd3atyAHgeNcCSm6omwrMU1Q+O2 -xSPhUHLQKVujbd+oR7Wkk7teS2uGkyUND2uJaFcqObTBSOkJcJPSsFoIO7IVdLaGB1Q97rQhnrsI -hxvWI0YmpWGF6KQrmRpSjTx3PYSopq0YtYSR2hEOLw8HwoEBGyeSHN6R0gdjxkVkcT2iJdJaXhLX -+0EYSaY0fzrpb0E6gqUVWNKig1oUMhIxdMj7cDwzqCdAyHWHeto7wsv8jfnSMn8TN7dAlV4wBkpg -FPN0RvNAafzcurBQ16vR5n7KyyzSK9Hi63Q8sPWBjYPJj1l42MVfR/nshyQ/big9fYOFK2yRIszY -+PNs/JM2fpWNf8bGF2z8Scqfx82tXQi7bXx7ZvbY+Pbftb02frmNv8/Gt6+/B2x8j40/ZeNX2vjH -bfz5Nv4JG3+Bjf+ijV/NFaEIRShCEYpQhCJ8dFDG3vAoE+V/boDi+JRRYp5Qxo54DuflZtOtIDKv -3gzf3ro2KCEdQ9HstAlw9XqkcWs1e4LQNyKNW7nZKUK3I41brNl9hP4c0ri1mt1D6GVI45ZqdpLQ -1yCNW6nZUUIvRhq3ObPDhF6ENG6pZrcQ+hKkcSs1uy6vPzCZb1+u9ZPYtlw52iktZ4xLobn/8FvN -rTSnvXWjqHeYYtB/G4W5ptcQLTmvHDpfqmRPKodmblD4o8pz542F4OAn1IHHnB7w1slz9qOtT4CI -yzT0KWOtj2BRyb5iVCkTrV8DYuZ2iHAmBl9Hy7NA87cfdtU/eycIB/zeul0k/Emrf+4JzJQCPzhF -OmfAJu/bYGk8OlmCVb1sPrybFJ7F6C5BdSU7cxtWs2uuv3PlVwlEWzrwXeCtU3KX+mCTH5wK5arP -erH11X+HTeUz2Iszi86b5qZbwQs6A0+22r11XN8z2PUblOxZ8FwfVSbKrq4njZZ9tRBu9q9Q2SYI -Xcmu8iHTQ/wp2Y4ABOjd9QRJebpZyR5Tsu/M+FE2cWcA0v8EBDBz/pxpZp8dO2MaCmX/ENmvMrZ3 -10+Jg0tfARR8U8k+vAVbPxHy1UaV5VUklOyMd/xHpOVH6iF8KwFNb4AfaO2nobXSgREQbwQvdV5M -AmkRJAey4MXaLj9nBTUzeg4z8foey0VrtjqvfTPo/q2KWIHT5wR0OmRF9isgnsaBOfPcv6mfA1C4 -5Y7ZPkwLcVV9hxXNbmJ4LxjeNDuEJs+ApjPf0s3SRqlP6u1Tsucg6++Cg01kfIyhdwia5PgRQsi+ -GpLkMpIT4E/sOwJeJiafwrQYPjnb62scP+V98Flow/jvvA8+DTj7ghL5pZzrMsde40+Du5BP6Qye -6sn+PnuXr+2Y7AuQUQSFZizIUAn0a6+vlo5feWKzLxCCDvDgPypcaCLuq5En7vI19kT+2DNx7z7C -C/maV5fmsNwJ1XfGfW2d/Fklt4Hvyf5BzoKD4KnOoBmKvIme61//LLQ7hPFjx9ZL0MiJXl9Nz8Qo -8ZarCipjR/lQ6f1ISrmqhu5c2eey70BuIu/3RI7L3v1lHvi6slnOviBnD51+if+FvOQlucXw+Qwv -hBaQeHN1xOwCveZQblVAXnKoc8mLYz/nu737VzVIp1+WWv7VnbtyRM6V1Ule+ferI2eILv8CVNQh -5zZfyq2OnO3CGjpzqy5RIi+Aucy/tDryMjBX1YJuTXuuaofET0k/MyFG/pCcPSZlf3XIrJMOna2T -gmfk4EvSkjMQlcQfl+49g0qd3rXHOr/CKysNyO34OZgYIWi1IkMKGiVsL9/ra5b5kK8NWyJ7x79E -5s5d0Pt7P8AS9v7YkU233S5tlm6X7pDChy3p/R9Yww5n51swE+YvIOP0U1U49h7HmQAjcV4VWQW8 -8ChKBvjMwg/I2N8r9QZ/B2PvveApMufBw9gb4gXTPgf9j2OE/JtgPg+DcHwrj7Pl4Dwex6IBYqy8 -+oNKJv88T2o8DYyDlTYlS7rUkr6HUs8FUq8ljUG0B+dfIH2bjJKoryZ46skKEuRbq7PTB3kS2pP4 -y3SQryFstKsJTpnPB0+BxpPziMiyQO6TDfzFdKG51WsxsBJX1ZmdwV8cLHUztVCu/Cj2Qa66Ca3K -+Lw/D1GQQaHcUrgcFcpdCt5vTHn3T51+EWcBLnMLSeozC6G3Reztw/g7fQCXnNf/NHPXGccKMgnL -8b2V5OeG/h7kyr+HNFmIHL/8RShCEYpQhCIU4f8V/P4GPRGJZ6JaA55HWQdg/hgXUROfMcRBzRCT -GWM4Y4j0rIvbhqdiVK/BcRblj3Bq9AuZtHFnWgVVFXmtgbu5QTWTTutqoj+eSd0Z1bbpliR4N+cw -5zqBEvOkSCoSrYpcMm7tsJbgOuLJtMba0ZHMxKNiImmIajyejKiGRk1Fcphnl5OjQCY1kgX1mJ8h -bSiZ2iEOJFNiQtsuDqTUIY2TSSxpEZuQFvWEaMQ0SySq4D2mD8bieNqXRsEQKmyP6Ybmn4uXHgeK -9R1LiDE5bgxJa8SNoQ6oM6qr4nBc3QGRQc60eHJYS6VhI2sdFUbFTCKK4YMhBD+UFpMDhOhe0yeG -tHQaZN1aQkupcXFdpj+uR0Rqea24DRxhgpf5gyK0Ka5ij7O46AnoGm37Otrh9ASUpJxzdRk8y1xR -uhLvIuAfMNNvmuYw4Ml/muZuwFveMk18JhsFfBydnzRN3LwPv22avYBPvmuaeL687z141AU8Bfjf -9JBuIY2H37me40dq+Cuq5nkmeeu+xJXwib1p+eaEmi6h9kbvgu2eUe6GRSuvWe5bzOxlrBL07Odv -yN8Mn70QI6lDFmrWCh5uFRQN+OwDfgr5klBzX4kk1I6VyoK4eb5QC4qS4OlZIAv1NwmBfqFeEsRu -oVYibE4Bkx9jfWDfzzP7LrTvFtpK9sxnmhI4UPqEdYOCIgltXYKyVliXIuWe+UTQuWAzaTjECvka -xf9g2oWah0rahdoHS9sFMVfWLtQ/UC4JgfsqZKF5bF63MMmXXlUyX2gGniTUgw7ogk071rZG2M3f -KuzhbwEsCZhCDh/ht0Ch8R0r/+j/gZIOofa+0i5BHCu7UWgrvZ+fL4gSCRrcdC7AJzk8+516dy6f -yNsLvKfedea4CEUoQhGKUIQiFKEIc/fj2H04dhetj3fSGi2wO2xJSufvutELeuzOl4fe37yc0uxe -3hVMTvEiitn9vCtd8vfOm0nEW+glPHbHrY1egmN32yapnN1R20PjY3fTaDj5O3Uj9J4au9t3kmK2 -v2d3++i1Um66wslvrnDGuZdidneO1fcJV3vOmlZ7WF7PU9pD/Zku+UlKT9OGv0/pC29N/m8gf3/c -BSto/3ZRvJHiAYq3UXwfxY9Q/B2K91N8lF3k/DBos1BDJp1qiOv9DVGtPzPY4I9u39kw0rwivKJx -aVxPZEaWDiYy+A/BUuvC6dJ+Na35iS53/A+Djye3tlY+tfOw7/3HhvnQ9//Sza1oikaD0cbm5YFl -y5qbAsGWpgEtct2K6PKm65qXtwRXtAQbW9QB6gHgB/I713H+dCxtpAy1n/PDE7jmhzr9/Rk9Hl2q -RzlCxdR0jPNHdyTSO4YsbKQsCX2qdRBhkKW0uIqKtDQcNzi/ntDhG4r+wSQUDG0EvgeAC0rJqGqo -nF+LhcnTfDgWTc1RlmlYTaXUHZYFK0MN6pAegQIx70+nSSRh0kA1bkASt9o4hPxfAM47nCtsfhV6 -z4CB+10NnDenYKwzezb/GQ5QPpsH7ue8ehoDs2frA8P7+Ll6eZs9m7cB6pvZs/WGYba+MHCRHJ5C -m7b42fxlmLWfxe96XYPr5Ky1gdFsfWCYtd8dP4NeKsvnv8KJ2Xrlzh9r/x3Uvp3FX+HEbL1E+4UX -sY9xtncrEFzv0bB1lYG7/yMue7HGife59N2v6yRc9idqnNidL48Lb3fZs/eOGK63X/621cvgbpc9 -+z1kuNKl727/GLXP3z0Xnfj6Eqe+u/4Jl32h93UK1f9Nl/3uxU78lGvAu/P5GGftFdj4yr+/s/Ti -+u7840s6Xps92x8Mf0R7vH9TYbPPvx9F7VnDylx2LI/4XxvPXbhfGGmw8Fc+pP4jLvv8C2uB/x4/ -g19SXn5+UntPAXv3+vPbi/Ds9ms/xP4vBew3UftGF989fhxtt8ED1P6ki++u6z9zoZ2wADgAAA==` - -var usrBinGs = `H4sICBWArF4AA2dzAO1be1hTV7Y/Ca8gGOKDluIrWuj4IOEtMIoQ5BE6+EBAbYEJIS8yxoDJQZCq -pYJotODj+qq1U9Q+rO14xWGuOloHBsTW1hEf17FWK462E9+MIlIVMnufs3c42UM+O/f2j3u/L+v7 -kl/Wb6+19jp777PZh7P3mykZqXwej8LiRk2joNY5lNUTER8QYzcBXCw1GHyPoUZTnkB359iReJHv -iAJ7PaxfgBuKT+BIyhF5HHSnnEuBtyNSon4/D45OYrCHI3L9mPrEiCeRTyDHD7ZNmITVw+Id8TzR -LtiPj/zkyE8e74hkfbg93dEnFrUfiWTapN98ZEdiMuWIuO2zvqfV/5P6ZiO/EFRAYjbliLi+TODn -Sf10wd07B9XnrB/a+Y6Ix1moQV84OSrUoJYY9MbSckl57GTJ5CipuVgaYc8L1gGHS9rMHNgdTZBz -4+Q9HOmwfO7jk0+mebwwYs3coA1/+sPylCNj6+7iGDxkQyF73MU4H/z5dyQPfIYOwI+g+vuEK+6g -gnED8F87iePrJM5xJ/ZW8BkyAH/fCf+WkzibnPDXnfB7nfDDeAPzmU7sFznh4bQoHoDvdWJ/hRr4 -erud2FNgHOrMcNzFUQqFbmGxUWGmlSZaoaAU6dkzFGqNSaPTm2mNKXvGdEOxUZOtLDRo2LIBS3Rm -ZYleoSnX0+in3qinFWV6ukihNOnMiDRraFCPWl+MdKOmDBiCmo0qHEKtMWhoDcmaSmGCJr1RBzNX -wcQnU1qtodRcBPIHpqoFClXRAoVWqTdQsAYjZdIo1fBncSkNQWMyUVq9QWMsprRlJj0NLkahKlcq -tHqj0qCvgCqMjJphoRJESMtIT5quiJBG2X9FSKNxA/LRPcVH9xWf4k6imOUBPEL1zxvD9frB8G4M -5rFcaaDeG9qHohsRzx94XmsfxmIswa9HfCLBY70pgUVPyvG+/4LDu3H4dg7vweEvcngvDt/B4QUc -3srhB3P4Tg4v4vA9HJ47TgsQD+vkzlFFHJ6bfwmH5/4dL+fw3Hm+ksN7c/jVHH4Qh1/P4X04/DYO -78vh6zm8kMPv4fB+HH4/h+fex4c4/DDKJS5xiUtc4pL/2/LQb/QTedUdgXytR3MoeMxc2UTzbe3y -qlZBC1Nui54I6Ae24EkA/MYw9kWw4MHNazabbT2j8xj9jF3nM3qzXXdj9Aa77s7oO+26B6NvsOue -jP6WXfdi9EV2XcDoSrvuzeiZdn0Qo8vsug+jh2MdXI1GCq8mmb1+oOcTeg6hzyD0FEKPJ/RoQpcQ -ejChjyJ0f0IfzNVlOemWR+HdsuwcSy/oO7FavtY9eDy4RrklOSggvEleuzRIJD+RHMQs4Kzp4Lpl -llbQs0Wyqmc76NfB93vLUsH3Rr/qT4EF+PXuIWgqO2oD38l+yR3ytSCSWh7JxvVb2QSjV7WKky0d -+bI8WX6L35hqZnykrLxxiHVK6bj9pewoHAq3m+VrYQa1g5cDVV7rf5CB6E4AVh+QDVNsuZdhuWU1 -9NlsluMgNxlDWi8DfeVdekxV77t0IEPJLHesWwB7MwF4poTfuFkAsKrn3dJJrEcFLMuAUWujp4Gl -LKo7I8g3/Kx1GRP+9m7wOWAVAwW2X/jddMuZX8stf5NX3eicnZ1R6+HJhwkOXsfkGb8N2Nn8T4ZQ -VNd6oF4LgazHJQhxPbQ/uFVqQthbxdvW4TemErZDC0Jg38DYR++FMKFPbumUN99LkDf3uMl5bfIz -ffRwEKAUBRDYOrRMv2J/mF9lvB4UU6WTcuRV8eNCmI79nvYFoesmwP7stdmsapBim4cMFPJgZzj4 -3ywDhRxdli23PJarzh6DnZOVbjnP3vCjOibB1vKxru6FXTLFKoII7nzQybmyvJb+8ebEfxvylyP/ -9mcD++fILY+yLOfQNLOC8Yq1joNeqnNyyz3LCetx5Jqb36KV4rHlNyaRrX9eRm18FRiIsrnplgvM -6JdlyyzPcuS1EhrQWRkT4H0gsIY+BVGae93oMeHfouvPsDzIsNxLtvxdZht+VV7VwpPHXSm9xVYF -B7Ls1zJFi7a/TlhfC3detM+ELnGJS1ziEpe4xCUuccn/f+E5vHWgKPMSM61ZqNaraHEo84ZFrNPQ -Yk25RjWI4o1wmzKbYv8P33XfZoMr5tZOm+0zgNX/sNmmgueD1gc2mwHojQ9ttlZoB/AOwKgum204 -KKcB0gBvAIQPi8NxHhVzKF65iDfC10uwHvHw3f9sEC8UGiR7MU+zQTAW+BSB+tshIRSlCgNe9fMp -E1RSCYG/nBgZxLzGhP7wHagI5NfEuT7oT4OPFcSdAolUoehtfoYwoMotTSjOFQYkC0VJQgFjtwOW -P3y+HbzOAGDHvBlOEYpq+MnQLlUoLhAGyIQiGbCDOcN2SAT5JPLYeLX8NGHA224yobjGPVWYyF8w -SCgGnklM5HT27QV87x4E7HtAvhqUxzp+ujCgzi1FKK51TxGOf9sjWRhW4ykXxlZ5pQkTjcJYmTBM -JhyfJBQn4WjMuxwDiCMGeXLfG7nEJS5xiUtc4hKXuMQlLnEueJ8Xua+Luz+Zi0cQ2vccoU1VeM9V -gD+LLyEd7ysbgXS8ZysQId5fNpIof9RnK4ZYjzZb4TV+PfqB91Q1oXK8Z2oKShTvlQpA6E85Ct7D -JUf7nPBToxj54+c4vPfsRYSJAkc+1ssxbwHyx3u7AgjE8tTGXh8PufYhXYDi2frLGelE+mV04T8i -nbsX7ecU+/5sQsJQfycinI2wAGEJwkqE6xHWI9yPsAlhO8IO7ua8/6XABmXCgcZLkwXxIpXh/fvi -0UDwdLCZyIftuB9tgNuIOh/3OYrjoVOpxHHSSGm4OCIsIiwsMjyGcx+wcTzhXsIhgPV05L3g+PJw -4MYy/T7Y0S511pzs9NTX2OH4gkPZJGYv6Pz5CllWVgqwmjUzi+LsFWT902bNoyamUJw9eyyv0ipK -TMW0RkXri42wgYjYxQv1tEJrUi7UKEqK9UZaY6I4+wRZG7TZ1aA0F3H2CrLXwpaZNEqDXmekUF4O -fWCOjYnltDE+jzFQX4lR8v6or/4lVgkbq2OYY3/pzJI4aUSMJEqqVUWGS8tjJysmR0nVmsJSHRX1 -lXpDb0zF/Ncpyv2HC42pfGosD7YBHfr97g7Rm27l+VTChpTqonjLR5fWXKuKmPmN94ioa163o4/t -LyzL2jTh1UDq6pbh3fcVX2TunJra8Jtg39OR9eZGad77Kve4UVcTzp3ZnWfKmfFoSEnVVcrPZj5x -ZVzDnOXRR9/5JHJFcMabEamTlJc/WD/rv7+OO8dXXDv5Te97USdTcpY+2/rRnNytf8sWvBKzctTa -L64fqFOofrVgY0WHTbInZWrAJt658lXDao2FEQtqP8h3P3n9jb3fxVlrH3m/n3GielXw8YOiuVny -Ze9sO/rj9gnn6zNSbKcG7d2jCvIKnBgn+qjZuHyxOX63MXH1Ed+03E8zP12w7sGDu4sbpjUH3muP -aP3h6JnZTwftb31858jHq3ji00/mHDPSuw42j59aodi+aLSvpqs1+a05tacu/seLQ86ob3YXvLum -u77s+4wlbRd2Dd4xq7tn9l+o+V9/s2J3ZsgoekLjs8N3effvFug2q76QZDYtseaG3bkc9KuRgpj7 -GxtDg2cmbGp/T/vK0zO/7Ww/Hpz+4ZUPi1aYCoYW9X5i/mvTuMc7QmtHLL2weuYbb2oFv7t7tPHh -gd+P3vvZ6gxj/asvX5w5b9jo2M3/Jan+0ByvfqluetyIxadKRmWc6HvSPffj8LZjqV2viDY2nL5x -dV5q2++HtN2rWHlrTM/iXOpa8K6dV1RPL6176fMp1zdPEhzx8NNurf7Yr9acuyexfsuwWRv//ttt -ASXdFTWax1n3vjkoyZzyuengqOCxt8R+Q/7aLIuorR+rrb75xh/vFH57NHZe2beK7ZEWYfx56fun -Rn+1653Uc4IdhoShPg3vSZN8NJu9D4R3ZR0J23X4wbF5p2v+kB999LaotW3TuL668xL3hhe/89+q -ul8d6ltT/0f34A8Svqzr2l1nNLz+wxr37Cd7foirOT3FUqpcdPidjaY/l4ecFY08dfCc4NThyy+s -7bmm+9Ohz5tSmhuX1HxZsXLavp2qw7eX59+6uLNQdz17abAw5M4v94WveZQ5TdI16TfaJ3tm1lce -6T1esTsu7y/RL9dPLZPXX8lNfjbhk/yvuuoUQcl1s4+HrGy4vv2JT946UcyrXpcu6EWbewdNiPzF -2doh372w2rpwQ0Pj9n37mnN1jaUVtySPHn7749j5v3hpetDVjTl5bebsQLeh+cVbc8t61X2ZE1uS -xpaFFM7wSHy5qqfycsy6NP95dd4rvnRf7qcPCZz/3a5Fxot514UVl7bcVGn3/efZ6gU9UTp5X1Bi -Q+rvIuK33Jh+b1jOKqpw66nIPcsODKF4m7wqx4J7si375YZW3VNmEnR/7XVKai4y0yZaWUhJmbmp -hJIawWQm1RlLpWBaK9GY6CUcqrBUb1BL9GpEyZLSJbRSRzFlRXD2kqqXGM1LFrJIm9iSxRqTGU6O -XEUBykwagxIaol8lBhpmoQff4KfUrFFRUlpTDlQtYIFRsVpJKymppgjNp0VqU7/GuiqUJpNyCeuB -f0MvWAMIwOSlXKgHkXXFNFtESQvNZqr/8qRKmjbpC0tpDcsqmGnOoDcu4KiM488jPuz0a18nOTvf -hoU83wOnai+Ov7PzVVgEhP4K4U+e6woi7MkzdXGE/1I3RyTrJ/3TwKcbrLmwP16X1hPXj//uk/nD -czg+nPrxuhVjB2owfO4A++N142uU41kqvA7GOIVocLL94TiwcfLH60iM44n8yWOA8I96H8cfr1Mx -hlED549lGcW2qb3/vRwRr5vJ9sPXvwr5JyEdr8Mxijn+AQP4b6D6zzAyQpyXxM8nWMj+f5vwF4sc -sZ2wJ49lbiX8O0WO2PQc/3rCH6+jMGqIBwFyOf0R4Y+f0zAOJuzJ6/+Mcrz/yQORIU7yx9JI+Ds7 -J+ms/j8T/pViAokBT44/eO4PnofBzWQ/NykZ2F5A4EWKXQ9jf/zcGvsT/W+g/LE/fs5OfI4/ln9Q -jmen7OdokT9uGPxciv1xPzwh6sfPvfJQFg89J38b4W8/EI1ufLK/yPnHAz3o2s+TIn/3n+g/iOd4 -DkxQ4OjfQziQ8eD8MNCZ02fhqJzgSf9QJ/4j2eOz1CHi/iNtHdqOI+fRub5pz5m//wmIsRLy2D4A -AA==` - -func unzipBase64Buffer(buffer string) ([]byte, error) { - gzipped, err := base64.StdEncoding.DecodeString(buffer) - if err != nil { - return []byte{}, fmt.Errorf("failed to base64-decode buffer: %w", err) - } - unzipper, err := gzip.NewReader(bytes.NewBuffer(gzipped)) - if err != nil { - return []byte{}, fmt.Errorf("failed to create gzip reader: %w", err) - } - defer unzipper.Close() - finaldata, err := io.ReadAll(unzipper) - if err != nil { - return []byte{}, fmt.Errorf("failed to unzip the decoded buffer: %w", err) - } - return finaldata, nil -} - -func doConfigure(t testing.TB) string { - // Set up a CacheDirectory as it is needed by the interval cache - cacheDirectory, err := os.MkdirTemp("", "*_TestGetIntervalStructures") - if err != nil { - t.Fatalf("Failed to create temporary directory: %v", err) - } - - err = config.SetConfiguration(&config.Config{ - ProjectID: 42, - CacheDirectory: cacheDirectory, - SecretToken: "secret"}) - if err != nil { - t.Fatalf("Failed to set temporary config: %s", err) - } - - return cacheDirectory -} - -func doWriteExes(t testing.TB) []string { - testFiles := []string{usrLibVlcPluginsVideoFilterLibEdgedetectionPluginSo, - usrLibVlcPluginsVideoFilterLibinvertPluginSo, usrBinGs} - filenames := make([]string, len(testFiles)) - for idx, inData := range testFiles { - exeData, err := unzipBase64Buffer(inData) - if err != nil { - t.Fatalf("failed to unzip buffer: %v", err) - } - filename, err1 := libpf.WriteTempFile(exeData, "", "elf_") - if err1 != nil { - t.Errorf("Failure to write tempfile for executable 1 %v", err1) - } - filenames[idx] = filename - } - - return filenames -} - -// dummyCache satisfies the nativeunwind.IntervalCache interface but does not cache -// data. It is used to simulate a broken cache. -type dummyCache struct{ hasIntervals bool } - -// HasIntervals satisfies IntervalCache.HasHasIntervals. -func (d *dummyCache) HasIntervals(host.FileID) bool { return d.hasIntervals } - -// GetIntervalData satisfies IntervalCache.GetIntervalData. -func (*dummyCache) GetIntervalData(host.FileID, *sdtypes.IntervalData) error { - return fmt.Errorf("getIntervalData is not implemented for dummyCache") -} - -// SaveIntervalData satisfies IntervalCache.SaSaveIntervalData. -func (*dummyCache) SaveIntervalData(host.FileID, *sdtypes.IntervalData) error { - // To fake an successful write to the cache we need to return nil here. - return nil -} - -// GetCurrentCacheSize satisfies IntervalCache.GetCurrentCacheSize. -func (*dummyCache) GetCurrentCacheSize() (uint64, error) { - return 0, nil -} - -// GetAndResetHitMissCounters satisfies IntervalCache.GetAndResetHitMissCounters -func (*dummyCache) GetAndResetHitMissCounters() (hit, miss uint64) { - return 0, 0 -} - -// Make sure on compile time of the test that dummyCache satisfies nativeunwind.IntervalCache. -var _ nativeunwind.IntervalCache = &dummyCache{} - -func TestGetIntervalStructuresForFile(t *testing.T) { - cacheDirectory := doConfigure(t) - defer os.RemoveAll(cacheDirectory) - filenames := doWriteExes(t) - defer func() { - for _, filename := range filenames { - os.Remove(filename) - } - }() - - localCache, err := localintervalcache.New(2000) - if err != nil { - t.Fatalf("Failed to get local interval cache: %v", err) - } - - tests := map[string]struct { - cache nativeunwind.IntervalCache - }{ - // Cache without intervals simulates a test case where the cache of the - // local stack delta provider does not cache at all. - "Cache without intervals": {cache: &dummyCache{}}, - // Cache with intervals simulates a test case where the cache of the - // local stack delta provider has cached something, but the files are actually - // not there so we expect an error to be logged and the rest of execution to be continued. - "Cache with intervals": {cache: &dummyCache{true}}, - // Local cache simulates a test case where a regular local interval cache is used - // to cache interval data. - "Local cache": {cache: localCache}, - } - - for name, testcase := range tests { - name := name - testcase := testcase - t.Run(name, func(t *testing.T) { - sdp := New(testcase.cache) - for _, filename := range filenames { - fileID, err := host.CalculateID(filename) - if err != nil { - t.Fatalf("Failed to get FileID for %s: %v", filename, err) - } - elfRef := pfelf.NewReference(filename, pfelf.SystemOpener) - - var intervalData, intervalData2 sdtypes.IntervalData - err = sdp.GetIntervalStructuresForFile(fileID, elfRef, &intervalData) - if err != nil { - t.Errorf("Failed to get interval structures: %s", err) - } - if len(intervalData.Deltas) == 0 { - t.Fatalf("Failed to get delta arrays for %s", filename) - } - err = sdp.GetIntervalStructuresForFile(fileID, elfRef, &intervalData2) - if err != nil { - t.Errorf("Failed to get interval structures: %s", err) - } - if len(intervalData2.Deltas) == 0 { - t.Fatalf("Failed to get delta arrays for %s", filename) - } - if diff := cmp.Diff(intervalData, intervalData2); diff != "" { - t.Errorf("Different interval data for same file:\n%s", diff) - } - elfRef.Close() - } - }) - } -} - -func BenchmarkLocalStackDeltaProvider_GetIntervalStructuresForFile(b *testing.B) { - b.StopTimer() - cacheDirectory := doConfigure(b) - defer os.RemoveAll(cacheDirectory) - // Try to extract the Go binary running this test - // this is not going to be comparable between different hosts/go runtimes but - // it can serve as a real-world case for reading real ELF files - underTest := ownGoBinary(b) - // We use dummycache to skip the HasInterval check and focus on the Extract elf method - sdp := New(&dummyCache{}) - for i := 0; i < b.N; i++ { - var intervalData sdtypes.IntervalData - fileID, err := host.CalculateID(underTest) - if err != nil { - b.Fatalf("failed to calculate fileID: %v", err) - } - b.StartTimer() - elfRef := pfelf.NewReference(underTest, pfelf.SystemOpener) - err = sdp.GetIntervalStructuresForFile(fileID, elfRef, &intervalData) - elfRef.Close() - b.StopTimer() - if err != nil { - b.Fatalf("failed to get interval structures: %v", err) - } - } -} - -func ownGoBinary(tb testing.TB) string { - executable, err := os.Readlink("/proc/self/exe") - if err != nil { - tb.Fatalf("can't read own process executable symlink") - } - return executable -} diff --git a/libpf/nopanicslicereader/nopanicslicereader_test.go b/libpf/nopanicslicereader/nopanicslicereader_test.go deleted file mode 100644 index e39bf78f..00000000 --- a/libpf/nopanicslicereader/nopanicslicereader_test.go +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -package nopanicslicereader - -import ( - "reflect" - "testing" - - "github.com/elastic/otel-profiling-agent/libpf" -) - -func assertEqual(t *testing.T, a, b any) { - if a == b { - return - } - t.Errorf("Received %v (type %v), expected %v (type %v)", - a, reflect.TypeOf(a), b, reflect.TypeOf(b)) -} - -func TestSliceReader(t *testing.T) { - data := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} - assertEqual(t, Uint16(data, 2), uint16(0x0403)) - assertEqual(t, Uint16(data, 7), uint16(0)) - assertEqual(t, Uint32(data, 0), uint32(0x04030201)) - assertEqual(t, Uint32(data, 100), uint32(0)) - assertEqual(t, Uint64(data, 0), uint64(0x0807060504030201)) - assertEqual(t, Uint64(data, 1), uint64(0)) - assertEqual(t, Ptr(data, 0), libpf.Address(0x0807060504030201)) - assertEqual(t, PtrDiff32(data, 4), libpf.Address(0x08070605)) -} diff --git a/libpf/periodiccaller/periodiccaller_test.go b/libpf/periodiccaller/periodiccaller_test.go deleted file mode 100644 index ea750b05..00000000 --- a/libpf/periodiccaller/periodiccaller_test.go +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -// Package periodiccaller allows periodic calls of functions. -package periodiccaller - -import ( - "context" - "sync/atomic" - "testing" - "time" - - "go.uber.org/goleak" -) - -// TestPeriodicCaller tests periodic calling for all exported periodiccaller functions -func TestPeriodicCaller(t *testing.T) { - // goroutine leak detector, see https://github.com/uber-go/goleak - defer goleak.VerifyNone(t) - interval := 10 * time.Millisecond - trigger := make(chan bool) - - tests := map[string]func(context.Context, func()) func(){ - "Start": func(ctx context.Context, cb func()) func() { - return Start(ctx, interval, cb) - }, - "StartWithJitter": func(ctx context.Context, cb func()) func() { - return StartWithJitter(ctx, interval, 0.2, cb) - }, - "StartWithManualTrigger": func(ctx context.Context, cb func()) func() { - return StartWithManualTrigger(ctx, interval, trigger, func(bool) { cb() }) - }, - } - - for name, testFunc := range tests { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - - done := make(chan bool) - var counter atomic.Int32 - - stop := testFunc(ctx, func() { - result := counter.Load() - if result < 2 { - result = counter.Add(1) - if result == 2 { - // done after 2 calls - done <- true - } - } - }) - - // We expect the timer to stop after 2 calls to the callback function - select { - case <-done: - result := counter.Load() - if result != 2 { - t.Errorf("failure (%s) - expected to run callback exactly 2 times, it run %d times", - name, result) - } - case <-ctx.Done(): - // Timeout - t.Errorf("timeout (%s) - periodiccaller not working", name) - } - - cancel() - stop() - } -} - -// TestPeriodicCallerCancellation tests the cancellation functionality for all -// exported periodiccaller functions -func TestPeriodicCallerCancellation(t *testing.T) { - // goroutine leak detector, see https://github.com/uber-go/goleak - defer goleak.VerifyNone(t) - interval := 1 * time.Millisecond - trigger := make(chan bool) - - tests := map[string]func(context.Context, func()) func(){ - "Start": func(ctx context.Context, cb func()) func() { - return Start(ctx, interval, cb) - }, - "StartWithJitter": func(ctx context.Context, cb func()) func() { - return StartWithJitter(ctx, interval, 0.2, cb) - }, - "StartWithManualTrigger": func(ctx context.Context, cb func()) func() { - return StartWithManualTrigger(ctx, interval, trigger, func(bool) { cb() }) - }, - } - - for name, testFunc := range tests { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) - - executions := make(chan struct{}, 20) - stop := testFunc(ctx, func() { - executions <- struct{}{} - }) - - // wait until timeout occurred - <-ctx.Done() - - // give callback time to execute, if cancellation didn't work - time.Sleep(10 * time.Millisecond) - - if len(executions) == 0 { - t.Errorf("failure (%s) - periodiccaller never called", name) - } else if len(executions) > 11 { - t.Errorf("failure (%s) - cancellation not working", name) - } - - cancel() - stop() - } -} - -// TestPeriodicCallerManualTrigger tests periodic calling with manual trigger -func TestPeriodicCallerManualTrigger(t *testing.T) { - // goroutine leak detector, see https://github.com/uber-go/goleak - defer goleak.VerifyNone(t) - // Number of manual triggers - numTrigger := 5 - // This should be something larger than time taken to execute triggers - interval := 10 * time.Second - ctx, cancel := context.WithTimeout(context.Background(), interval) - defer cancel() - - var counter atomic.Int32 - trigger := make(chan bool) - done := make(chan bool) - - stop := StartWithManualTrigger(ctx, interval, trigger, func(manualTrigger bool) { - if !manualTrigger { - t.Errorf("failure - manualTrigger should be true") - } - n := counter.Add(1) - if n == int32(numTrigger) { - done <- true - } - }) - defer stop() - - for i := 0; i < numTrigger; i++ { - trigger <- true - } - <-done - - numExec := counter.Load() - if int(numExec) != numTrigger { - t.Errorf("failure - expected to run callback exactly %d times, it run %d times", - numTrigger, numExec) - } -} diff --git a/libpf/pfelf/addressmapper_test.go b/libpf/pfelf/addressmapper_test.go index 1c5c5aa8..e9293541 100644 --- a/libpf/pfelf/addressmapper_test.go +++ b/libpf/pfelf/addressmapper_test.go @@ -12,6 +12,7 @@ import ( "github.com/elastic/otel-profiling-agent/testsupport" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func assertFileToVA(t *testing.T, mapper AddressMapper, fileAddress, virtualAddress uint64) { @@ -22,11 +23,11 @@ func assertFileToVA(t *testing.T, mapper AddressMapper, fileAddress, virtualAddr func TestAddressMapper(t *testing.T) { debugExePath, err := testsupport.WriteTestExecutable2() - assert.NoError(t, err) + require.NoError(t, err) defer os.Remove(debugExePath) ef, err := Open(debugExePath) - assert.NoError(t, err) + require.NoError(t, err) mapper := ef.GetAddressMapper() assertFileToVA(t, mapper, 0x1000, 0x401000) diff --git a/libpf/pfelf/data b/libpf/pfelf/data deleted file mode 100644 index cd729e36..00000000 Binary files a/libpf/pfelf/data and /dev/null differ diff --git a/libpf/pfelf/file.go b/libpf/pfelf/file.go index 11c936fc..659a354e 100644 --- a/libpf/pfelf/file.go +++ b/libpf/pfelf/file.go @@ -37,7 +37,7 @@ import ( "github.com/elastic/otel-profiling-agent/libpf" "github.com/elastic/otel-profiling-agent/libpf/readatbuf" - "github.com/elastic/otel-profiling-agent/libpf/remotememory" + "github.com/elastic/otel-profiling-agent/remotememory" ) const ( @@ -486,6 +486,95 @@ func (f *File) GetBuildID() (string, error) { return getBuildIDFromNotes(data) } +// TLSDescriptors returns a map of all TLS descriptor symbol -> address +// mappings in the executable. +func (f *File) TLSDescriptors() (map[string]libpf.Address, error) { + var err error + if err = f.LoadSections(); err != nil { + return nil, err + } + + descs := make(map[string]libpf.Address) + for i := range f.Sections { + section := &f.Sections[i] + // NOTE: SHT_REL is not relevant for the archs that we care about + if section.Type == elf.SHT_RELA { // nolint:misspell + if err = f.insertTLSDescriptorsForSection(descs, section); err != nil { + return nil, err + } + } + } + + return descs, nil +} + +func (f *File) insertTLSDescriptorsForSection(descs map[string]libpf.Address, + relaSection *Section) error { + if relaSection.Link > uint32(len(f.Sections)) { + return errors.New("rela section link is out-of-bounds") // nolint:misspell + } + if relaSection.Link == 0 { + return errors.New("rela section link is empty") // nolint:misspell + } + if relaSection.Size > maxBytesLargeSection { + return fmt.Errorf("relocation section too big (%d bytes)", relaSection.Size) + } + if relaSection.Size%uint64(unsafe.Sizeof(elf.Rela64{})) != 0 { + return errors.New("relocation section size isn't multiple of rela64 struct") + } + + symtabSection := &f.Sections[relaSection.Link] + if symtabSection.Link > uint32(len(f.Sections)) { + return errors.New("symtab link is out-of-bounds") + } + if symtabSection.Size%uint64(unsafe.Sizeof(elf.Sym64{})) != 0 { + return errors.New("symbol section size isn't multiple of sym64 struct") + } + + strtabSection := &f.Sections[symtabSection.Link] + if strtabSection.Size > maxBytesLargeSection { + return fmt.Errorf("string table too big (%d bytes)", strtabSection.Size) + } + + strtabData, err := strtabSection.Data(uint(strtabSection.Size)) + if err != nil { + return fmt.Errorf("failed to read string table: %w", err) + } + + relaData, err := relaSection.Data(uint(relaSection.Size)) + if err != nil { + return fmt.Errorf("failed to read relocation section: %w", err) + } + + relaSz := int(unsafe.Sizeof(elf.Rela64{})) + for i := 0; i < len(relaData); i += relaSz { + rela := (*elf.Rela64)(unsafe.Pointer(&relaData[i])) // nolint:misspell + + ty := rela.Info & 0xffff + if !(f.Machine == elf.EM_AARCH64 && elf.R_AARCH64(ty) == elf.R_AARCH64_TLSDESC) && + !(f.Machine == elf.EM_X86_64 && elf.R_X86_64(ty) == elf.R_X86_64_TLSDESC) { + continue + } + + sym := elf.Sym64{} + symSz := int64(unsafe.Sizeof(sym)) + symNo := int64(rela.Info >> 32) + n, err := symtabSection.ReadAt(libpf.SliceFrom(&sym), symNo*symSz) + if err != nil || n != int(symSz) { + return fmt.Errorf("failed to read relocation symbol: %w", err) + } + + symStr, ok := getString(strtabData, int(sym.Name)) + if !ok { + return errors.New("failed to get relocation name string") + } + + descs[symStr] = libpf.Address(rela.Off) + } + + return nil +} + // GetDebugLink reads and parses the .gnu_debuglink section. // If the link does not exist then ErrNoDebugLink is returned. func (f *File) GetDebugLink() (linkName string, crc int32, err error) { @@ -612,7 +701,7 @@ func (ph *Prog) DataReader(maxSize uint) (io.Reader, error) { // Data loads the whole section header referenced data, and returns it as a slice. func (sh *Section) Data(maxSize uint) ([]byte, error) { if sh.Flags&elf.SHF_COMPRESSED != 0 { - return nil, fmt.Errorf("compressed sections not supported") + return nil, errors.New("compressed sections not supported") } if sh.FileSize > uint64(maxSize) { return nil, fmt.Errorf("section size %d is too large", sh.FileSize) @@ -684,7 +773,7 @@ func calcSysvHash(s libpf.SymbolName) uint32 { // LookupSymbol searches for a given symbol in the ELF func (f *File) LookupSymbol(symbol libpf.SymbolName) (*libpf.Symbol, error) { - if f.gnuHash.addr != 0 { + if f.gnuHash.addr != 0 { //nolint: gocritic // Standard DT_GNU_HASH lookup code follows. Please check the DT_GNU_HASH // blog link (on top of this file) for details how this works. hdr := &f.gnuHash.header diff --git a/libpf/pfelf/file_test.go b/libpf/pfelf/file_test.go index 1f9d1a9a..43fe929b 100644 --- a/libpf/pfelf/file_test.go +++ b/libpf/pfelf/file_test.go @@ -10,22 +10,24 @@ import ( "os" "testing" - "github.com/elastic/otel-profiling-agent/libpf" "github.com/elastic/otel-profiling-agent/testsupport" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/otel-profiling-agent/libpf" ) func getPFELF(path string, t *testing.T) *File { file, err := Open(path) - assert.Nil(t, err) + assert.NoError(t, err) return file } func TestGnuHash(t *testing.T) { - assert.Equal(t, calcGNUHash(""), uint32(0x00001505)) - assert.Equal(t, calcGNUHash("printf"), uint32(0x156b2bb8)) - assert.Equal(t, calcGNUHash("exit"), uint32(0x7c967e3f)) - assert.Equal(t, calcGNUHash("syscall"), uint32(0xbac212a0)) + assert.Equal(t, uint32(0x00001505), calcGNUHash("")) + assert.Equal(t, uint32(0x156b2bb8), calcGNUHash("printf")) + assert.Equal(t, uint32(0x7c967e3f), calcGNUHash("exit")) + assert.Equal(t, uint32(0xbac212a0), calcGNUHash("syscall")) } func lookupSymbolAddress(ef *File, name libpf.SymbolName) libpf.SymbolValue { @@ -35,39 +37,33 @@ func lookupSymbolAddress(ef *File, name libpf.SymbolName) libpf.SymbolValue { func TestPFELFSymbols(t *testing.T) { exePath, err := testsupport.WriteSharedLibrary() - if err != nil { - t.Fatalf("Failed to write test executable: %v", err) - } + require.NoError(t, err) defer os.Remove(exePath) ef, err := Open(exePath) - if err != nil { - t.Fatalf("Failed to open test executable: %v", err) - } + require.NoError(t, err) defer ef.Close() // Test GNU hash lookup - assert.Equal(t, lookupSymbolAddress(ef, "func"), libpf.SymbolValue(0x1000)) - assert.Equal(t, lookupSymbolAddress(ef, "not_existent"), libpf.SymbolValueInvalid) + assert.Equal(t, libpf.SymbolValue(0x1000), lookupSymbolAddress(ef, "func")) + assert.Equal(t, libpf.SymbolValueInvalid, lookupSymbolAddress(ef, "not_existent")) // Test SYSV lookup ef.gnuHash.addr = 0 - assert.Equal(t, lookupSymbolAddress(ef, "func"), libpf.SymbolValue(0x1000)) - assert.Equal(t, lookupSymbolAddress(ef, "not_existent"), libpf.SymbolValueInvalid) + assert.Equal(t, libpf.SymbolValue(0x1000), lookupSymbolAddress(ef, "func")) + assert.Equal(t, libpf.SymbolValueInvalid, lookupSymbolAddress(ef, "not_existent")) } func TestPFELFSections(t *testing.T) { elfFile, err := Open("testdata/fixed-address") - if !assert.Nil(t, err) { - return - } + require.NoError(t, err) defer elfFile.Close() // The fixed-address test executable has a section named `.coffee_section` at address 0xC0FFEE sh := elfFile.Section(".coffee_section") if assert.NotNil(t, sh) { - assert.Equal(t, sh.Name, ".coffee_section") - assert.Equal(t, sh.Addr, uint64(0xC0FFEE)) + assert.Equal(t, ".coffee_section", sh.Name) + assert.Equal(t, uint64(0xC0FFEE), sh.Addr) // Try to find a section that does not exist sh = elfFile.Section(".tea_section") @@ -78,7 +74,7 @@ func TestPFELFSections(t *testing.T) { func testPFELFIsGolang(t *testing.T, filename string, isGoExpected bool) { ef := getPFELF(filename, t) defer ef.Close() - assert.Equal(t, ef.IsGolang(), isGoExpected) + assert.Equal(t, isGoExpected, ef.IsGolang()) } func TestPFELFIsGolang(t *testing.T) { diff --git a/libpf/pfelf/pfelf.go b/libpf/pfelf/pfelf.go index f73a6847..61f21675 100644 --- a/libpf/pfelf/pfelf.go +++ b/libpf/pfelf/pfelf.go @@ -15,175 +15,30 @@ import ( "encoding/hex" "errors" "fmt" - "hash/fnv" "io" "os" "regexp" "strings" - "github.com/minio/sha256-simd" - "github.com/elastic/otel-profiling-agent/libpf" ) -// ELF files start with \x7F followed by 'ELF' - \x7f\x45\x4c\x46 -var elfHeader = []byte{ - 0x7F, 0x45, 0x4C, 0x46, -} - -// IsELFReader checks if the first four bytes of the provided ReadSeeker match the ELF magic bytes, -// and returns true if so, or false otherwise. -// -// *** WARNING *** -// ANY CHANGE IN BEHAVIOR CAN EASILY BREAK OUR INFRASTRUCTURE, POSSIBLY MAKING THE ENTIRETY -// OF THE DEBUG INDEX OR FRAME METADATA WORTHLESS (BREAKING BACKWARDS COMPATIBILITY). -func IsELFReader(reader io.ReadSeeker) (bool, error) { - fileHeader := make([]byte, 4) - nbytes, err := reader.Read(fileHeader) - - // restore file position - if _, err2 := reader.Seek(-int64(nbytes), io.SeekCurrent); err2 != nil { - return false, fmt.Errorf("failed to rewind: %s", err2) - } - - if err != nil { - if err == io.EOF { - return false, nil - } - return false, fmt.Errorf("failed to read ELF header: %s", err) - } - - if bytes.Equal(elfHeader, fileHeader) { - return true, nil - } - - return false, nil -} - -// IsELF checks if the first four bytes of the provided file match the ELF magic bytes -// and returns true if so, or false otherwise. -func IsELF(filePath string) (bool, error) { - f, err := os.Open(filePath) - if err != nil { - return false, fmt.Errorf("failed to open %s: %s", filePath, err) - } - defer f.Close() - - isELF, err := IsELFReader(f) - if err != nil { - return false, fmt.Errorf("failed to read %s: %s", filePath, err) - } - - return isELF, nil -} - -// fileHashReader hashes the contents of the reader in order to generate a system-independent -// identifier. -// ELF files are partially hashed to save CPU cycles: only the first 4K and last 4K of the files -// are used for the hash, as they likely contain the program and section headers, respectively. -// -// *** WARNING *** -// ANY CHANGE IN BEHAVIOR CAN EASILY BREAK OUR INFRASTRUCTURE, POSSIBLY MAKING THE ENTIRETY -// OF THE DEBUG INDEX OR FRAME METADATA WORTHLESS (BREAKING BACKWARDS COMPATIBILITY). -func fileHashReader(reader io.ReadSeeker) ([]byte, error) { - isELF, err := IsELFReader(reader) - if err != nil { - return nil, err - } - h := sha256.New() - - if isELF { - // Hash algorithm: SHA256 of the following: - // 1) 4 KiB header: should cover the program headers, and usually the GNU Build ID (if - // present) plus other sections. - // 2) 4 KiB trailer: in practice, should cover the ELF section headers, as well as the - // contents of the debug link and other sections. - // 3) File length (8 bytes, big-endian). Just for paranoia: ELF files can be appended to - // without restrictions, so it feels a bit too easy to produce valid ELF files that would - // produce identical hashes using only 1) and 2). - - // 1) Hash header - _, err = io.Copy(h, io.LimitReader(reader, 4096)) - if err != nil { - return nil, fmt.Errorf("failed to hash file header: %v", err) - } - - var size int64 - size, err = reader.Seek(0, io.SeekEnd) - if err != nil { - return nil, fmt.Errorf("failed to seek end of file: %v", err) - } - - // 2) Hash trailer - // This will double-hash some data if the file is < 8192 bytes large. Better keep - // it simple since the logic is customer-facing. - tailBytes := min(size, 4096) - _, err = reader.Seek(-tailBytes, io.SeekEnd) - if err != nil { - return nil, fmt.Errorf("failed to seek file trailer: %v", err) +// SafeOpenELF opens the given ELF file in a safely way in that +// it recovers from panics inside elf.Open(). +// Under cirumstances we see fatal errors from inside the runtime, which +// are not recoverable, e.g. "fatal error: runtime: out of memory". +func SafeOpenELF(name string) (elfFile *elf.File, err error) { + defer func() { + // debug/elf has issues with malformed ELF files + if r := recover(); r != nil { + if elfFile != nil { + elfFile.Close() + elfFile = nil + } + err = fmt.Errorf("failed to open ELF file (recovered from panic): %s", name) } - - _, err = io.Copy(h, reader) - if err != nil { - return nil, fmt.Errorf("failed to hash file trailer: %v", err) - } - - // 3) Hash length - lengthArray := make([]byte, 8) - binary.BigEndian.PutUint64(lengthArray, uint64(size)) - _, err = io.Copy(h, bytes.NewReader(lengthArray)) - if err != nil { - return nil, fmt.Errorf("failed to hash file length: %v", err) - } - } else { - // hash complete file - _, err = io.Copy(h, reader) - if err != nil { - return nil, fmt.Errorf("failed to hash file: %v", err) - } - } - - return h.Sum(nil), nil -} - -func FileHash(fileName string) ([]byte, error) { - f, err := os.Open(fileName) - if err != nil { - return nil, err - } - defer f.Close() - - return fileHashReader(f) -} - -// CalculateIDFromReader calculates a 128-bit executable ID of the contents of a reader. -// For kernel files (modules & kernel image), use CalculateKernelFileID instead. -func CalculateIDFromReader(reader io.ReadSeeker) (libpf.FileID, error) { - hash, err := fileHashReader(reader) - if err != nil { - return libpf.FileID{}, err - } - return libpf.FileIDFromBytes(hash[0:16]) -} - -// CalculateID calculates a 128-bit executable ID of the contents of a file. -// For kernel files (modules & kernel image), use CalculateKernelFileID instead. -func CalculateID(fileName string) (libpf.FileID, error) { - hash, err := FileHash(fileName) - if err != nil { - return libpf.FileID{}, err - } - return libpf.FileIDFromBytes(hash[0:16]) -} - -// CalculateIDString provides a string representation of the hash of a given file. -func CalculateIDString(fileName string) (string, error) { - hash, err := FileHash(fileName) - if err != nil { - return "", err - } - - return fmt.Sprintf("%x", hash), nil + }() + return elf.Open(name) } // HasDWARFData returns true if the provided ELF file contains actionable DWARF debugging @@ -238,7 +93,7 @@ var ErrNoDebugLink = errors.New("no debug link") func ParseDebugLink(data []byte) (linkName string, crc32 int32, err error) { strEnd := bytes.IndexByte(data, 0) if strEnd < 0 { - return "", 0, fmt.Errorf("malformed debug link, not zero terminated") + return "", 0, errors.New("malformed debug link, not zero terminated") } linkName = strings.ToValidUTF8(string(data[:strEnd]), "") @@ -280,6 +135,41 @@ func GetDebugLink(elfFile *elf.File) (linkName string, crc32 int32, err error) { return ParseDebugLink(sectionData) } +// GetDebugAltLink returns the contents of the `.gnu_debugaltlink` section (path and build +// ID). If no link is present, ErrNoDebugLink is returned. +func GetDebugAltLink(elfFile *elf.File) (fileName, buildID string, err error) { + // The .gnu_debugaltlink section is not always present + sectionData, err := getSectionData(elfFile, ".gnu_debugaltlink") + if err != nil { + return "", "", ErrNoDebugLink + } + + // The whole .gnu_debugaltlink section consists of: + // 1) path to target (variable-length string) + // 2) null character separator (1 byte) + // 3) build ID (usually 20 bytes, but can vary) + // + // First, find the position of the null character: + nullCharIdx := bytes.IndexByte(sectionData, 0) + if nullCharIdx == -1 { + return "", "", nil + } + + // The path consists of all the characters before the first null character + path := strings.ToValidUTF8(string(sectionData[:nullCharIdx]), "") + + // Check that we can read a build ID: there should be at least 1 byte after the null character. + if nullCharIdx+1 == len(sectionData) { + return "", "", errors.New("malformed .gnu_debugaltlink section (missing build ID)") + } + + // The build ID consists of all the bytes after the first null character + buildIDBytes := sectionData[nullCharIdx+1:] + buildID = hex.EncodeToString(buildIDBytes) + + return path, buildID, nil +} + var ErrNoBuildID = errors.New("no build ID") var ubuntuKernelSignature = regexp.MustCompile(` \(Ubuntu[^)]*\)\n$`) @@ -299,13 +189,13 @@ func GetKernelVersionBytes(elfFile *elf.File) ([]byte, error) { startIdx := bytes.Index(sectionData, procVersionContents) if startIdx < 0 { - return nil, fmt.Errorf("unable to find Linux version") + return nil, errors.New("unable to find Linux version") } // Skip the null character startIdx++ endIdx := bytes.IndexByte(sectionData[startIdx:], 0x0) if endIdx < 0 { - return nil, fmt.Errorf("unable to find Linux version (can't find end of string)") + return nil, errors.New("unable to find Linux version (can't find end of string)") } versionBytes := sectionData[startIdx : startIdx+endIdx] @@ -397,7 +287,7 @@ func getNoteHexString(sectionBytes []byte, name string, noteType uint32) ( return "", false, nil } if idx < 4 { // there needs to be room for descsz - return "", false, fmt.Errorf("could not read note data size") + return "", false, errors.New("could not read note data size") } idxDataStart := idx + len(noteHeader) @@ -416,6 +306,32 @@ func getNoteHexString(sectionBytes []byte, name string, noteType uint32) ( return hex.EncodeToString(sectionBytes[idxDataStart:idxDataEnd]), true, nil } +// GetLinuxBuildSalt extracts the linux kernel build salt from the provided ELF path. +// It is read from the .notes ELF section. +// It should be present in both kernel modules and the kernel image of most distro-vended kernel +// packages, and should be identical across all the files: kernel modules will have the same salt +// as their corresponding vmlinux image if they were built at the same time. +// This can be used to identify the kernel image corresponding to a module. +// See https://lkml.org/lkml/2018/7/3/1156 +func GetLinuxBuildSalt(filePath string) (salt string, found bool, err error) { + elfFile, err := SafeOpenELF(filePath) + if err != nil { + return "", false, fmt.Errorf("could not open %s: %w", filePath, err) + } + defer elfFile.Close() + + sectionData, err := getSectionData(elfFile, ".note.Linux") + if err != nil { + sectionData, err = getSectionData(elfFile, ".notes") + if err != nil { + return "", false, nil + } + } + + // 0x100 is defined as LINUX_ELFNOTE_BUILD_SALT in include/linux/build-salt.h + return getNoteHexString(sectionData, "Linux", 0x100) +} + func symbolMapFromELFSymbols(syms []elf.Symbol) *libpf.SymbolMap { symmap := &libpf.SymbolMap{} for _, sym := range syms { @@ -429,6 +345,16 @@ func symbolMapFromELFSymbols(syms []elf.Symbol) *libpf.SymbolMap { return symmap } +// GetSymbols gets the symbols of elf.File and returns them as libpf.SymbolMap for +// fast lookup by address and name. +func GetSymbols(elfFile *elf.File) (*libpf.SymbolMap, error) { + syms, err := elfFile.Symbols() + if err != nil { + return nil, err + } + return symbolMapFromELFSymbols(syms), nil +} + // GetDynamicSymbols gets the dynamic symbols of elf.File and returns them as libpf.SymbolMap for // fast lookup by address and name. func GetDynamicSymbols(elfFile *elf.File) (*libpf.SymbolMap, error) { @@ -439,27 +365,41 @@ func GetDynamicSymbols(elfFile *elf.File) (*libpf.SymbolMap, error) { return symbolMapFromELFSymbols(syms), nil } -// CalculateKernelFileID returns the FileID of a kernel image or module, which consists of a hash of -// its GNU BuildID in hex string form. -// The hashing step is to ensure that the FileID remains an opaque concept to the end user. -func CalculateKernelFileID(buildID string) (fileID libpf.FileID) { - h := fnv.New128a() - _, _ = h.Write([]byte(buildID)) - // Cannot fail, ignore error. - fileID, _ = libpf.FileIDFromBytes(h.Sum(nil)) - return fileID +// IsKernelModule returns true if the provided ELF file looks like a kernel module (an ELF with a +// .modinfo and .gnu.linkonce.this_module sections). +func IsKernelModule(file *elf.File) (bool, error) { + sectionFound, err := HasSection(file, ".modinfo") + if err != nil { + return false, err + } + + if !sectionFound { + return false, nil + } + + return HasSection(file, ".gnu.linkonce.this_module") +} + +// IsKernelImage returns true if the provided ELF file looks like a kernel image (an ELF with a +// __modver section). +func IsKernelImage(file *elf.File) (bool, error) { + return HasSection(file, "__modver") } -// KernelFileIDToggleDebug returns the FileID of a kernel debug file (image or module) based on the -// FileID of its non-debug counterpart. This function is its own inverse, so it can be used for the -// opposite operation. -// This provides 2 properties: -// - FileIDs must be different between kernel files and their debug files. -// - A kernel FileID (debug and non-debug) must only depend on its GNU BuildID (see KernelFileID), -// and can always be computed in the Host Agent or during indexing without external information. -func KernelFileIDToggleDebug(kernelFileID libpf.FileID) (fileID libpf.FileID) { - // Reverse high and low. - return libpf.NewFileID(kernelFileID.Lo(), kernelFileID.Hi()) +// IsKernelFile returns true if the provided ELF file looks like a kernel file (either a kernel +// image or a kernel module). +func IsKernelFile(file *elf.File) (bool, error) { + isModule, err := IsKernelImage(file) + if err != nil { + return false, err + } + + isImage, err := IsKernelModule(file) + if err != nil { + return false, err + } + + return isModule || isImage, nil } // IsGoBinary returns true if the provided file is a Go binary (= an ELF file with diff --git a/libpf/pfelf/pfelf_test.go b/libpf/pfelf/pfelf_test.go index 86a562b8..f1ed76ac 100644 --- a/libpf/pfelf/pfelf_test.go +++ b/libpf/pfelf/pfelf_test.go @@ -12,10 +12,12 @@ import ( "os" "testing" - "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/pfelf" "github.com/elastic/otel-profiling-agent/testsupport" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/libpf/pfelf" ) var ( @@ -29,159 +31,62 @@ var ( func getELF(path string, t *testing.T) *elf.File { file, err := elf.Open(path) - assert.Nil(t, err) + assert.NoError(t, err) return file } func TestGetBuildID(t *testing.T) { debugExePath, err := testsupport.WriteTestExecutable1() - if err != nil { - t.Fatalf("Failed to write test executable: %v", err) - } + require.NoError(t, err) defer os.Remove(debugExePath) elfFile := getELF(debugExePath, t) defer elfFile.Close() buildID, err := pfelf.GetBuildID(elfFile) - if err != nil { - t.Fatalf("getBuildID failed with error: %s", err) - } - - if buildID != "6920fd217a8416131f4377ef018a2c932f311b6d" { - t.Fatalf("Invalid build-id: %s", buildID) - } + require.NoError(t, err) + assert.Equal(t, "6920fd217a8416131f4377ef018a2c932f311b6d", buildID) } func TestGetDebugLink(t *testing.T) { debugExePath, err := testsupport.WriteTestExecutable1() - if err != nil { - t.Fatalf("Failed to write test executable: %v", err) - } + require.NoError(t, err) defer os.Remove(debugExePath) elfFile := getELF(debugExePath, t) defer elfFile.Close() debugLink, crc32, err := pfelf.GetDebugLink(elfFile) - if err != nil { - t.Fatalf("getDebugLink failed with error: %s", err) - } - - if debugLink != "dumpmscat-4.10.8-0.fc30.x86_64.debug" { - t.Fatalf("Invalid debug link: %s", debugLink) - } - - if uint32(crc32) != 0xfe3099b8 { - t.Fatalf("Invalid debug link CRC32: %v", crc32) - } + require.NoError(t, err) + assert.Equal(t, "dumpmscat-4.10.8-0.fc30.x86_64.debug", debugLink) + assert.Equal(t, uint32(0xfe3099b8), uint32(crc32)) } func TestGetBuildIDError(t *testing.T) { debugExePath, err := testsupport.WriteTestExecutable2() - if err != nil { - t.Fatalf("Failed to write test executable: %v", err) - } + require.NoError(t, err) defer os.Remove(debugExePath) elfFile := getELF(debugExePath, t) defer elfFile.Close() buildID, err := pfelf.GetBuildID(elfFile) - if err != pfelf.ErrNoBuildID { - t.Fatalf("Expected errNoBuildID but got: %s", err) - } - if buildID != "" { - t.Fatalf("Expected an empty string but got: %s", err) + if assert.ErrorIs(t, pfelf.ErrNoBuildID, err) { + assert.Equal(t, "", buildID) } } func TestGetDebugLinkError(t *testing.T) { debugExePath, err := testsupport.WriteTestExecutable2() - if err != nil { - t.Fatalf("Failed to write test executable: %v", err) - } + require.NoError(t, err) defer os.Remove(debugExePath) elfFile := getELF(debugExePath, t) defer elfFile.Close() debugLink, _, err := pfelf.GetDebugLink(elfFile) - if err != pfelf.ErrNoDebugLink { - t.Fatalf("expected errNoDebugLink but got: %s", err) - } - - if debugLink != "" { - t.Fatalf("Expected an empty string but got: %s", err) - } -} - -func TestIsELF(t *testing.T) { - if _, err := os.Stat(withoutDebugSymsPath); err != nil { - t.Fatalf("Could not access test file %s: %v", withoutDebugSymsPath, err) - } - - asciiFile, err := os.CreateTemp("", "pfelf_test_ascii_") - if err != nil { - t.Fatalf("Failed to open tempfile: %v", err) - } - - _, err = asciiFile.WriteString("Some random ascii text") - if err != nil { - t.Fatalf("Failed to write to tempfile: %v", err) - } - asciiPath := asciiFile.Name() - if err = asciiFile.Close(); err != nil { - t.Fatalf("Error closing file: %v", err) - } - defer os.Remove(asciiPath) - - shortFile, err := os.CreateTemp("", "pfelf_test_short_") - if err != nil { - t.Fatalf("Failed to open tempfile: %v", err) - } - - _, err = shortFile.Write([]byte{0x7f}) - if err != nil { - t.Fatalf("Failed to write to tempfile: %v", err) - } - shortFilePath := shortFile.Name() - if err := shortFile.Close(); err != nil { - t.Fatalf("Error closing file: %v", err) - } - defer os.Remove(shortFilePath) - - tests := map[string]struct { - filePath string - expectedResult bool - expectedError bool - }{ - "ELF executable": {withoutDebugSymsPath, true, false}, - "ASCII file": {asciiPath, false, false}, - "Short file": {shortFilePath, false, false}, - "Invalid path": {"/some/invalid/path", false, true}, - } - - for testName, testCase := range tests { - name := testName - tc := testCase - t.Run(name, func(t *testing.T) { - isELF, err := pfelf.IsELF(tc.filePath) - if tc.expectedError { - if err == nil { - t.Fatalf("Expected an error but didn't get one") - } - return - } - - if err != nil { - t.Fatalf("%v", err) - } - - if isELF != tc.expectedResult { - t.Fatalf("Expected %v but got %v", tc.expectedResult, isELF) - } - }) + if assert.ErrorIs(t, pfelf.ErrNoDebugLink, err) { + assert.Equal(t, "", debugLink) } } @@ -203,10 +108,7 @@ func TestHasDWARFData(t *testing.T) { defer elfFile.Close() hasDWARF := pfelf.HasDWARFData(elfFile) - - if hasDWARF != tc.expectedResult { - t.Fatalf("Expected %v but got %v", tc.expectedResult, hasDWARF) - } + assert.Equal(t, tc.expectedResult, hasDWARF) }) } } @@ -217,35 +119,20 @@ func TestGetSectionAddress(t *testing.T) { // The fixed-address test executable has a section named `.coffee_section` at address 0xC0FFEE address, found, err := pfelf.GetSectionAddress(elfFile, ".coffee_section") - if err != nil { - t.Fatal(err) - } - if !found { - t.Fatalf("unable to find .coffee_section") - } - expectedAddress := uint64(0xC0FFEE) - if address != expectedAddress { - t.Fatalf("expected address 0x%x, got 0x%x", expectedAddress, address) - } + require.NoError(t, err) + assert.True(t, found, "unable to find .coffee_section") + assert.Equal(t, uint64(0xC0FFEE), address) // Try to find a section that does not exist _, found, err = pfelf.GetSectionAddress(elfFile, ".tea_section") - if err != nil { - t.Fatal(err) - } - if found { - t.Fatalf("did not expect to find .tea_section") - } + require.NoError(t, err) + assert.False(t, found) } func TestGetBuildIDFromNotesFile(t *testing.T) { buildID, err := pfelf.GetBuildIDFromNotesFile("testdata/the_notorious_build_id") - if err != nil { - t.Fatal(err) - } - if buildID != hex.EncodeToString([]byte("_notorious_build_id_")) { - t.Fatalf("got wrong buildID: %v", buildID) - } + require.NoError(t, err) + assert.Equal(t, hex.EncodeToString([]byte("_notorious_build_id_")), buildID) } func TestGetKernelVersionBytes(t *testing.T) { @@ -257,123 +144,36 @@ func TestGetKernelVersionBytes(t *testing.T) { defer elfFile.Close() ver, err := pfelf.GetKernelVersionBytes(elfFile) - if err != nil { - t.Fatal(err) - } - versionString := string(ver) - if versionString != "Linux version 1.2.3\n" { - t.Fatalf("unexpected value: %v", versionString) - } + require.NoError(t, err) + assert.Equal(t, "Linux version 1.2.3\n", string(ver)) }) } } -func TestFilehandling(t *testing.T) { - // The below hashes can be generated or checked with bash like: - // $ printf "\x7fELF\x00\x01\x02\x03\x04"|sha256sum - // 39022213564b1d52549ebe535dfff027c618ab0a599d5e7c69ed4a2e1d3dd687 - - tests := map[string]struct { - data []byte - id libpf.FileID - hash string - }{ - "emptyFile": { - data: []byte{}, - id: libpf.NewFileID(0xe3b0c44298fc1c14, 0x9afbf4c8996fb924), - hash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - }, - "simpleFile": { - data: []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA}, - id: libpf.NewFileID(0xc848e1013f9f04a9, 0xd63fa43ce7fd4af0), - hash: "c848e1013f9f04a9d63fa43ce7fd4af035152c7c669a4a404b67107cee5f2e4e", - }, - "ELF file": { // ELF file magic is 0x7f,'E','L','F'" - data: []byte{0x7F, 'E', 'L', 'F', 0x00, 0x01, 0x2, 0x3, 0x4}, - id: libpf.NewFileID(0xcaf6e5907166ac76, 0xeef618e5f7f59cd9), - hash: "caf6e5907166ac76eef618e5f7f59cd98a02f0ab46acf413aa6a293a84fe1721", - }, - } - - for name, testcase := range tests { - name := name - testcase := testcase - t.Run(name, func(t *testing.T) { - fileName, err := libpf.WriteTempFile(testcase.data, "", name) - if err != nil { - t.Fatalf("Failed to write temporary file: %v", err) - } - defer os.Remove(fileName) - - fileID, err := pfelf.CalculateID(fileName) - if err != nil { - t.Fatalf("Failed to calculate executable ID: %v", err) - } - if fileID != testcase.id { - t.Fatalf("Unexpected FileID. Expected %d, got %d", testcase.id, fileID) - } - - hash, err := pfelf.CalculateIDString(fileName) - if err != nil { - t.Fatalf("Failed to generate hash of file: %v", err) - } - if hash != testcase.hash { - t.Fatalf("Unexpected Hash. Expected %s, got %s", testcase.hash, hash) - } - }) - } -} - -func assertSymbol(t *testing.T, symmap *libpf.SymbolMap, name libpf.SymbolName, - expectedAddress libpf.SymbolValue) { - sym, _ := symmap.LookupSymbol(name) - if expectedAddress == libpf.SymbolValueInvalid { - if sym != nil { - t.Fatalf("symbol '%s', was unexpectedly found", name) - } - } else { - if sym == nil { - t.Fatalf("symbol '%s', was unexpectedly not found", name) - } - if sym.Address != expectedAddress { - t.Fatalf("symbol '%s', expected address 0x%x, got 0x%x", - name, expectedAddress, sym.Address) - } - } -} - -func assertRevSymbol(t *testing.T, symmap *libpf.SymbolMap, addr libpf.SymbolValue, - expectedName libpf.SymbolName, expectedOffset libpf.Address) { - name, offs, ok := symmap.LookupByAddress(addr) - if !ok { - t.Fatalf("address '%x', unexpectedly has no name", addr) - } - if name != expectedName || expectedOffset != offs { - t.Fatalf("address '%x', expected name %s+%d, got %s+%d", - addr, expectedName, expectedOffset, name, offs) - } -} - func TestSymbols(t *testing.T) { exePath, err := testsupport.WriteSharedLibrary() - if err != nil { - t.Fatalf("Failed to write test executable: %v", err) - } + require.NoError(t, err) defer os.Remove(exePath) ef, err := elf.Open(exePath) - if err != nil { - t.Fatalf("Failed to open test executable: %v", err) - } + require.NoError(t, err) defer ef.Close() - syms, err := pfelf.GetDynamicSymbols(ef) - if err != nil { - t.Fatalf("Failed to get dynamic symbols: %v", err) + symmap, err := pfelf.GetDynamicSymbols(ef) + require.NoError(t, err) + + sym, _ := symmap.LookupSymbol("func") + if assert.NotNil(t, sym) { + assert.Equal(t, libpf.SymbolValue(0x1000), sym.Address) } + sym, _ = symmap.LookupSymbol("not_existent") + assert.Nil(t, sym) - assertSymbol(t, syms, "func", 0x1000) - assertSymbol(t, syms, "not_existent", libpf.SymbolValueInvalid) - assertRevSymbol(t, syms, 0x1002, "func", 2) + name, offs, ok := symmap.LookupByAddress(0x1002) + if assert.True(t, ok) { + assert.Equal(t, libpf.SymbolName("func"), name) + assert.Equal(t, libpf.Address(2), offs) + } } func testGoBinary(t *testing.T, filename string, isGoExpected bool) { @@ -381,8 +181,8 @@ func testGoBinary(t *testing.T, filename string, isGoExpected bool) { defer ef.Close() isGo, err := pfelf.IsGoBinary(ef) - assert.Nil(t, err) - assert.Equal(t, isGo, isGoExpected) + require.NoError(t, err) + assert.Equal(t, isGoExpected, isGo) } func TestIsGoBinary(t *testing.T) { @@ -408,24 +208,7 @@ func TestHasCodeSection(t *testing.T) { defer elfFile.Close() hasCode := pfelf.HasCodeSection(elfFile) - - if hasCode != tc.expectedResult { - t.Fatalf("Expected %v but got %v", tc.expectedResult, hasCode) - } + assert.Equal(t, tc.expectedResult, hasCode) }) } } - -func TestCalculateKernelFileID(t *testing.T) { - buildID := "f8e1cf0f60558098edaec164ac7749df" - fileID := pfelf.CalculateKernelFileID(buildID) - expectedFileID, _ := libpf.FileIDFromString("026a2d6a60ee6b4eb8ec85adf2e76f4d") - assert.Equal(t, expectedFileID, fileID) -} - -func TestKernelFileIDToggleDebug(t *testing.T) { - fileID, _ := libpf.FileIDFromString("026a2d6a60ee6b4eb8ec85adf2e76f4d") - toggled := pfelf.KernelFileIDToggleDebug(fileID) - expectedFileID, _ := libpf.FileIDFromString("b8ec85adf2e76f4d026a2d6a60ee6b4e") - assert.Equal(t, expectedFileID, toggled) -} diff --git a/libpf/pfelf/testdata/test.c b/libpf/pfelf/testdata/test.c index 156d92c3..fc2b6e0b 100644 --- a/libpf/pfelf/testdata/test.c +++ b/libpf/pfelf/testdata/test.c @@ -1,9 +1,3 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - #ifndef LINUX_VERSION #define LINUX_VERSION "" #endif diff --git a/libpf/readatbuf/readatbuf.go b/libpf/readatbuf/readatbuf.go index 9314b910..ef20f4c0 100644 --- a/libpf/readatbuf/readatbuf.go +++ b/libpf/readatbuf/readatbuf.go @@ -9,6 +9,7 @@ package readatbuf import ( + "errors" "fmt" "io" @@ -50,10 +51,10 @@ func HashUInt(v uint) uint32 { // to cache. func New(inner io.ReaderAt, pageSize, cacheSize uint) (reader *Reader, err error) { if pageSize == 0 { - return nil, fmt.Errorf("pageSize cannot be zero") + return nil, errors.New("pageSize cannot be zero") } if cacheSize == 0 { - return nil, fmt.Errorf("cacheSize cannot be zero") + return nil, errors.New("cacheSize cannot be zero") } reader = &Reader{ @@ -170,7 +171,7 @@ func (reader *Reader) getOrReadPage(pageIdx uint) (data []byte, eof bool, err er } if !eof && uint(n) < reader.pageSize { - return nil, false, fmt.Errorf("failed to read whole page") + return nil, false, errors.New("failed to read whole page") } reader.cache.Add(pageIdx, page{data: buffer, eof: eof}) diff --git a/libpf/readatbuf/readatbuf_test.go b/libpf/readatbuf/readatbuf_test.go index cf6fe2d9..68a1132b 100644 --- a/libpf/readatbuf/readatbuf_test.go +++ b/libpf/readatbuf/readatbuf_test.go @@ -12,16 +12,14 @@ import ( "github.com/elastic/otel-profiling-agent/libpf/readatbuf" "github.com/elastic/otel-profiling-agent/testsupport" + "github.com/stretchr/testify/require" ) func testVariant(t *testing.T, fileSize, granularity, cacheSize uint) { file := testsupport.GenerateTestInputFile(255, fileSize) rawReader := bytes.NewReader(file) cachingReader, err := readatbuf.New(rawReader, granularity, cacheSize) - if err != nil { - t.Fatalf("failed to create caching reader: %v", err) - } - + require.NoError(t, err) testsupport.ValidateReadAtWrapperTransparency(t, 10000, file, cachingReader) } diff --git a/libpf/symbol.go b/libpf/symbol.go index 96ecb3be..b32bb367 100644 --- a/libpf/symbol.go +++ b/libpf/symbol.go @@ -99,10 +99,10 @@ func (symmap *SymbolMap) LookupByAddress(val SymbolValue) (SymbolName, Address, return SymbolNameUnknown, Address(val), false } -// ScanAllNames calls the provided callback with all the symbol names in the map. -func (symmap *SymbolMap) ScanAllNames(cb func(SymbolName)) { +// VisitAll calls the provided callback with all the symbols in the map. +func (symmap *SymbolMap) VisitAll(cb func(Symbol)) { for _, f := range symmap.nameToSymbol { - cb(f.Name) + cb(*f) } } diff --git a/libpf/testdata/crc32_test_data b/libpf/testdata/crc32_test_data deleted file mode 100644 index 72488fc7..00000000 --- a/libpf/testdata/crc32_test_data +++ /dev/null @@ -1 +0,0 @@ -crc32_test_data diff --git a/libpf/trace.go b/libpf/trace.go new file mode 100644 index 00000000..dd2ad966 --- /dev/null +++ b/libpf/trace.go @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package libpf + +// Trace represents a stack trace. Each tuple (Files[i], Linenos[i]) represents a +// stack frame via the file ID and line number at the offset i in the trace. The +// information for the most recently called function is at offset 0. +type Trace struct { + Files []FileID + Linenos []AddressOrLineno + FrameTypes []FrameType + Hash TraceHash +} + +// AppendFrame appends a frame to the columnar frame array. +func (trace *Trace) AppendFrame(ty FrameType, file FileID, addrOrLine AddressOrLineno) { + trace.FrameTypes = append(trace.FrameTypes, ty) + trace.Files = append(trace.Files, file) + trace.Linenos = append(trace.Linenos, addrOrLine) +} diff --git a/libpf/tracehash.go b/libpf/tracehash.go new file mode 100644 index 00000000..c7de4090 --- /dev/null +++ b/libpf/tracehash.go @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package libpf + +import ( + "encoding" + "encoding/base64" + + "github.com/elastic/otel-profiling-agent/libpf/basehash" +) + +// TraceHash represents the unique hash of a trace +type TraceHash struct { + basehash.Hash128 +} + +func NewTraceHash(hi, lo uint64) TraceHash { + return TraceHash{basehash.New128(hi, lo)} +} + +// TraceHashFromBytes parses a byte slice of a trace hash into the internal data representation. +func TraceHashFromBytes(b []byte) (TraceHash, error) { + h, err := basehash.New128FromBytes(b) + if err != nil { + return TraceHash{}, err + } + return TraceHash{h}, nil +} + +// TraceHashFromString parses a byte slice of a trace hash into the internal data representation. +func TraceHashFromString(s string) (TraceHash, error) { + h, err := basehash.New128FromString(s) + if err != nil { + return TraceHash{}, err + } + return TraceHash{h}, nil +} + +func (h TraceHash) Equal(other TraceHash) bool { + return h.Hash128.Equal(other.Hash128) +} + +func (h TraceHash) Less(other TraceHash) bool { + return h.Hash128.Less(other.Hash128) +} + +// EncodeTo encodes the hash into the base64 encoded representation +// and stores it in the provided destination byte array. +// The length of the destination must be at least EncodedLen(). +func (h TraceHash) EncodeTo(dst []byte) { + base64.RawURLEncoding.Encode(dst, h.Bytes()) +} + +// EncodedLen returns the length of the hash's base64 representation. +func (TraceHash) EncodedLen() int { + // TraceHash is 16 bytes long, the base64 representation is one base64 byte per 6 bits. + return ((16)*8)/6 + 1 +} + +// Hash32 returns a 32 bits hash of the input. +// It's main purpose is to be used for LRU caching. +func (h TraceHash) Hash32() uint32 { + return uint32(h.Lo()) +} + +// Compile-time interface checks +var _ encoding.TextUnmarshaler = (*TraceHash)(nil) +var _ encoding.TextMarshaler = (*TraceHash)(nil) diff --git a/libpf/tracehash_test.go b/libpf/tracehash_test.go new file mode 100644 index 00000000..a62f2ced --- /dev/null +++ b/libpf/tracehash_test.go @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package libpf + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTraceHashSprintf(t *testing.T) { + origHash := NewTraceHash(0x0001C03F8D6B8520, 0xEDEAEEA9460BEEBB) + + marshaled := fmt.Sprintf("%d", origHash) + // nolint:goconst + expected := "{492854164817184 17143777342331285179}" + assert.Equal(t, expected, marshaled) + + marshaled = fmt.Sprintf("%s", origHash) + expected = "{%!s(uint64=492854164817184) %!s(uint64=17143777342331285179)}" + assert.Equal(t, expected, marshaled) + + marshaled = fmt.Sprintf("%v", origHash) + // nolint:goconst + expected = "{492854164817184 17143777342331285179}" + assert.Equal(t, expected, marshaled) + + marshaled = fmt.Sprintf("%#v", origHash) + expected = "0x1c03f8d6b8520edeaeea9460beebb" + assert.Equal(t, expected, marshaled) + + // Values were chosen to test non-zero-padded output + traceHash := NewTraceHash(42, 100) + + marshaled = fmt.Sprintf("%x", traceHash) + expected = "2a0000000000000064" + assert.Equal(t, expected, marshaled) + + marshaled = fmt.Sprintf("%X", traceHash) + expected = "2A0000000000000064" + assert.Equal(t, expected, marshaled) + + marshaled = fmt.Sprintf("%#x", traceHash) + expected = "0x2a0000000000000064" + assert.Equal(t, expected, marshaled) + + marshaled = fmt.Sprintf("%#X", traceHash) + expected = "0x2A0000000000000064" + assert.Equal(t, expected, marshaled) +} + +func TestTraceHashMarshal(t *testing.T) { + origHash := NewTraceHash(0x600DF00D, 0xF00D600D) + + // Test (Un)MarshalJSON + data, err := origHash.MarshalJSON() + require.NoError(t, err) + + marshaled := string(data) + expected := "\"00000000600df00d00000000f00d600d\"" + assert.Equal(t, expected, marshaled) + + var jsonHash TraceHash + err = jsonHash.UnmarshalJSON(data) + require.NoError(t, err) + assert.Equal(t, origHash, jsonHash) + + // Test (Un)MarshalText + data, err = origHash.MarshalText() + require.NoError(t, err) + + marshaled = string(data) + expected = "00000000600df00d00000000f00d600d" + assert.Equal(t, expected, marshaled) + + var textHash TraceHash + err = textHash.UnmarshalText(data) + require.NoError(t, err) + assert.Equal(t, origHash, textHash) +} diff --git a/libpf/vc/vc.go b/libpf/vc/vc.go deleted file mode 100644 index a828ad3c..00000000 --- a/libpf/vc/vc.go +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -// Package vc provides buildtime information. -package vc - -var ( - // The following variables are going to be set at link time using ldflags - // and can be referenced later in the program: - // the container image tag is named - - // revision of the service - revision = "OTEL-review" - // buildTimestamp, timestamp of the build - buildTimestamp = "N/A" - // Service version in vX.Y.Z{-N-abbrev} format (via git-describe --tags) - version = "1.0.0" -) - -// Revision of the service. -func Revision() string { - return revision -} - -// BuildTimestamp returns the timestamp of the build. -func BuildTimestamp() string { - return buildTimestamp -} - -// Version in vX.Y.Z{-N-abbrev} format. -func Version() string { - return version -} diff --git a/libpf/xsync/once_test.go b/libpf/xsync/once_test.go index c57d723c..6534ff50 100644 --- a/libpf/xsync/once_test.go +++ b/libpf/xsync/once_test.go @@ -14,8 +14,9 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/elastic/otel-profiling-agent/libpf/xsync" - assert "github.com/stretchr/testify/require" ) func TestOnceLock(t *testing.T) { diff --git a/libpf/xsync/rwlock_test.go b/libpf/xsync/rwlock_test.go index 8a53fae2..c325c9d7 100644 --- a/libpf/xsync/rwlock_test.go +++ b/libpf/xsync/rwlock_test.go @@ -12,7 +12,7 @@ import ( "testing" "github.com/elastic/otel-profiling-agent/libpf/xsync" - assert "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" ) type SharedResourceMutable struct { diff --git a/lpm/lpm_test.go b/lpm/lpm_test.go index ca52d084..1beef11d 100644 --- a/lpm/lpm_test.go +++ b/lpm/lpm_test.go @@ -1,3 +1,6 @@ +//go:build !integration +// +build !integration + /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Apache License 2.0. @@ -9,7 +12,8 @@ package lpm import ( "testing" - "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetRightmostSetBit(t *testing.T) { @@ -28,10 +32,7 @@ func TestGetRightmostSetBit(t *testing.T) { test := test t.Run(name, func(t *testing.T) { output := getRightmostSetBit(test.input) - if output != test.expected { - t.Fatalf("Expected %d (0b%b) but got %d (0b%b)", - test.expected, test.expected, output, output) - } + assert.Equal(t, test.expected, output) }) } } @@ -63,19 +64,12 @@ func TestCalculatePrefixList(t *testing.T) { test := test t.Run(name, func(t *testing.T) { prefixes, err := CalculatePrefixList(test.start, test.end) - if err != nil { - if test.err { - // We received and expected an error. So we can return here. - return - } - t.Fatalf("Unexpected error: %v", err) - } if test.err { - t.Fatalf("Expected an error but got none") - } - if diff := cmp.Diff(test.expect, prefixes); diff != "" { - t.Fatalf("CalculatePrefixList() mismatching prefixes (-want +got):\n%s", diff) + require.Error(t, err) + return } + require.NoError(t, err) + assert.Equal(t, test.expect, prefixes) }) } } diff --git a/maccess/maccess_amd64.go b/maccess/maccess_amd64.go index 551335e9..e3114ee4 100644 --- a/maccess/maccess_amd64.go +++ b/maccess/maccess_amd64.go @@ -11,7 +11,7 @@ package maccess import ( "bytes" "encoding/binary" - "fmt" + "errors" ) // CopyFromUserNoFaultIsPatched tries to find a relative jump instruction in codeblob @@ -19,7 +19,7 @@ import ( func CopyFromUserNoFaultIsPatched(codeblob []byte, faultyFuncAddr uint64, newCheckFuncAddr uint64) (bool, error) { if len(codeblob) == 0 { - return false, fmt.Errorf("empty code blob") + return false, errors.New("empty code blob") } for i := 0; i < len(codeblob); { diff --git a/maccess/maccess_amd64_test.go b/maccess/maccess_amd64_test.go index 6c461614..5d09c04d 100644 --- a/maccess/maccess_amd64_test.go +++ b/maccess/maccess_amd64_test.go @@ -8,7 +8,11 @@ package maccess -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) // nolint:lll var codeblobs = map[string]struct { @@ -109,11 +113,8 @@ func TestGetJumpInCopyFromUserNoFault(t *testing.T) { t.Run(name, func(t *testing.T) { isPatched, err := CopyFromUserNoFaultIsPatched(test.code, test.copyFromUserNofaultAddr, test.nmiUaccessOkayAddr) - if err != nil { - t.Fatal(err) - } - if isPatched != test.isPatched { - t.Fatalf("Expected %v but got %v", test.isPatched, isPatched) + if assert.NoError(t, err) { + assert.Equal(t, test.isPatched, isPatched) } }) } diff --git a/maccess/maccess_arm64.go b/maccess/maccess_arm64.go index fe87feb5..4e5af572 100644 --- a/maccess/maccess_arm64.go +++ b/maccess/maccess_arm64.go @@ -11,7 +11,7 @@ package maccess import ( "fmt" - ah "github.com/elastic/otel-profiling-agent/libpf/armhelpers" + ah "github.com/elastic/otel-profiling-agent/armhelpers" aa "golang.org/x/arch/arm64/arm64asm" ) @@ -51,7 +51,7 @@ func CopyFromUserNoFaultIsPatched(codeblob []byte, _ uint64, _ uint64) (bool, er // [2] https://github.com/torvalds/linux/blob/1c41041124bd14dd6610da256a3da4e5b74ce6b1/include/asm-generic/access_ok.h#L40 // In the set of expected assembly instructions, one argument register is used by all instructions. - var trackedReg int = -1 + var trackedReg = -1 // Statemachine to keep track of the previously encountered and expected instructions. var expectedInstructionTracker = stepNone diff --git a/maccess/maccess_arm64_test.go b/maccess/maccess_arm64_test.go index 8e379583..ed04924f 100644 --- a/maccess/maccess_arm64_test.go +++ b/maccess/maccess_arm64_test.go @@ -8,7 +8,11 @@ package maccess -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) // nolint:lll var codeblobs = map[string]struct { @@ -109,11 +113,8 @@ func TestGetJumpInCopyFromUserNoFault(t *testing.T) { test := test t.Run(name, func(t *testing.T) { isPatched, err := CopyFromUserNoFaultIsPatched(test.code, 0, 0) - if err != nil { - t.Fatal(err) - } - if isPatched != test.isPatched { - t.Fatalf("Expected %v but got %v", test.isPatched, isPatched) + if assert.NoError(t, err) { + assert.Equal(t, test.isPatched, isPatched) } }) } diff --git a/main.go b/main.go index 1146f9fa..9cefb6d5 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( "runtime" "time" + "github.com/elastic/otel-profiling-agent/containermetadata" "golang.org/x/sys/unix" "github.com/elastic/otel-profiling-agent/host" @@ -32,8 +33,7 @@ import ( log "github.com/sirupsen/logrus" - "github.com/elastic/otel-profiling-agent/libpf/memorydebug" - "github.com/elastic/otel-profiling-agent/libpf/vc" + "github.com/elastic/otel-profiling-agent/memorydebug" ) // Short copyright / license text for eBPF code @@ -58,6 +58,8 @@ https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html type exitCode int const ( + version string = "0.1.0" + exitSuccess exitCode = 0 exitFailure exitCode = 1 @@ -74,7 +76,14 @@ func startTraceHandling(ctx context.Context, rep reporter.TraceReporter, return fmt.Errorf("failed to start map monitors: %v", err) } - return tracehandler.Start(ctx, rep, trc, traceCh, times) + containerMetadataHandler, err := containermetadata.GetHandler(ctx, times.MonitorInterval()) + if err != nil { + return fmt.Errorf("failed to create container metadata handler: %v", err) + } + + _, err = tracehandler.Start(ctx, containerMetadataHandler, rep, + trc.TraceProcessor(), traceCh, times) + return err } func main() { @@ -84,12 +93,12 @@ func main() { func mainWithExitCode() exitCode { err := parseArgs() if err != nil { - fmt.Fprintf(os.Stderr, "Failure to parse arguments: %s", err) + log.Errorf("Failure to parse arguments: %s", err) return exitParseError } if argMapScaleFactor > 8 { - fmt.Fprintf(os.Stderr, "eBPF map scaling factor %d exceeds limit (max: %d)\n", + log.Errorf("eBPF map scaling factor %d exceeds limit (max: %d)", argMapScaleFactor, maxArgMapScaleFactor) return exitParseError } @@ -100,12 +109,12 @@ func mainWithExitCode() exitCode { } if argVersion { - fmt.Printf("%s\n", vc.Version()) + fmt.Printf("%s\n", version) return exitSuccess } if argBpfVerifierLogLevel > 2 { - fmt.Fprintf(os.Stderr, "invalid eBPF verifier log level: %d", argBpfVerifierLogLevel) + log.Errorf("Invalid eBPF verifier log level: %d", argBpfVerifierLogLevel) return exitParseError } @@ -116,13 +125,13 @@ func mainWithExitCode() exitCode { // Sanity check for probabilistic profiling arguments if argProbabilisticInterval < 1*time.Minute || argProbabilisticInterval > 5*time.Minute { - fmt.Fprintf(os.Stderr, "Invalid argument for probabilistic-interval: use "+ + log.Error("Invalid argument for probabilistic-interval: use " + "a duration between 1 and 5 minutes") return exitParseError } if argProbabilisticThreshold < 1 || argProbabilisticThreshold > tracer.ProbabilisticThresholdMax { - fmt.Fprintf(os.Stderr, "Invalid argument for probabilistic-threshold. Value "+ + log.Errorf("Invalid argument for probabilistic-threshold. Value "+ "should be between 1 and %d", tracer.ProbabilisticThresholdMax) return exitParseError } @@ -134,8 +143,7 @@ func mainWithExitCode() exitCode { } startTime := time.Now() - log.Infof("Starting OTEL profiling agent %s (revision %s, build timestamp %s)", - vc.Version(), vc.Revision(), vc.BuildTimestamp()) + log.Infof("Starting OTEL profiling agent") // Enable dumping of full heaps if the size of the allocated Golang heap // exceeds 150m, and start dumping memory profiles when the heap exceeds @@ -146,8 +154,7 @@ func mainWithExitCode() exitCode { var major, minor, patch uint32 major, minor, patch, err = tracer.GetCurrentKernelVersion() if err != nil { - msg := fmt.Sprintf("Failed to get kernel version: %v", err) - log.Error(msg) + log.Errorf("Failed to get kernel version: %v", err) return exitFailure } @@ -160,28 +167,24 @@ func mainWithExitCode() exitCode { // https://github.com/torvalds/linux/commit/6ae08ae3dea2cfa03dd3665a3c8475c2d429ef47 minMajor, minMinor = 5, 5 default: - msg := fmt.Sprintf("unsupported architecture: %s", runtime.GOARCH) - log.Error(msg) + log.Errorf("unsupported architecture: %s", runtime.GOARCH) return exitFailure } if major < minMajor || (major == minMajor && minor < minMinor) { - msg := fmt.Sprintf("Host Agent requires kernel version "+ + log.Errorf("Host Agent requires kernel version "+ "%d.%d or newer but got %d.%d.%d", minMajor, minMinor, major, minor, patch) - log.Error(msg) return exitFailure } } if err = tracer.ProbeBPFSyscall(); err != nil { - msg := fmt.Sprintf("Failed to probe eBPF syscall: %v", err) - log.Error(msg) + log.Errorf(fmt.Sprintf("Failed to probe eBPF syscall: %v", err)) return exitFailure } if err = tracer.ProbeTracepoint(); err != nil { - msg := fmt.Sprintf("Failed to probe tracepoint: %v", err) - log.Error(msg) + log.Errorf("Failed to probe tracepoint: %v", err) return exitFailure } @@ -191,8 +194,7 @@ func mainWithExitCode() exitCode { var presentCores uint16 presentCores, err = hostmeta.PresentCPUCores() if err != nil { - msg := fmt.Sprintf("Failed to read CPU file: %v", err) - log.Error(msg) + log.Errorf("Failed to read CPU file: %v", err) return exitFailure } @@ -200,8 +202,7 @@ func mainWithExitCode() exitCode { // sent to the backend with certain RPCs. hostMetadataMap := make(map[string]string) if err = hostmeta.AddMetadata(argCollAgentAddr, hostMetadataMap); err != nil { - msg := fmt.Sprintf("Unable to get host metadata for config: %v", err) - log.Error(msg) + log.Errorf("Unable to get host metadata for config: %v", err) } // Metadata retrieval may fail, in which case, we initialize all values @@ -229,7 +230,6 @@ func mainWithExitCode() exitCode { Verbose: argVerboseMode, DisableTLS: argDisableTLS, NoKernelVersionCheck: argNoKernelVersionCheck, - UploadSymbols: false, BpfVerifierLogLevel: argBpfVerifierLogLevel, BpfVerifierLogSize: argBpfVerifierLogSize, MonitorInterval: argMonitorInterval, @@ -248,16 +248,17 @@ func mainWithExitCode() exitCode { ProbabilisticThreshold: argProbabilisticThreshold, } if err = config.SetConfiguration(&conf); err != nil { - msg := fmt.Sprintf("Failed to set configuration: %s", err) - log.Error(msg) + log.Errorf("Failed to set configuration: %s", err) return exitFailure } + // Start periodic synchronization of monotonic clock + config.StartMonotonicSync(mainCtx) log.Debugf("Done setting configuration") times := config.GetTimes() log.Debugf("Determining tracers to include") - includeTracers, err := parseTracers(argTracers) + includeTracers, err := config.ParseTracers(argTracers) if err != nil { msg := fmt.Sprintf("Failed to parse the included tracers: %s", err) log.Error(msg) @@ -292,10 +293,10 @@ func mainWithExitCode() exitCode { // Network operations to CA start here var rep reporter.Reporter // Connect to the collection agent - rep, err = reporter.StartOTLP(mainCtx, &reporter.Config{ + rep, err = reporter.Start(mainCtx, &reporter.Config{ CollAgentAddr: argCollAgentAddr, MaxRPCMsgSize: 33554432, // 32 MiB - ExecMetadataMaxQueue: 1024, + ExecMetadataMaxQueue: 2048, CountsForTracesMaxQueue: tracesQSize, MetricsMaxQueue: 1024, FramesForTracesMaxQueue: tracesQSize, @@ -343,7 +344,7 @@ func mainWithExitCode() exitCode { now := time.Now() // Initial scan of /proc filesystem to list currently-active PIDs and have them processed. - if err := trc.StartPIDEventProcessor(mainCtx); err != nil { + if err = trc.StartPIDEventProcessor(mainCtx); err != nil { log.Errorf("Failed to list processes from /proc: %v", err) } metrics.Add(metrics.IDProcPIDStartupMs, metrics.MetricValue(time.Since(now).Milliseconds())) diff --git a/main_test.go b/main_test.go deleted file mode 100644 index 3b4522fb..00000000 --- a/main_test.go +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -package main - -import ( - "testing" - - "github.com/elastic/otel-profiling-agent/config" -) - -// tests expected to succeed -var tracersTestsOK = []struct { - in string - php bool - python bool -}{ - {"all", true, true}, - {"all,", true, true}, - {"all,native", true, true}, - {"native", false, false}, - {"native,php", true, false}, - {"native,python", false, true}, - {"native,php,python", true, true}, -} - -// tests expected to fail -var tracersTestsFail = []struct { - in string -}{ - {"NNative"}, - {"foo"}, -} - -func TestParseTracers(t *testing.T) { - for _, tt := range tracersTestsOK { - tt := tt - in := tt.in - t.Run(tt.in, func(t *testing.T) { - include, err := parseTracers(in) - - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - - if tt.php != include[config.PHPTracer] { - t.Errorf("Expected PHPTracer enabled by %s", in) - } - - if tt.python != include[config.PythonTracer] { - t.Errorf("Expected PythonTracer enabled by %s", in) - } - }) - } - - for _, tt := range tracersTestsFail { - in := tt.in - t.Run(tt.in, func(t *testing.T) { - if _, err := parseTracers(in); err == nil { - t.Errorf("Unexpected success with '%s'", in) - } - }) - } -} diff --git a/libpf/memorydebug/README.md b/memorydebug/README.md similarity index 100% rename from libpf/memorydebug/README.md rename to memorydebug/README.md diff --git a/libpf/memorydebug/memorydebug_debug.go b/memorydebug/memorydebug_debug.go similarity index 100% rename from libpf/memorydebug/memorydebug_debug.go rename to memorydebug/memorydebug_debug.go diff --git a/libpf/memorydebug/memorydebug_release.go b/memorydebug/memorydebug_release.go similarity index 100% rename from libpf/memorydebug/memorydebug_release.go rename to memorydebug/memorydebug_release.go diff --git a/libpf/memorydebug/memorydebug_test.go b/memorydebug/memorydebug_test.go similarity index 100% rename from libpf/memorydebug/memorydebug_test.go rename to memorydebug/memorydebug_test.go diff --git a/metrics/agentmetrics/agent.go b/metrics/agentmetrics/agent.go index 457274ae..ecc058df 100644 --- a/metrics/agentmetrics/agent.go +++ b/metrics/agentmetrics/agent.go @@ -12,10 +12,9 @@ import ( "runtime" "time" - "golang.org/x/sys/unix" - - "github.com/elastic/otel-profiling-agent/libpf/periodiccaller" "github.com/elastic/otel-profiling-agent/metrics" + "github.com/elastic/otel-profiling-agent/periodiccaller" + "golang.org/x/sys/unix" log "github.com/sirupsen/logrus" ) diff --git a/metrics/agentmetrics/agent_test.go b/metrics/agentmetrics/agent_test.go index 501078b2..d0c79706 100644 --- a/metrics/agentmetrics/agent_test.go +++ b/metrics/agentmetrics/agent_test.go @@ -10,6 +10,8 @@ import ( "testing" "golang.org/x/sys/unix" + + "github.com/stretchr/testify/assert" ) func TestTimeDelta(t *testing.T) { @@ -53,9 +55,7 @@ func TestTimeDelta(t *testing.T) { tc := tc t.Run(name, func(t *testing.T) { delta := timeDelta(tc.now, tc.prev) - if delta != tc.delta { - t.Fatalf("Expected %d Got %d", tc.delta, delta) - } + assert.Equal(t, tc.delta, delta) }) } } diff --git a/metrics/cpumetrics/cpu.go b/metrics/cpumetrics/cpu.go new file mode 100644 index 00000000..7d5da00f --- /dev/null +++ b/metrics/cpumetrics/cpu.go @@ -0,0 +1,275 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +/* +Package cpumetrics is responsible for measuring CPU metrics. + +The package is assumed and designed to run only once in host agent (singleton). +The "downside" is that we can not Start() this package twice - that would even +be considered a bug as we would store the same metrics twice in the database. + +The directory structure is + + cpumetrics/ + ├── cpu.go + ├── cpu_test.go + └── testdata + ├── procstat.empty + ├── procstat.garbage + └── procstat.ok + +The CPU usage reporting is started after the metrics package has been started +by calling the Start() function with a context and an interval argument. + +The context variable allows for explicit cancellation of the background goroutine +in case defer doesn't work, e.g. when the application is stopped by os.Exit(). + +The interval specifies in which intervals the CPU usage is collected. We agreed upon +1x per second. This interval is independent of the reporting interval, which is how often +buffered metrics data is sent to the backend (collection agent / storage). + +Start returns a Stop() function that should be called to release package resources. + +Example code from main.go to start CPU metric reporting with a 1s interval: + + defer cpumetrics.Start(mainCtx, 1*time.Second)() + +The description of '/proc/stat' can be found at + + https://man7.org/linux/man-pages/man5/proc.5.html. +*/ +package cpumetrics + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "math" + "os" + "runtime" + "strconv" + "strings" + "sync" + "time" + + "github.com/elastic/otel-profiling-agent/periodiccaller" + "github.com/elastic/otel-profiling-agent/stringutil" + + log "github.com/sirupsen/logrus" + + "github.com/elastic/otel-profiling-agent/metrics" + + sysconf "github.com/tklauser/go-sysconf" +) + +var ( + // scannerBuffer is a static buffer used by the scanner in parse() + // The reason we can use a static buffer because the code is guaranteed concurrency free. + scannerBuffer [8192]byte + + // procStatFile is the file name to read the CPU usage values from. + procStatFile = "/proc/stat" + + // file is the open file to parse user and system CPU load from + file *os.File + + // nCPUs is the number of configured CPUs (not the number of online CPUs) + nCPUs uint32 + + // userHZ is the ticks per second, the unit for the values in /proc/stat + userHZ uint32 + + // prevUser is the previously measured user time in ticks (userHZ units) + prevUser uint64 + + // prevSystem is the previously measured system time in ticks (userHZ units) + prevSystem uint64 + + // prevTime is the timestamp of the previous measurement + prevTime time.Time + + // onceStart helps to make this package a thread-safe singleton + onceStart sync.Once + + // onceStop helps to make this package a thread-safe singleton + onceStop sync.Once +) + +// initialize contains the one-time initialization - called from Report(). +func initialize() error { + var err error + + file, err = os.Open(procStatFile) + if err != nil { + return fmt.Errorf("failed to initialize: %v", err) + } + + // From 'man 5 proc': + // The amount of time, measured in units of USER_HZ + // (1/100ths of a second on most architectures), use + // sysconf(_SC_CLK_TCK) to obtain the right value). + tmpUserHZ, err := sysconf.Sysconf(sysconf.SC_CLK_TCK) + if err != nil { + log.Warnf("Failed to get value of UserHZ / SC_CLK_TCK (using 100 as default)") + tmpUserHZ = 100 // default on most Linux systems + } + userHZ = uint32(tmpUserHZ) + + tmpNCPUs := int64(runtime.NumCPU()) + if tmpNCPUs < 0 { + log.Warnf("Failed to get number of available CPUs (using 1 as default)") + tmpNCPUs = 1 + } + nCPUs = uint32(tmpNCPUs) + + log.Debugf("userHZ %d nCPUs %d", userHZ, nCPUs) + + // Initialize prevUser and prevSystem for further delta calculations. + // If we don't do this we'll see a single 100% spike in the metrics. + if prevUser, prevSystem, err = parse(); err != nil { + return fmt.Errorf("failed to init CPU delta values: %v", err) + } + + return nil +} + +// parse parses and returns the system and user CPU usage values. +// The format of /proc/stat is described in +// https://man7.org/linux/man-pages/man5/proc.5.html. +func parse() (user, system uint64, err error) { + // rewind procStatFile instead of open/close at every interval + if _, err = file.Seek(0, io.SeekStart); err != nil { + return 0, 0, err + } + + scanner := bufio.NewScanner(file) + // We only want to read the first line which fits very likely into buf. + // The fallback is to support up to 8192 bytes per line. + scanner.Buffer(scannerBuffer[:], cap(scannerBuffer)) + + 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()) + + if !strings.HasPrefix(line, "cpu ") { + continue + } + + // Avoid heap allocations here - do not use strings.FieldsN() + var fields [5]string + n := stringutil.FieldsN(line, fields[:]) + if n < 4 { + return 0, 0, fmt.Errorf("failed to find at least 4 fields in '%s'", line) + } + + if user, err = strconv.ParseUint(fields[1], 10, 64); err != nil { + return 0, 0, errors.New("failed to parse CPU user value") + } + + if system, err = strconv.ParseUint(fields[3], 10, 64); err != nil { + return 0, 0, errors.New("failed to parse CPU system value") + } + + return user, system, nil + } + + if err = scanner.Err(); err != nil { + return 0, 0, fmt.Errorf("failed to parse %s: %v", procStatFile, err) + } + + return 0, 0, fmt.Errorf("failed to find 'cpu' keyword in %s", procStatFile) +} + +// getCPUUsage measures and calculates the average CPU usage as percentage value measured between +// the previous (successful) call and now. +func getCPUUsage() (pAvgCPU uint16, err error) { + user, system, err := parse() + if err != nil { + return 0, err + } + + now := time.Now() + duration := now.Sub(prevTime) + prevTime = now + + var load uint64 + + // handle wrap-around of user value + if user < prevUser { + log.Debugf("User wrap-around detected %d -> %d", prevUser, user) + load = (math.MaxUint64 - prevUser) + user + 1 + } else { + load = user - prevUser + } + + // handle wrap-around of system value + if system < prevSystem { + log.Debugf("System wrap-around detected %d -> %d", prevSystem, system) + load += (math.MaxUint64 - prevSystem) + system + 1 + } else { + load += system - prevSystem + } + + prevUser = user + prevSystem = system + + // Calculate the maximum possible value for the elapsed time (duration). + // nCPUs*userHZ: The max. number of ticks per second. + // duration / time.Second: Time elapsed in seconds. + max := float64(nCPUs*userHZ) * (float64(duration) / float64(time.Second)) + + // Calculate the % value of the CPU usage with rounding. + if max > 0 { + pAvgCPU = uint16(float64(load*100)/max + 0.5) + if pAvgCPU > 100 { + pAvgCPU = 100 + } + } + + return pAvgCPU, nil +} + +// report get the actual measurement and reports it to the metrics package. +func report() { + if value, err := getCPUUsage(); err != nil { + log.Errorf("Failed to measure CPU metrics: %v", err) + } else { + metrics.Add(metrics.IDCPUUsage, metrics.MetricValue(value)) + } +} + +// Start starts the CPU metric retrieval and reporting. +func Start(ctx context.Context, interval time.Duration) func() { + var stopPeriodic func() + + onceStart.Do(func() { // <-- atomic, does not allow repeating + err := initialize() + if err != nil { + log.Errorf("Failed to initialize CPU metrics: %v", err) + return + } + + if interval != 0 { + // Start CPU metric reporting, report every second. + log.Infof("Start CPU metrics") + stopPeriodic = periodiccaller.Start(ctx, interval, report) + } + }) + + // return a one-time close function to avoid leaks + return func() { + onceStop.Do(func() { // <-- atomic, does not allow repeating + if stopPeriodic != nil { + stopPeriodic() + file.Close() + } + }) + } +} diff --git a/metrics/cpumetrics/cpu_test.go b/metrics/cpumetrics/cpu_test.go new file mode 100644 index 00000000..b0b1e5bf --- /dev/null +++ b/metrics/cpumetrics/cpu_test.go @@ -0,0 +1,254 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package cpumetrics + +import ( + "context" + "fmt" + "math" + "os" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestUsage(t *testing.T) { + // Other tests can set procStatFile to something else. But for this test, + // we explicitly want to use its original value. + procStatFile = "/proc/stat" + + defer Start(context.TODO(), 0)() + + // check internal values + require.NotEqual(t, 0, userHZ, "userHZ not set") + require.NotEqual(t, 0, nCPUs, "nCPUs not set") + + _, err := getCPUUsage() + require.NoError(t, err) + + // wait to get a 1s value + time.Sleep(1 * time.Second) + + avg, err := getCPUUsage() + require.NoError(t, err) + + t.Logf("CPU Usage: %d%%\n", avg) + require.LessOrEqual(t, avg, uint16(100)) +} + +func TestParse(t *testing.T) { + tests := map[string]struct { + inputFile string + err bool + }{ + "successful file parsing of /proc/stat": { + inputFile: "/proc/stat", + err: false}, + "successful file parsing of procstat.ok": { + inputFile: "testdata/procstat.ok", + err: false}, + "unparsable file content": { + inputFile: "testdata/procstat.garbage", + err: true}, + "empty file content": { + inputFile: "testdata/procstat.empty", + err: true}, + "not existing file": { + inputFile: "testdata/__does-not-exist__", + err: true}, + } + var err error + + for name, testcase := range tests { + testcase := testcase + + t.Run(name, func(t *testing.T) { + procStatFile = testcase.inputFile + file, err = os.Open(procStatFile) + if err != nil { + require.Truef(t, testcase.err, "open failed: %v", err) + return + } + defer file.Close() + + // Start calls parse() internally and reports any error + _, err := getCPUUsage() + if testcase.err { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +// createProcStat creates an ad-hoc /proc/stat like file. +func createProcStat(t *testing.T, user, system uint64, + addLongLineBeforeCPU, addLongLineAfterCPU bool) string { + f, err := os.CreateTemp("", "*_procstat") + require.NoError(t, err) + defer f.Close() + + addLongLine := func() { + _, err2 := fmt.Fprintf(f, "intr%s\n", strings.Repeat(" 0", 2048)) + require.NoError(t, err2) + } + + if addLongLineBeforeCPU { + addLongLine() + } + + _, err = fmt.Fprintf(f, "cpu %d 0 %d\n", user, system) + require.NoError(t, err) + + if addLongLineAfterCPU { + addLongLine() + } + + return f.Name() +} + +func TestGet(t *testing.T) { + tests := map[string]struct { + // userHz and nCPUs represent the host specific values that are set in Start() + userHZ uint32 + nCPUs uint32 + // prevUser and prevSystem simulate the values from the previous call of getCPUUsage() + prevUser uint64 + prevSystem uint64 + // user and system are put into the dynamically created procStatFile + user uint64 + system uint64 + // expAvgCPU represents the expected return from getCPUUsage() + expAvgCPU uint16 + // addLongLineBeforeCPU indicates whether we add a very long line before 'cpu ...' + addLongLineBeforeCPU bool + // addLongLineAfterCPU indicates whether we add a very long line after 'cpu ...' + addLongLineAfterCPU bool + }{ + "0% CPU": { + userHZ: 100, + nCPUs: 2, + prevUser: 0, + prevSystem: 0, + user: 0, + system: 0, + expAvgCPU: 0, + }, + "50% CPU": { + userHZ: 100, + nCPUs: 2, + prevUser: 500, + prevSystem: 700, + user: 550, + system: 750, + expAvgCPU: 50, + }, + "100% CPU": { + userHZ: 100, + nCPUs: 2, + prevUser: 0, + prevSystem: 0, + user: 75, + system: 125, + expAvgCPU: 100, + }, + "100% CPU (timing glitch)": { + userHZ: 100, + nCPUs: 2, + prevUser: 0, + prevSystem: 0, + user: 75, + system: 135, // basically at 105%, still expect report of 100% + expAvgCPU: 100, + }, + "User wrap-around": { + userHZ: 100, + nCPUs: 2, + prevUser: math.MaxUint64 - 25, + prevSystem: 0, + user: 24, + system: 0, + expAvgCPU: 25, + }, + "System wrap-around": { + userHZ: 100, + nCPUs: 2, + prevUser: 0, + prevSystem: math.MaxUint64 - 25, + user: 0, + system: 24, + expAvgCPU: 25, + }, + "Double wrap-around": { + userHZ: 100, + nCPUs: 2, + prevUser: math.MaxUint64 - 25, + prevSystem: math.MaxUint64 - 25, + user: 24, + system: 24, + expAvgCPU: 50, + }, + "Many cores, high userHZ": { + userHZ: 1000, + nCPUs: 1024, + prevUser: 0, + prevSystem: 0, + user: (1000 * 1024 / 100) * 15, // 15% user load + system: (1000 * 1024 / 100) * 20, // 20% system load + expAvgCPU: 35, // 35% total load + }, + "50% CPU LongLineBefore": { + userHZ: 100, + nCPUs: 2, + prevUser: 500, + prevSystem: 700, + user: 550, + system: 750, + expAvgCPU: 50, + addLongLineBeforeCPU: true, + }, + "50% CPU LongLineAfter": { + userHZ: 100, + nCPUs: 2, + prevUser: 500, + prevSystem: 700, + user: 550, + system: 750, + expAvgCPU: 50, + addLongLineAfterCPU: true, + }, + } + var err error + + for name, testcase := range tests { + name := name + tc := testcase + t.Run(name, func(t *testing.T) { + testProcStatFile := createProcStat(t, tc.user, tc.system, + tc.addLongLineBeforeCPU, tc.addLongLineAfterCPU) + defer os.Remove(testProcStatFile) + + file, err = os.Open(testProcStatFile) + require.NoError(t, err) + defer file.Close() + + procStatFile = testProcStatFile + userHZ = tc.userHZ + nCPUs = tc.nCPUs + prevUser = tc.prevUser + prevSystem = tc.prevSystem + prevTime = time.Now().Add(-1 * time.Second) + + avgCPU, err := getCPUUsage() + require.NoError(t, err) + require.Equal(t, tc.expAvgCPU, avgCPU) + }) + } +} diff --git a/utils/coredump/testdata/amd64/.gitkeep b/metrics/cpumetrics/testdata/procstat.empty similarity index 100% rename from utils/coredump/testdata/amd64/.gitkeep rename to metrics/cpumetrics/testdata/procstat.empty diff --git a/metrics/cpumetrics/testdata/procstat.garbage b/metrics/cpumetrics/testdata/procstat.garbage new file mode 100644 index 00000000..9ce7649d --- /dev/null +++ b/metrics/cpumetrics/testdata/procstat.garbage @@ -0,0 +1 @@ +garbage diff --git a/metrics/cpumetrics/testdata/procstat.ok b/metrics/cpumetrics/testdata/procstat.ok new file mode 100644 index 00000000..074ad737 --- /dev/null +++ b/metrics/cpumetrics/testdata/procstat.ok @@ -0,0 +1,16 @@ +cpu 15290291 15822 3206606 232724510 102983 0 1523631 0 572593 0 +cpu0 2018113 1856 392759 29002642 13048 0 754144 0 97615 0 +cpu1 2191900 2058 394655 28832841 12754 0 374831 0 123522 0 +cpu2 1909336 1854 390423 29087714 12595 0 166354 0 78985 0 +cpu3 2324574 1996 397620 28709526 12905 0 79848 0 116478 0 +cpu4 1738388 2039 397555 29292276 13078 0 42665 0 7695 0 +cpu5 1777820 2013 385714 29274969 12861 0 31727 0 25127 0 +cpu6 1585775 2046 452426 29232388 12998 0 52710 0 25993 0 +cpu7 1744382 1958 395450 29292150 12741 0 21349 0 97175 0 +intr 728238070 8 828 0 0 0 0 0 0 0 103461 0 0 3673 0 572 0 0 28274 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 168 49500016 1539046 1660563 1590534 1580307 1605432 1579996 1510568 1603306 0 592 592 0 0 0 0 0 0 0 0 0 0 0 0 0 0 109621112 40 2364 762524 2685 2469 294 4199 454 2309 3266 494 4697 57758 572 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +ctxt 1652577503 +btime 1591088718 +processes 1364813 +procs_running 2 +procs_blocked 0 +softirq 710165713 39866846 219493404 61840 14941220 174 0 525231 222391821 220 212884957 diff --git a/metrics/doc.go b/metrics/doc.go new file mode 100644 index 00000000..8d916ea9 --- /dev/null +++ b/metrics/doc.go @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +/* +Package metrics contains the code for receiving and reporting host metrics. + +The design document can be found at + + https://docs.google.com/document/d/1nGxf-J0gVNqxgGDqJNdJvcw_VBrUa8PVSZYmHj2BH1s + +This is the implementation of Proposal C from the design doc. + +Example code to initialize metrics reporting: + + defer metrics.Start(mainCtx)() + +# Aim + +The UI should allow to quickly detect unusual issues. These could be spikes in metrics, +one or more processes eating 100% CPU over a long time, slowness issues over time, +unusual increase in error counters, etc. + +We should add user stories here. + +# Directory Structure + +The current directory structure looks like + + metrics + ├── [sub packages] + ├── doc.go // this file + ├── metrics.go // implement Start(), Add() and AddSlice() + ├── metrics_test.go // tests the metrics package + └── types.go // definitions of metric ids and Metric, MetricID, MetricValue + +# CPU Usage + +The CPU usage (sum of user + system read from /proc/stat) is a first 'sub package' +of the metrics package. + +The directory structure is + + metrics + └──cpumetrics/ + ├── cpu.go + ├── cpu_test.go + └── testdata + ├── procstat.empty + ├── procstat.garbage + └── procstat.ok + +# Comments + +Clickhouse stores data "columnar" (which means data for a column will be stored sequentially), +and you can chose different encodings for it. +For example: If you have data that very rarely changes, you can tell clickhouse to encode +only the deltas and put ZSTD compression on that. Essentially all you need to do is append +CODEC(Delta, ZSTD) to your column declaration in the schema. + +Clickhouse Overwiev + + https://www.altinity.com/blog/2019/7/new-encodings-to-improve-clickhouse + +Clickhouse Docs + + https://clickhouse.tech/docs/en/sql-reference/statements/create/#create-query-specialized-codecs +*/ +package metrics diff --git a/metrics/iometrics/io.go b/metrics/iometrics/io.go new file mode 100644 index 00000000..7793d3b1 --- /dev/null +++ b/metrics/iometrics/io.go @@ -0,0 +1,264 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +/* +Package iometrics is responsible for measuring I/O metrics. + +The package is assumed and designed to run only once in host agent (singleton). +The "downside" is that we can not Start() this package twice - that would even +be considered a bug as we would store the same metrics twice in the database. + +The directory structure is + + iometrics/ + ├── io.go + ├── io_test.go + └── testdata + ├── diskstats.empty + ├── diskstats.garbage + └── diskstats.ok + +The I/O metrics reporting is started after the metrics package has been started +by calling the Start() function with a context and an interval argument. + +The context variable allows for explicit cancellation of the background goroutine +in case defer doesn't work, e.g. when the application is stopped by os.Exit(). + +The interval specifies in which intervals the I/O metrics are collected. We agreed upon +1x per second. This interval is independent of the reporting interval, which is how often +buffered metrics data is sent to the backend (collection agent / storage). + +Start returns a Stop() function that should be called to release package resources. + +Example code from main.go to start I/O metrics reporting with a 1s interval: + + defer iometrics.Start(mainCtx, 1*time.Second)() + +The description of '/proc/diskstats' can be found at + + https://www.kernel.org/doc/Documentation/iostats.txt. +*/ +package iometrics + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "math" + "os" + "strconv" + "sync" + "time" + + "github.com/elastic/otel-profiling-agent/periodiccaller" + "github.com/elastic/otel-profiling-agent/stringutil" + + log "github.com/sirupsen/logrus" + + "github.com/elastic/otel-profiling-agent/metrics" +) + +var ( + // scannerBuffer is a static buffer used by the scanner in parse(). + // The reason we can use a static buffer because the code is guaranteed concurrency free. + scannerBuffer [1024]byte + + // procDiskstatsFile is the file name to read the IO metrics values from. + procDiskstatsFile = "/proc/diskstats" + + // file is the open file to parse I/O metrics from + file *os.File + + // prevThroughput is the previously measured I/O throughput (blocks read+write) + prevThroughput uint64 + + // prevIODuration is the previously measured I/O duration in "weighted # of milliseconds" + prevIODuration uint64 + + // prevTime is the timestamp of the previous measurement + prevTime time.Time + + // onceStart helps to make this package a thread-safe singleton + onceStart sync.Once + + // onceStop helps to make this package a thread-safe singleton + onceStop sync.Once +) + +const bytesPerBlock = 512 + +// initialize contains the one-time initialization - called from report(). +func initialize() error { + var err error + + file, err = os.Open(procDiskstatsFile) + if err != nil { + return fmt.Errorf("failed to initialize: %v", err) + } + + // Initialize prevUser and prevSystem for further delta calculations. + // If we don't do this we'll see a single 100% spike in the metrics. + if prevThroughput, prevIODuration, err = parse(); err != nil { + return fmt.Errorf("failed to init I/O delta values: %v", err) + } + + prevTime = time.Now() + + return nil +} + +// parse returns the I/O throughput and duration values parsed from /proc/diskstats. +// I/O throughput is measured in blocks read and written. +// duration is measured in millisends spent for read and write. +// The format of /proc/diskstats is described in +// https://www.kernel.org/doc/Documentation/iostats.txt +func parse() (totalThroughput, totalDuration uint64, err error) { + // rewind procDiskstatsFile instead of open/close at every interval + if _, err = file.Seek(0, io.SeekStart); err != nil { + return 0, 0, err + } + + scanner := bufio.NewScanner(file) + // We have to parse the whole file. Since these are normally not too big and the lines are + // not long, a good default value may be 1024 (length of buf). + // 4096 is the scanner's internal default value for the buffer size. + scanner.Buffer(scannerBuffer[:], 4096) + ok := false + + 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 [15]string + n := stringutil.FieldsN(line, fields[:]) + if n < 14 { + continue + } + + // we are only interested in devices (minor ID 0) + if fields[1] != "0" { + continue + } + + ioRead, err := strconv.ParseUint(fields[5], 10, 64) + if err != nil { + return 0, 0, errors.New("failed to parse read blocks") + } + + ioWrite, err := strconv.ParseUint(fields[9], 10, 64) + if err != nil { + return 0, 0, errors.New("failed to parse written blocks") + } + + ioDuration, err := strconv.ParseUint(fields[13], 10, 64) + if err != nil { + return 0, 0, errors.New("failed to parse I/O duration") + } + + totalThroughput += ioRead + ioWrite + totalDuration += ioDuration + ok = true + } + if !ok { + return 0, 0, errors.New("no data found") + } + return totalThroughput, totalDuration, nil +} + +// Get returns the I/O throughput and I/O duration measured between +// the previous (successful) call and now. +func getIOData(now time.Time) (avgThroughput, avgDuration uint64, err error) { + var deltaThroughput, deltaDuration uint64 + + ioThroughput, ioDuration, err := parse() + if err != nil { + return 0, 0, err + } + + duration := now.Sub(prevTime) + prevTime = now + + // handle wrap-around + if ioThroughput < prevThroughput { + log.Debugf("I/O throughput wrap-around detected %d -> %d", prevThroughput, ioThroughput) + deltaThroughput = (math.MaxUint64 - prevThroughput) + ioThroughput + 1 + } else { + deltaThroughput = ioThroughput - prevThroughput + } + + // handle wrap-around + if ioDuration < prevIODuration { + log.Debugf("I/O duration wrap-around detected %d -> %d", prevIODuration, ioDuration) + deltaDuration = (math.MaxUint64 - prevIODuration) + ioDuration + 1 + } else { + deltaDuration = ioDuration - prevIODuration + } + + prevThroughput = ioThroughput + prevIODuration = ioDuration + + // scaling regarding the interval duration + scale := float64(time.Second) / float64(duration) + + // average throughput delta as bytes + avgThroughput = uint64(scale * float64(deltaThroughput*bytesPerBlock)) + + // average I/O duration delta as milliseconds + avgDuration = uint64(scale * float64(deltaDuration)) + + return avgThroughput, avgDuration, nil +} + +// report get the actual measurement and reports it to the metrics package. +func report() { + if avgThroughput, avgDuration, err := getIOData(time.Now()); err != nil { + log.Errorf("Failed to measure I/O metrics: %v", err) + } else { + metrics.AddSlice([]metrics.Metric{ + { + ID: metrics.IDIOThroughput, + Value: metrics.MetricValue(avgThroughput), + }, + { + ID: metrics.IDIODuration, + Value: metrics.MetricValue(avgDuration), + }, + }) + } +} + +// Start starts the I/O metric retrieval and reporting. +func Start(ctx context.Context, interval time.Duration) func() { + var stopPeriodic func() + + onceStart.Do(func() { // <-- atomic, does not allow repeating + if err := initialize(); err != nil { + log.Errorf("Failed to initialize I/O metrics: %v", err) + return + } + + if interval != 0 { + // Start I/O metric reporting, report every second. + log.Infof("Start I/O metrics") + stopPeriodic = periodiccaller.Start(ctx, interval, report) + } + }) + + // return a one-time close function to avoid leaks + return func() { + onceStop.Do(func() { // <-- atomic, does not allow repeating + if stopPeriodic != nil { + stopPeriodic() + file.Close() + } + }) + } +} diff --git a/metrics/iometrics/io_test.go b/metrics/iometrics/io_test.go new file mode 100644 index 00000000..53afff99 --- /dev/null +++ b/metrics/iometrics/io_test.go @@ -0,0 +1,187 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package iometrics + +import ( + "context" + "fmt" + "math" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUsage(t *testing.T) { + // Other tests can set procDiskstatsFile to something else. But for this test, + // we explicitly want to use its original value. + procDiskstatsFile = "/proc/diskstats" + + defer Start(context.TODO(), 0)() + + _, _, err := getIOData(time.Now()) + require.NoError(t, err) + + // wait to get a 1s value + time.Sleep(1 * time.Second) + + throughput, duration, err := getIOData(time.Now()) + require.NoError(t, err) + + t.Logf("I/O: throughput %d%% duration %d\n", throughput, duration) +} + +func TestParse(t *testing.T) { + tests := map[string]struct { + inputFile string + err bool + }{ + "successful file parsing of /proc/diskstats": { + inputFile: "/proc/diskstats", + err: false}, + "successful file parsing of procstat.ok": { + inputFile: "testdata/diskstats.ok", + err: false}, + "unparsable file content": { + inputFile: "testdata/diskstats.garbage", + err: true}, + "empty file content": { + inputFile: "testdata/diskstats.empty", + err: true}, + "not existing file": { + inputFile: "testdata/__does-not-exist__", + err: true}, + } + var err error + + for name, testcase := range tests { + testcase := testcase + + t.Run(name, func(t *testing.T) { + procDiskstatsFile = testcase.inputFile + file, err = os.Open(procDiskstatsFile) + if err != nil { + require.Truef(t, testcase.err, "failed to open %s: %v", + procDiskstatsFile, err) + return + } + defer file.Close() + + // Start calls parse() internally and reports any error + _, _, err := getIOData(time.Now()) + if testcase.err { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +// createProcDiskstats creates an ad-hoc /proc/stat like file +func createProcDiskstats(t *testing.T, throughput, duration uint64) string { + f, err := os.CreateTemp("", "*_diskstats") + require.NoError(t, err) + defer f.Close() + + _, err = fmt.Fprintf(f, "1 0 hda 0 0 %d 0 0 0 %d 0 0 0 %d\n", + throughput/2, throughput-(throughput/2), duration) + require.NoError(t, err) + + return f.Name() +} + +func TestGet(t *testing.T) { + tests := map[string]struct { + // prevThroughput simulates the value from the previous call of getIOData() + prevThroughput uint64 + // throughput is put into the dynamically created input file + throughput uint64 + + // prevDuration simulates the value from the previous call of getIOData() + prevDuration uint64 + // duration is put into the dynamically created input file + duration uint64 + + // expThroughput represents the expected return from getIOData() + expThroughput uint64 + // expDuration represents the expected return from getIOData() + expDuration uint64 + }{ + "0 Throughput, 0 wait": { + prevThroughput: 0, + prevDuration: 0, + throughput: 0, + duration: 0, + expThroughput: 0, + expDuration: 0, + }, + "Test #2": { + prevThroughput: 500, + throughput: 550, + prevDuration: 150, + duration: 750, + expThroughput: 25600, + expDuration: 600, + }, + "Kilo throughput": { + prevThroughput: 0, + throughput: 2000, + duration: 135, + expThroughput: 1024000, + expDuration: 135, + }, + "Mega throughput": { + prevThroughput: 0, + throughput: 2000000, + duration: 135, + expThroughput: 1024000000, + expDuration: 135, + }, + "Giga throughput": { + prevThroughput: 0, + throughput: 2000000000, + duration: 135, + expThroughput: 1024000000000, + expDuration: 135, + }, + "Throughput wrap-around": { + prevThroughput: math.MaxUint64 - 25, + throughput: 24, + duration: 0, + expThroughput: 25600, + expDuration: 0, + }, + } + var err error + + for name, testcase := range tests { + name := name + tc := testcase + t.Run(name, func(t *testing.T) { + testProcDiskstatsFile := createProcDiskstats(t, tc.throughput, tc.duration) + defer os.Remove(testProcDiskstatsFile) + + file, err = os.Open(testProcDiskstatsFile) + require.NoError(t, err) + defer file.Close() + + now := time.Now() + procDiskstatsFile = testProcDiskstatsFile + prevIODuration = tc.prevDuration + prevThroughput = tc.prevThroughput + prevTime = now.Add(-1 * time.Second) + + throughput, duration, err := getIOData(now) + require.NoError(t, err) + assert.Equal(t, tc.expThroughput, throughput) + assert.Equal(t, tc.expDuration, duration) + }) + } +} diff --git a/utils/coredump/testdata/arm64/.gitkeep b/metrics/iometrics/testdata/diskstats.empty similarity index 100% rename from utils/coredump/testdata/arm64/.gitkeep rename to metrics/iometrics/testdata/diskstats.empty diff --git a/metrics/iometrics/testdata/diskstats.garbage b/metrics/iometrics/testdata/diskstats.garbage new file mode 100644 index 00000000..9ce7649d --- /dev/null +++ b/metrics/iometrics/testdata/diskstats.garbage @@ -0,0 +1 @@ +garbage diff --git a/metrics/iometrics/testdata/diskstats.ok b/metrics/iometrics/testdata/diskstats.ok new file mode 100644 index 00000000..056fd313 --- /dev/null +++ b/metrics/iometrics/testdata/diskstats.ok @@ -0,0 +1,7 @@ + 259 0 nvme0n1 480377 157691 35931161 139239 4688164 6159082 169867696 9222964 0 1042048 9384799 0 0 0 0 60189 22596 + 259 1 nvme0n1p1 192 0 12704 50 2 0 2 0 0 108 50 0 0 0 0 0 0 + 259 2 nvme0n1p2 202 141 11108 34 11 0 22 29 0 124 64 0 0 0 0 0 0 + 259 3 nvme0n1p3 479905 157550 35902757 139134 4627962 6159082 169867672 9199716 0 1038348 9338850 0 0 0 0 0 0 + 254 0 dm-0 637365 0 35900290 293372 10816019 0 169867672 108525380 0 1047664 108818752 0 0 0 0 0 0 + 254 1 dm-1 636582 0 35888042 293232 10795316 0 170098456 98162016 0 1046616 98455248 0 0 0 0 0 0 + 254 2 dm-2 733 0 9592 1060 4754 0 46208 2324732 0 7264 2325792 0 0 0 0 0 0 diff --git a/metrics/metrics.go b/metrics/metrics.go index d8fd8813..7b8ef418 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -4,12 +4,10 @@ * See the file "LICENSE" for details. */ -// Package metrics contains the code for receiving and reporting host metrics. package metrics import ( "sync" - "time" log "github.com/sirupsen/logrus" @@ -82,7 +80,7 @@ func report() { // This ensures that the buffered metrics from the previous timestamp are sent // with the correctly assigned TSMetric.Timestamp. func AddSlice(newMetrics []Metric) { - now := libpf.UnixTime32(time.Now().Unix()) + now := libpf.UnixTime32(libpf.NowAsUInt32()) mutex.Lock() defer mutex.Unlock() diff --git a/metrics/metrics.json b/metrics/metrics.json index 35bb9181..2cc57f9b 100644 --- a/metrics/metrics.json +++ b/metrics/metrics.json @@ -1435,6 +1435,7 @@ "id": 201 }, { + "obsolete": true, "description": "Number of cache hits in the stack delta provider", "type": "counter", "name": "StackDeltaProviderCacheHit", @@ -1442,6 +1443,7 @@ "id": 202 }, { + "obsolete": true, "description": "Number of cache misses in the stack delta provider", "type": "counter", "name": "StackDeltaProviderCacheMiss", @@ -1823,5 +1825,110 @@ "name": "UnwindHotspotErrLrUnwindingMidTrace", "field": "bpf.hotspot.errors.lr_unwinding_mid_trace", "id": 256 + }, + { + "description": "Number of failures to get TSD base for APM correlation", + "type": "counter", + "name": "UnwindApmIntErrReadTsdBase", + "field": "bpf.apmint.errors.read_tsd_base", + "id": 257 + }, + { + "description": "Number of failures read the APM correlation pointer", + "type": "counter", + "name": "UnwindApmIntErrReadCorrBufPtr", + "field": "bpf.apmint.errors.read_corr_buf_ptr", + "id": 258 + }, + { + "description": "Number of failures read the APM correlation buffer", + "type": "counter", + "name": "UnwindApmIntErrReadCorrBuf", + "field": "bpf.apmint.errors.read_corr_buf", + "id": 259 + }, + { + "description": "Number of successful reads of APM correlation info", + "type": "counter", + "name": "UnwindApmIntReadSuccesses", + "field": "bpf.apmint.read.successes", + "id": 260 + }, + { + "description": "Number of attempted dotnet unwinds", + "type": "counter", + "name": "UnwindDotnetAttempts", + "field": "bpf.dotnet.attempts", + "id": 261 + }, + { + "description": "Number of unwound dotnet frames", + "type": "counter", + "name": "UnwindDotnetFrames", + "field": "bpf.dotnet.frames", + "id": 262 + }, + { + "description": "Number of times we didn't find an entry for this process in the dotnet process info array", + "type": "counter", + "name": "UnwindDotnetErrNoProcInfo", + "field": "bpf.dotnet.errors.no_proc_info", + "id": 263 + }, + { + "description": "Number of failures to read dotnet frame pointer data", + "type": "counter", + "name": "UnwindDotnetErrBadFP", + "field": "bpf.dotnet.errors.bad_fp", + "id": 264 + }, + { + "description": "Number of failures to read dotnet CodeHeader", + "type": "counter", + "name": "UnwindDotnetErrCodeHeader", + "field": "bpf.dotnet.errors.code_header", + "id": 265 + }, + { + "description": "Number of failures to unwind dotnet frame due to large code size", + "type": "counter", + "name": "UnwindDotnetErrCodeTooLarge", + "field": "bpf.dotnet.errors.code_too_large", + "id": 266 + }, + { + "description": "Number of successfully symbolized dotnet frames", + "type": "counter", + "name": "DotnetSymbolizationSuccesses", + "field": "agent.dotnet.symbolization.successes", + "id": 267 + }, + { + "description": "Number of dotnet frames that failed symbolization", + "type": "counter", + "name": "DotnetSymbolizationFailures", + "field": "agent.dotnet.symbolization.failures", + "id": 268 + }, + { + "description": "Number of cache hits for dotnet AddrToMethod", + "type": "counter", + "name": "DotnetAddrToMethodHit", + "field": "agent.dotnet.addr_to_method.hits", + "id": 269 + }, + { + "description": "Number of cache misses for dotnet AddrToMethod", + "type": "counter", + "name": "DotnetAddrToMethodMiss", + "field": "agent.dotnet.addr_to_method.misses", + "id": 270 + }, + { + "description": "Number of times the stack delta provider succeeded to extract stack deltas", + "type": "counter", + "name": "StackDeltaProviderSuccess", + "field": "agent.stack_delta_extraction.success", + "id": 271 } ] diff --git a/metrics/metrics_test.go b/metrics/metrics_test.go new file mode 100644 index 00000000..c6e4cfcb --- /dev/null +++ b/metrics/metrics_test.go @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package metrics + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +type fakeReporter struct { + result chan []Metric +} + +func (f fakeReporter) ReportMetrics(_ uint32, ids []uint32, values []int64) { + metricsResult := make([]Metric, len(ids)) + + for j := range ids { + metricsResult[j].ID = MetricID(ids[j]) + metricsResult[j].Value = MetricValue(values[j]) + } + + // send the result back for comparison with client-side input + f.result <- metricsResult +} + +// TestMetrics +func TestMetrics(t *testing.T) { + reporter := &fakeReporter{result: make(chan []Metric, 128)} + SetReporter(reporter) + + // This makes sure that we have enough time to call Add/AddSlice below + // within the same timestamp (second resolution). + time.Sleep(1*time.Second - time.Duration(time.Now().Nanosecond())) + + inputMetrics := []Metric{ + {IDCPUUsage, MetricValue(33)}, + {IDIOThroughput, MetricValue(55)}, + {IDIODuration, MetricValue(66)}, + {IDAgentGoRoutines, MetricValue(20)}, + } + + AddSlice(inputMetrics[0:2]) // 33, 55 + Add(inputMetrics[1].ID, inputMetrics[1].Value) // 55, dropped + Add(inputMetrics[2].ID, inputMetrics[2].Value) // 66 + AddSlice(inputMetrics[3:4]) // 20 + Add(inputMetrics[0].ID, inputMetrics[0].Value) // 33, dropped + AddSlice(inputMetrics[1:3]) // 55, 66 dropped + + // trigger reporting + time.Sleep(1 * time.Second) + AddSlice(nil) + + timeout := time.NewTimer(3 * time.Second) + select { + case outputMetrics := <-reporter.result: + assert.Equal(t, inputMetrics, outputMetrics) + case <-timeout.C: + // Timeout + assert.Fail(t, "timeout - no metrics received in time") + } +} diff --git a/metrics/reportermetrics/reportermetrics.go b/metrics/reportermetrics/reportermetrics.go index 6fa7d010..473862f2 100644 --- a/metrics/reportermetrics/reportermetrics.go +++ b/metrics/reportermetrics/reportermetrics.go @@ -11,9 +11,9 @@ import ( "context" "time" + "github.com/elastic/otel-profiling-agent/periodiccaller" "github.com/elastic/otel-profiling-agent/reporter" - "github.com/elastic/otel-profiling-agent/libpf/periodiccaller" "github.com/elastic/otel-profiling-agent/metrics" ) diff --git a/libpf/nativeunwind/elfunwindinfo/README.md b/nativeunwind/elfunwindinfo/README.md similarity index 100% rename from libpf/nativeunwind/elfunwindinfo/README.md rename to nativeunwind/elfunwindinfo/README.md diff --git a/libpf/nativeunwind/elfunwindinfo/elfehframe.go b/nativeunwind/elfunwindinfo/elfehframe.go similarity index 94% rename from libpf/nativeunwind/elfunwindinfo/elfehframe.go rename to nativeunwind/elfunwindinfo/elfehframe.go index 1e37339e..e4108e5a 100644 --- a/libpf/nativeunwind/elfunwindinfo/elfehframe.go +++ b/nativeunwind/elfunwindinfo/elfehframe.go @@ -18,8 +18,8 @@ import ( log "github.com/sirupsen/logrus" "github.com/elastic/otel-profiling-agent/libpf/hash" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" "github.com/elastic/otel-profiling-agent/libpf/pfelf" + sdtypes "github.com/elastic/otel-profiling-agent/nativeunwind/stackdeltatypes" ) const ( @@ -41,6 +41,8 @@ type ehframeHooks interface { fdeHook(cie *cieInfo, fde *fdeInfo) bool // deltaHook is called for each stack delta found deltaHook(ip uintptr, regs *vmRegs, delta sdtypes.StackDelta) + // golangHook is called if .gopclntab is found to report its coverage + golangHook(start, end uintptr) } // uleb128 is the data type for unsigned little endian base-128 encoded number @@ -269,7 +271,7 @@ func (r *reader) expression() ([]dwarfExpression, error) { blen := uintptr(r.uleb()) data := r.bytes(blen) if data == nil { - return nil, fmt.Errorf("expression data missing") + return nil, errors.New("expression data missing") } ed := reader{ data: data, @@ -722,7 +724,7 @@ func (r *reader) parseHDR(expectCIE bool) (hlen, ciePos uint64, err error) { var idPos, cieMarker uint64 pos := r.pos hlen = uint64(r.u32()) - if hlen < 0xfffffff0 { + if hlen < 0xfffffff0 { //nolint:gocritic // Normal 32-bit dwarf hlen += 4 idPos = uint64(r.pos) @@ -834,21 +836,26 @@ func (r *reader) parseCIE(cie *cieInfo) error { } if !r.isValid() { - return fmt.Errorf("CIE not valid after header") + return errors.New("CIE not valid after header") } return err } // getUnwindInfo generates the needed unwind information from the register set -func (regs *vmRegs) getUnwindInfo() sdtypes.UnwindInfo { +func (regs *vmRegs) getUnwindInfo(allowGenericRegisters bool) sdtypes.UnwindInfo { + var info sdtypes.UnwindInfo switch regs.arch { case elf.EM_AARCH64: - return regs.getUnwindInfoARM() + info = regs.getUnwindInfoARM() case elf.EM_X86_64: - return regs.getUnwindInfoX86() + info = regs.getUnwindInfoX86() default: panic(fmt.Sprintf("architecture %d is not supported", regs.arch)) } + if !allowGenericRegisters && info.Opcode == sdtypes.UnwindOpcodeBaseReg { + return sdtypes.UnwindInfoInvalid + } + return info } // newVMRegs initializes vmRegs structure for given architecture @@ -885,9 +892,8 @@ func isSignalTrampoline(efCode *pfelf.File, fde *fdeInfo) bool { // The FDE format is described in: // http://dwarfstd.org/doc/DWARF5.pdf §6.4.1 // https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html -func (r *reader) parseFDE(ef, efCode *pfelf.File, ipStart uintptr, deltas *sdtypes.StackDeltaArray, - hooks ehframeHooks, cieCache *lru.LRU[uint64, *cieInfo], sorted bool) ( - size uintptr, err error) { +func (ee *elfExtractor) parseFDE(r *reader, ef *pfelf.File, ipStart uintptr, + cieCache *lru.LRU[uint64, *cieInfo], sorted bool) (size uintptr, err error) { // Parse FDE header fdeID := r.pos fde := fdeInfo{sorted: sorted} @@ -957,21 +963,19 @@ func (r *reader) parseFDE(ef, efCode *pfelf.File, ipStart uintptr, deltas *sdtyp } // Process the FDE opcodes - if hooks != nil && !hooks.fdeHook(st.cie, &fde) { + if !ee.hooks.fdeHook(st.cie, &fde) { return uintptr(fde.len), nil } st.loc = fde.ipStart - if st.cie.isSignalHandler || isSignalTrampoline(efCode, &fde) { + if st.cie.isSignalHandler || isSignalTrampoline(ee.file, &fde) { delta := sdtypes.StackDelta{ Address: uint64(st.loc), Hints: sdtypes.UnwindHintKeep, Info: sdtypes.UnwindInfoSignal, } - if hooks != nil { - hooks.deltaHook(st.loc, &st.cur, delta) - } - deltas.AddEx(delta, sorted) + ee.hooks.deltaHook(st.loc, &st.cur, delta) + ee.deltas.AddEx(delta, sorted) } else { hint := sdtypes.UnwindHintKeep for r.hasData() { @@ -982,21 +986,19 @@ func (r *reader) parseFDE(ef, efCode *pfelf.File, ipStart uintptr, deltas *sdtyp delta := sdtypes.StackDelta{ Address: uint64(ip), Hints: hint, - Info: st.cur.getUnwindInfo(), - } - if hooks != nil { - hooks.deltaHook(ip, &st.cur, delta) + Info: st.cur.getUnwindInfo(ee.allowGenericRegs), } - deltas.AddEx(delta, sorted) + ee.hooks.deltaHook(ip, &st.cur, delta) + ee.deltas.AddEx(delta, sorted) hint = sdtypes.UnwindHintNone } delta := sdtypes.StackDelta{ Address: uint64(st.loc), Hints: hint, - Info: st.cur.getUnwindInfo(), + Info: st.cur.getUnwindInfo(ee.allowGenericRegs), } - deltas.AddEx(delta, sorted) + ee.deltas.AddEx(delta, sorted) if !r.isValid() { return 0, fmt.Errorf("FDE %x parsing failed", fdeID) @@ -1010,7 +1012,7 @@ func (r *reader) parseFDE(ef, efCode *pfelf.File, ipStart uintptr, deltas *sdtyp // Add end-of-function stop delta. This might later get removed if there is // another function starting on this address. - deltas.AddEx(sdtypes.StackDelta{ + ee.deltas.AddEx(sdtypes.StackDelta{ Address: uint64(fde.ipStart + fde.ipLen), Hints: sdtypes.UnwindHintGap, Info: info, @@ -1134,7 +1136,7 @@ func findEhSections(ef *pfelf.File) ( // is not in a suitable format, we thus can't do a linear sweep of the FDEs, simply because // we have no idea where the actual list of FDEs starts. Thus, we pretend that the section // doesn't exist at all here. - return nil, nil, fmt.Errorf("no suitable way to parsing eh_frame found") + return nil, nil, errors.New("no suitable way to parsing eh_frame found") } ehFrameSec = &elfRegion{ @@ -1146,7 +1148,7 @@ func findEhSections(ef *pfelf.File) ( // the case with cranelift generated binaries in coredumps, because they don't have the // eh_frame section in a PT_LOAD region. if len(data) == 0 { - return nil, nil, fmt.Errorf("the eh_frame section is empty") + return nil, nil, errors.New("the eh_frame section is empty") } return ehFrameHdrSec, ehFrameSec, nil @@ -1154,8 +1156,8 @@ func findEhSections(ef *pfelf.File) ( // walkBinSearchTable parses FDEs by following all references in the binary search table in the // `.eh_frame_hdr` section. -func walkBinSearchTable(ef *pfelf.File, ehFrameHdrSec *elfRegion, ehFrameSec *elfRegion, - deltas *sdtypes.StackDeltaArray, hooks ehframeHooks) error { +func (ee *elfExtractor) walkBinSearchTable(parsedFile *pfelf.File, ehFrameHdrSec *elfRegion, + ehFrameSec *elfRegion) error { h := (*ehFrameHdr)(unsafe.Pointer(&ehFrameHdrSec.data[0])) // Skip header, which is immediately followed by the binary search table. The header was @@ -1193,7 +1195,7 @@ func walkBinSearchTable(ef *pfelf.File, ehFrameHdrSec *elfRegion, ehFrameSec *el } fr := ehFrameSec.reader(fdeAddr-ehFrameSec.vaddr, false) - _, err = fr.parseFDE(ef, ef, ipStart, deltas, hooks, cieCache, true) + _, err = ee.parseFDE(&fr, parsedFile, ipStart, cieCache, true) if err != nil { return fmt.Errorf("failed to parse FDE: %v", err) } @@ -1203,8 +1205,7 @@ func walkBinSearchTable(ef *pfelf.File, ehFrameHdrSec *elfRegion, ehFrameSec *el } // walkFDEs walks .debug_frame or .eh_frame section, and processes it for stack deltas. -func walkFDEs(ef, efCode *pfelf.File, ehFrameSec *elfRegion, deltas *sdtypes.StackDeltaArray, - hooks ehframeHooks, debugFrame bool) error { +func (ee *elfExtractor) walkFDEs(ef *pfelf.File, ehFrameSec *elfRegion, debugFrame bool) error { var err error cieCache, err := lru.New[uint64, *cieInfo](cieCacheSize, hashUint64) @@ -1216,7 +1217,7 @@ func walkFDEs(ef, efCode *pfelf.File, ehFrameSec *elfRegion, deltas *sdtypes.Sta var entryLen uintptr for f := uintptr(0); f < uintptr(len(ehFrameSec.data)); f += entryLen { fr := ehFrameSec.reader(f, debugFrame) - entryLen, err = fr.parseFDE(ef, efCode, 0, deltas, hooks, cieCache, false) + entryLen, err = ee.parseFDE(&fr, ef, 0, cieCache, false) if err != nil && !errors.Is(err, errUnexpectedType) { return fmt.Errorf("failed to parse FDE %#x: %v", f, err) } @@ -1233,8 +1234,8 @@ func hashUint64(u uint64) uint32 { } // parseEHFrame parses the .eh_frame DWARF info, extracting stack deltas. -func parseEHFrame(ef *pfelf.File, deltas *sdtypes.StackDeltaArray, hooks ehframeHooks) error { - ehFrameHdrSec, ehFrameSec, err := findEhSections(ef) +func (ee *elfExtractor) parseEHFrame() error { + ehFrameHdrSec, ehFrameSec, err := findEhSections(ee.file) if err != nil { return fmt.Errorf("failed to get EH sections: %w", err) } @@ -1249,20 +1250,19 @@ func parseEHFrame(ef *pfelf.File, deltas *sdtypes.StackDeltaArray, hooks ehframe // If we have both the header and the actual eh_frame section, walk the FDEs via the // binary search table. Because the binary search table is ordered, this spares us from // having to sort the FDEs later. - return walkBinSearchTable(ef, ehFrameHdrSec, ehFrameSec, deltas, hooks) + return ee.walkBinSearchTable(ee.file, ehFrameHdrSec, ehFrameSec) } // Otherwise, manually walk the FDEs. - return walkFDEs(ef, ef, ehFrameSec, deltas, hooks, false) + return ee.walkFDEs(ee.file, ehFrameSec, false) } // parseDebugFrame parses the .debug_frame DWARF info, extracting stack deltas. -func parseDebugFrame(ef, efCode *pfelf.File, deltas *sdtypes.StackDeltaArray, - hooks ehframeHooks) error { +func (ee *elfExtractor) parseDebugFrame(ef *pfelf.File) error { debugFrameSection := elfRegionFromSection(ef.Section(".debug_frame")) if debugFrameSection == nil { return nil } - return walkFDEs(ef, efCode, debugFrameSection, deltas, hooks, true) + return ee.walkFDEs(ef, debugFrameSection, true) } diff --git a/libpf/nativeunwind/elfunwindinfo/elfehframe_aarch64.go b/nativeunwind/elfunwindinfo/elfehframe_aarch64.go similarity index 98% rename from libpf/nativeunwind/elfunwindinfo/elfehframe_aarch64.go rename to nativeunwind/elfunwindinfo/elfehframe_aarch64.go index a2ae2aff..34d38833 100644 --- a/libpf/nativeunwind/elfunwindinfo/elfehframe_aarch64.go +++ b/nativeunwind/elfunwindinfo/elfehframe_aarch64.go @@ -14,7 +14,7 @@ import ( "debug/elf" "fmt" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" + sdtypes "github.com/elastic/otel-profiling-agent/nativeunwind/stackdeltatypes" ) //nolint:deadcode,varcheck diff --git a/libpf/nativeunwind/elfunwindinfo/elfehframe_test.go b/nativeunwind/elfunwindinfo/elfehframe_test.go similarity index 77% rename from libpf/nativeunwind/elfunwindinfo/elfehframe_test.go rename to nativeunwind/elfunwindinfo/elfehframe_test.go index 71dd020d..a5e35d77 100644 --- a/libpf/nativeunwind/elfunwindinfo/elfehframe_test.go +++ b/nativeunwind/elfunwindinfo/elfehframe_test.go @@ -7,12 +7,13 @@ package elfunwindinfo import ( - "errors" "testing" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/google/go-cmp/cmp" + sdtypes "github.com/elastic/otel-profiling-agent/nativeunwind/stackdeltatypes" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type ehtester struct { @@ -36,14 +37,14 @@ func (e *ehtester) deltaHook(ip uintptr, regs *vmRegs, delta sdtypes.StackDelta) regs.fp.String(), regs.ra.String()) if expected, ok := e.res[ip]; ok { - if diff := cmp.Diff(delta.Info, expected); diff != "" { - e.t.Fatalf("expected stack delta @%x %s", - ip, diff) - } + assert.Equal(e.t, expected, delta.Info) e.found++ } } +func (e *ehtester) golangHook(_, _ uintptr) { +} + func genDelta(opcode uint8, cfa, rbp int32) sdtypes.UnwindInfo { res := sdtypes.UnwindInfo{ Opcode: opcode, @@ -107,49 +108,27 @@ func TestEhFrame(t *testing.T) { test := test t.Run(name, func(t *testing.T) { ef, err := pfelf.Open(test.elfFile) - if err != nil { - t.Fatalf("Failed to open ELF: %v", err) - } + require.NoError(t, err) defer ef.Close() tester := ehtester{t, test.res, 0} - deltas := sdtypes.StackDeltaArray{} - err = parseEHFrame(ef, &deltas, &tester) - if err != nil { - t.Fatalf("Failed to parse ELF deltas: %v", err) - } - if tester.found != len(test.res) { - t.Fatalf("Expected %v deltas, got %v", len(test.res), tester.found) + ee := elfExtractor{ + file: ef, + deltas: &sdtypes.StackDeltaArray{}, + hooks: &tester, } + err = ee.parseEHFrame() + require.NoError(t, err) + assert.Equal(t, len(test.res), tester.found) }) } } -// cmpCie is a helper function to compare two cieInfo structs. -func cmpCie(t *testing.T, a, b *cieInfo) bool { - t.Helper() - - if a.codeAlign != b.codeAlign || - a.dataAlign != b.dataAlign || - a.regRA != b.regRA || - a.enc != b.enc || - a.ldsaEnc != b.ldsaEnc || - a.hasAugmentation != b.hasAugmentation || - a.isSignalHandler != b.isSignalHandler || - a.initialState.cfa != b.initialState.cfa || - a.initialState.fp != b.initialState.fp || - a.initialState.ra != b.initialState.ra { - return false - } - return true -} - func TestParseCIE(t *testing.T) { tests := map[string]struct { data []byte expected *cieInfo debugFrame bool - err error }{ // Call frame information example for version 4. // http://dwarfstd.org/doc/DWARF5.pdf Table D.5 "Call frame information example" @@ -197,13 +176,8 @@ func TestParseCIE(t *testing.T) { } extracted := &cieInfo{} err := fakeReader.parseCIE(extracted) - if !errors.Is(err, tc.err) { - t.Fatal(err) - } - - if !cmpCie(t, tc.expected, extracted) { - t.Fatalf("Expected %#v but got %#v", tc.expected, extracted) - } + require.NoError(t, err) + assert.Equal(t, tc.expected, extracted) }) } } diff --git a/libpf/nativeunwind/elfunwindinfo/elfehframe_x86.go b/nativeunwind/elfunwindinfo/elfehframe_x86.go similarity index 90% rename from libpf/nativeunwind/elfunwindinfo/elfehframe_x86.go rename to nativeunwind/elfunwindinfo/elfehframe_x86.go index 4bec2a4e..a3d05f8e 100644 --- a/libpf/nativeunwind/elfunwindinfo/elfehframe_x86.go +++ b/nativeunwind/elfunwindinfo/elfehframe_x86.go @@ -14,7 +14,7 @@ import ( "debug/elf" "fmt" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" + sdtypes "github.com/elastic/otel-profiling-agent/nativeunwind/stackdeltatypes" ) //nolint:deadcode,varcheck @@ -140,6 +140,14 @@ func (regs *vmRegs) getUnwindInfoX86() sdtypes.UnwindInfo { info.Opcode = sdtypes.UnwindOpcodeBaseSP info.Param = int32(regs.cfa.off) } + case x86RegRAX, x86RegR9, x86RegR11, x86RegR15: + // openssl libcrypto has handwritten assembly that use these registers + // as the CFA directly. These function do not call other code that would + // trash the register, so allow these for libcrypto. + if regs.cfa.off%8 == 0 { + info.Opcode = sdtypes.UnwindOpcodeBaseReg + info.Param = int32(regs.cfa.reg) + int32(regs.cfa.off)<<1 + } case regExprPLT: info.Opcode = sdtypes.UnwindOpcodeCommand info.Param = sdtypes.UnwindCommandPLT diff --git a/libpf/nativeunwind/elfunwindinfo/elfgopclntab.go b/nativeunwind/elfunwindinfo/elfgopclntab.go similarity index 96% rename from libpf/nativeunwind/elfunwindinfo/elfgopclntab.go rename to nativeunwind/elfunwindinfo/elfgopclntab.go index 7581aa5f..d409ee20 100644 --- a/libpf/nativeunwind/elfunwindinfo/elfgopclntab.go +++ b/nativeunwind/elfunwindinfo/elfgopclntab.go @@ -16,8 +16,8 @@ import ( "fmt" "unsafe" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" "github.com/elastic/otel-profiling-agent/libpf/pfelf" + sdtypes "github.com/elastic/otel-profiling-agent/nativeunwind/stackdeltatypes" log "github.com/sirupsen/logrus" ) @@ -337,10 +337,12 @@ func SearchGoPclntab(ef *pfelf.File) ([]byte, error) { // Parse Golang .gopclntab spdelta tables and try to produce minified intervals // by using large frame pointer ranges when possible -func parseGoPclntab(ef *pfelf.File, deltas *sdtypes.StackDeltaArray, f *extractionFilter) error { +func (ee *elfExtractor) parseGoPclntab() error { var err error var data []byte + ef := ee.file + if ef.InsideCore { // Section tables not available. Use heuristic. Ignore errors as // this might not be a Go binary. @@ -518,7 +520,7 @@ func parseGoPclntab(ef *pfelf.File, deltas *sdtypes.StackDeltaArray, f *extracti // First, check for functions with special handling. funcName := getString(funcnametab, int(fun.nameOff)) if info, found := goFunctionsStopDelta[string(funcName)]; found { - deltas.Add(sdtypes.StackDelta{ + ee.deltas.Add(sdtypes.StackDelta{ Address: fun.startPc, Info: *info, }) @@ -546,12 +548,12 @@ func parseGoPclntab(ef *pfelf.File, deltas *sdtypes.StackDeltaArray, f *extracti switch arch { case elf.EM_X86_64: - if err := parseX86pclntabFunc(deltas, fun, dataLen, pctab, strategy, i, + if err := parseX86pclntabFunc(ee.deltas, fun, dataLen, pctab, strategy, i, hdr.quantum); err != nil { return err } case elf.EM_AARCH64: - if err := parseArm64pclntabFunc(deltas, fun, dataLen, pctab, i, + if err := parseArm64pclntabFunc(ee.deltas, fun, dataLen, pctab, i, hdr.quantum); err != nil { return err } @@ -559,26 +561,27 @@ func parseGoPclntab(ef *pfelf.File, deltas *sdtypes.StackDeltaArray, f *extracti } // Filter out .gopclntab info from other sources + var start, end uintptr if IsGo118orNewer(hdr.magic) { // nolint:lll // https://github.com/golang/go/blob/6df0957060b1315db4fd6a359eefc3ee92fcc198/src/debug/gosym/pclntab.go#L440-L450 - f.start = uintptr(*(*uint32)(unsafe.Pointer(&functab[0]))) - f.start += textStart + start = uintptr(*(*uint32)(unsafe.Pointer(&functab[0]))) + start += textStart // From go12symtab document, reason for indexing beyond hdr.numFuncs: // "The final pcN value is the address just beyond func(N-1), so that the binary // search can distinguish between a pc inside func(N-1) and a pc outside the text // segment." - f.end = uintptr(*(*uint32)(unsafe.Pointer(&functab[uintptr(hdr.numFuncs)*mapSize]))) - f.end += textStart + end = uintptr(*(*uint32)(unsafe.Pointer(&functab[uintptr(hdr.numFuncs)*mapSize]))) + end += textStart } else { - f.start = *(*uintptr)(unsafe.Pointer(&functab[0])) - f.end = *(*uintptr)(unsafe.Pointer(&functab[uintptr(hdr.numFuncs)*mapSize])) + start = *(*uintptr)(unsafe.Pointer(&functab[0])) + end = *(*uintptr)(unsafe.Pointer(&functab[uintptr(hdr.numFuncs)*mapSize])) } - f.golangFrames = true + ee.hooks.golangHook(start, end) // Add end of code indicator - deltas.Add(sdtypes.StackDelta{ - Address: uint64(f.end), + ee.deltas.Add(sdtypes.StackDelta{ + Address: uint64(end), Info: sdtypes.UnwindInfoInvalid, }) diff --git a/libpf/nativeunwind/elfunwindinfo/elfgopclntab_test.go b/nativeunwind/elfunwindinfo/elfgopclntab_test.go similarity index 76% rename from libpf/nativeunwind/elfunwindinfo/elfgopclntab_test.go rename to nativeunwind/elfunwindinfo/elfgopclntab_test.go index 0dbddec1..d32a985e 100644 --- a/libpf/nativeunwind/elfunwindinfo/elfgopclntab_test.go +++ b/nativeunwind/elfunwindinfo/elfgopclntab_test.go @@ -10,8 +10,11 @@ import ( "debug/elf" "testing" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" "github.com/elastic/otel-profiling-agent/libpf/pfelf" + sdtypes "github.com/elastic/otel-profiling-agent/nativeunwind/stackdeltatypes" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // Go 1.2 spec Appendix: PC-Value Table Encoding example @@ -35,15 +38,11 @@ func TestPcval(t *testing.T) { i := 0 for ok := true; ok; ok = p.step() { t.Logf("Pcval %d, %x", p.val, p.pcEnd) - if p.val != res[i].val || p.pcEnd != res[i].pc { - t.Fatalf("Unexpected pcval %d, %x != %d, %x", - p.val, p.pcEnd, res[i].val, res[i].pc) - } + assert.Equal(t, res[i].val, p.val) + assert.Equal(t, res[i].pc, p.pcEnd) i++ } - if i != len(res) { - t.Fatalf("Table not decoded in full") - } + assert.Equal(t, len(res), i) } // Pcval with sequence that would result in out-of-bound read @@ -66,9 +65,7 @@ func TestGoStrategy(t *testing.T) { } for _, x := range res { s := getSourceFileStrategy(elf.EM_X86_64, []byte(x.file)) - if s != x.strategy { - t.Fatalf("File %v strategy %v != %v", x.file, s, x.strategy) - } + assert.Equal(t, x.strategy, s) } } @@ -90,19 +87,17 @@ func TestParseGoPclntab(t *testing.T) { name := name test := test t.Run(name, func(t *testing.T) { - deltas := sdtypes.StackDeltaArray{} - filter := &extractionFilter{} - ef, err := pfelf.Open(test.elfFile) - if err != nil { - t.Fatal(err) - } - if err := parseGoPclntab(ef, &deltas, filter); err != nil { - t.Fatal(err) - } - if len(deltas) == 0 { - t.Fatal("Failed to extract stack deltas") + require.NoError(t, err) + + ee := elfExtractor{ + file: ef, + hooks: &extractionFilter{}, + deltas: &sdtypes.StackDeltaArray{}, } + err = ee.parseGoPclntab() + require.NoError(t, err) + assert.NotEmpty(t, *ee.deltas) }) } } diff --git a/libpf/nativeunwind/elfunwindinfo/stackdeltaextraction.go b/nativeunwind/elfunwindinfo/stackdeltaextraction.go similarity index 74% rename from libpf/nativeunwind/elfunwindinfo/stackdeltaextraction.go rename to nativeunwind/elfunwindinfo/stackdeltaextraction.go index 774f2b7c..404da561 100644 --- a/libpf/nativeunwind/elfunwindinfo/stackdeltaextraction.go +++ b/nativeunwind/elfunwindinfo/stackdeltaextraction.go @@ -7,11 +7,13 @@ package elfunwindinfo import ( + "debug/elf" "fmt" "sort" + "strings" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" "github.com/elastic/otel-profiling-agent/libpf/pfelf" + sdtypes "github.com/elastic/otel-profiling-agent/nativeunwind/stackdeltatypes" ) const ( @@ -63,21 +65,51 @@ func (f *extractionFilter) fdeHook(_ *cieInfo, fde *fdeInfo) bool { func (f *extractionFilter) deltaHook(uintptr, *vmRegs, sdtypes.StackDelta) { } -func extractDebugDeltas(elfFile *pfelf.File, elfRef *pfelf.Reference, - deltas *sdtypes.StackDeltaArray, filter *extractionFilter) error { +// golangHook reports the .gopclntab area +func (f *extractionFilter) golangHook(start, end uintptr) { + f.start = start + f.end = end + f.golangFrames = true +} + +// elfExtractor is the main context for parsing stack deltas from an ELF +type elfExtractor struct { + ref *pfelf.Reference + file *pfelf.File + + hooks ehframeHooks + + deltas *sdtypes.StackDeltaArray + + // allowGenericRegs enables generation of unwinding using specific general purpose + // registers as CFA base. This is possible for code that does not call into other + // functions that would trash these registers (we cannot recover these registers + // during unwind). This is currently enabled for openssl libcrypto only. + allowGenericRegs bool +} + +func (ee *elfExtractor) extractDebugDeltas() error { var err error // Attempt finding the associated debug information file with .debug_frame, // but ignore errors if it's not available; many production systems // do not intentionally have debug packages installed. - debugELF, _ := elfFile.OpenDebugLink(elfRef.FileName(), elfRef) + debugELF, _ := ee.file.OpenDebugLink(ee.ref.FileName(), ee.ref) if debugELF != nil { - err = parseDebugFrame(debugELF, elfFile, deltas, filter) + err = ee.parseDebugFrame(debugELF) debugELF.Close() } return err } +func isLibCrypto(elfFile *pfelf.File) bool { + if name, err := elfFile.DynString(elf.DT_SONAME); err == nil && len(name) == 1 { + // Allow generic register CFA for openssl libcrypto + return strings.HasPrefix(name[0], "libcrypto.so.") + } + return false +} + // Extract takes a filename for a modern ELF file that is accessible // and provides the stack delta intervals in the interval parameter func Extract(filename string, interval *sdtypes.IntervalData) error { @@ -95,22 +127,29 @@ func ExtractELF(elfRef *pfelf.Reference, interval *sdtypes.IntervalData) error { } // Parse the stack deltas from the ELF + filter := extractionFilter{} deltas := sdtypes.StackDeltaArray{} - filter := &extractionFilter{} + ee := elfExtractor{ + ref: elfRef, + file: elfFile, + deltas: &deltas, + hooks: &filter, + allowGenericRegs: isLibCrypto(elfFile), + } - if err = parseGoPclntab(elfFile, &deltas, filter); err != nil { + if err = ee.parseGoPclntab(); err != nil { return fmt.Errorf("failure to parse golang stack deltas: %v", err) } - if err = parseEHFrame(elfFile, &deltas, filter); err != nil { + if err = ee.parseEHFrame(); err != nil { return fmt.Errorf("failure to parse eh_frame stack deltas: %v", err) } - if err = parseDebugFrame(elfFile, elfFile, &deltas, filter); err != nil { + if err = ee.parseDebugFrame(elfFile); err != nil { return fmt.Errorf("failure to parse debug_frame stack deltas: %v", err) } if len(deltas) < numIntervalsToOmitDebugLink { // There is only few stack deltas. See if we find the .gnu_debuglink // debug information for additional .debug_frame stack deltas. - if err = extractDebugDeltas(elfFile, elfRef, &deltas, filter); err != nil { + if err = ee.extractDebugDeltas(); err != nil { return fmt.Errorf("failure to parse debug stack deltas: %v", err) } } diff --git a/libpf/nativeunwind/elfunwindinfo/stackdeltaextraction_test.go b/nativeunwind/elfunwindinfo/stackdeltaextraction_test.go similarity index 93% rename from libpf/nativeunwind/elfunwindinfo/stackdeltaextraction_test.go rename to nativeunwind/elfunwindinfo/stackdeltaextraction_test.go index 7bd42af6..164a67d1 100644 --- a/libpf/nativeunwind/elfunwindinfo/stackdeltaextraction_test.go +++ b/nativeunwind/elfunwindinfo/stackdeltaextraction_test.go @@ -11,8 +11,9 @@ import ( "os" "testing" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" - "github.com/google/go-cmp/cmp" + sdtypes "github.com/elastic/otel-profiling-agent/nativeunwind/stackdeltatypes" + + "github.com/stretchr/testify/require" ) // Base64-encoded data from /usr/bin/volname on a stock debian box, the smallest @@ -138,35 +139,24 @@ var firstDeltas = sdtypes.StackDeltaArray{ func TestExtractStackDeltasFromFilename(t *testing.T) { buffer, err := base64.StdEncoding.DecodeString(usrBinVolname) - if err != nil { - t.Errorf("Failed to base64-decode the embedded executable?") - } + require.NoError(t, err) // Write the executable file to a temporary file, and the symbol // file, too. exeFile, err := os.CreateTemp("/tmp", "dwarf_extract_elf_") - if err != nil { - t.Errorf("failure to open tempfile") - } + require.NoError(t, err) defer exeFile.Close() - if _, err = exeFile.Write(buffer); err != nil { - t.Fatalf("failed to write buffer to file: %v", err) - } - if err = exeFile.Sync(); err != nil { - t.Fatalf("failed to synchronize file: %v", err) - } + _, err = exeFile.Write(buffer) + require.NoError(t, err) + err = exeFile.Sync() + require.NoError(t, err) defer os.Remove(exeFile.Name()) filename := exeFile.Name() var data sdtypes.IntervalData err = Extract(filename, &data) - if err != nil { - t.Errorf("%v", err) - } + require.NoError(t, err) for _, delta := range data.Deltas { t.Logf("%#v", delta) } - - if diff := cmp.Diff(data.Deltas[:len(firstDeltas)], firstDeltas); diff != "" { - t.Errorf("Deltas are wrong: %s", diff) - } + require.Equal(t, data.Deltas[:len(firstDeltas)], firstDeltas) } diff --git a/nativeunwind/elfunwindinfo/stackdeltaprovider.go b/nativeunwind/elfunwindinfo/stackdeltaprovider.go new file mode 100644 index 00000000..324fa909 --- /dev/null +++ b/nativeunwind/elfunwindinfo/stackdeltaprovider.go @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package elfunwindinfo + +import ( + "errors" + "fmt" + "os" + "sync/atomic" + + "github.com/elastic/otel-profiling-agent/host" + "github.com/elastic/otel-profiling-agent/libpf/pfelf" + "github.com/elastic/otel-profiling-agent/nativeunwind" + sdtypes "github.com/elastic/otel-profiling-agent/nativeunwind/stackdeltatypes" +) + +// ELFStackDeltaProvider extracts stack deltas from ELF executables available +// via the pfelf.File interface. +type ELFStackDeltaProvider struct { + // Metrics + successCount atomic.Uint64 + extractionErrorCount atomic.Uint64 +} + +// Compile time check that the ELFStackDeltaProvider implements its interface correctly. +var _ nativeunwind.StackDeltaProvider = (*ELFStackDeltaProvider)(nil) + +// NewStackDeltaProvider creates a stack delta provider using the ELF eh_frame extraction. +func NewStackDeltaProvider() nativeunwind.StackDeltaProvider { + return &ELFStackDeltaProvider{} +} + +// GetIntervalStructuresForFile builds the stack delta information for a single executable. +func (provider *ELFStackDeltaProvider) GetIntervalStructuresForFile(_ host.FileID, + elfRef *pfelf.Reference, interval *sdtypes.IntervalData) error { + err := ExtractELF(elfRef, interval) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + provider.extractionErrorCount.Add(1) + } + return fmt.Errorf("failed to extract stack deltas from %s: %w", + elfRef.FileName(), err) + } + provider.successCount.Add(1) + return nil +} + +func (provider *ELFStackDeltaProvider) GetAndResetStatistics() nativeunwind.Statistics { + return nativeunwind.Statistics{ + Success: provider.successCount.Swap(0), + ExtractionErrors: provider.extractionErrorCount.Swap(0), + } +} diff --git a/libpf/nativeunwind/elfunwindinfo/testdata/.gitignore b/nativeunwind/elfunwindinfo/testdata/.gitignore similarity index 100% rename from libpf/nativeunwind/elfunwindinfo/testdata/.gitignore rename to nativeunwind/elfunwindinfo/testdata/.gitignore diff --git a/libpf/nativeunwind/elfunwindinfo/testdata/Makefile b/nativeunwind/elfunwindinfo/testdata/Makefile similarity index 100% rename from libpf/nativeunwind/elfunwindinfo/testdata/Makefile rename to nativeunwind/elfunwindinfo/testdata/Makefile diff --git a/libpf/nativeunwind/elfunwindinfo/testdata/helloworld.go b/nativeunwind/elfunwindinfo/testdata/helloworld.go similarity index 100% rename from libpf/nativeunwind/elfunwindinfo/testdata/helloworld.go rename to nativeunwind/elfunwindinfo/testdata/helloworld.go diff --git a/libpf/nativeunwind/elfunwindinfo/testdata/schrodinger-libpython3.8.so.1.0 b/nativeunwind/elfunwindinfo/testdata/schrodinger-libpython3.8.so.1.0 similarity index 100% rename from libpf/nativeunwind/elfunwindinfo/testdata/schrodinger-libpython3.8.so.1.0 rename to nativeunwind/elfunwindinfo/testdata/schrodinger-libpython3.8.so.1.0 diff --git a/libpf/nativeunwind/elfunwindinfo/testdata/test.so b/nativeunwind/elfunwindinfo/testdata/test.so similarity index 100% rename from libpf/nativeunwind/elfunwindinfo/testdata/test.so rename to nativeunwind/elfunwindinfo/testdata/test.so diff --git a/libpf/nativeunwind/stackdeltaprovider.go b/nativeunwind/stackdeltaprovider.go similarity index 82% rename from libpf/nativeunwind/stackdeltaprovider.go rename to nativeunwind/stackdeltaprovider.go index a507c44e..de5dafbb 100644 --- a/libpf/nativeunwind/stackdeltaprovider.go +++ b/nativeunwind/stackdeltaprovider.go @@ -8,15 +8,13 @@ package nativeunwind import ( "github.com/elastic/otel-profiling-agent/host" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" "github.com/elastic/otel-profiling-agent/libpf/pfelf" + sdtypes "github.com/elastic/otel-profiling-agent/nativeunwind/stackdeltatypes" ) type Statistics struct { - // Number of times for a hit of a cache entry. - Hit uint64 - // Number of times for a miss of a cache entry. - Miss uint64 + // Number of times of successful extractions. + Success uint64 // Number of times extracting stack deltas failed. ExtractionErrors uint64 diff --git a/libpf/nativeunwind/stackdeltatypes/stackdeltatypes.go b/nativeunwind/stackdeltatypes/stackdeltatypes.go similarity index 98% rename from libpf/nativeunwind/stackdeltatypes/stackdeltatypes.go rename to nativeunwind/stackdeltatypes/stackdeltatypes.go index d73653fd..9535c50d 100644 --- a/libpf/nativeunwind/stackdeltatypes/stackdeltatypes.go +++ b/nativeunwind/stackdeltatypes/stackdeltatypes.go @@ -9,7 +9,7 @@ // stack delta information that is used in all relevant packages. package stackdeltatypes -// #include "../../../support/ebpf/stackdeltatypes.h" +// #include "../../support/ebpf/stackdeltatypes.h" import "C" const ( @@ -28,6 +28,7 @@ const ( UnwindOpcodeBaseSP uint8 = C.UNWIND_OPCODE_BASE_SP UnwindOpcodeBaseFP uint8 = C.UNWIND_OPCODE_BASE_FP UnwindOpcodeBaseLR uint8 = C.UNWIND_OPCODE_BASE_LR + UnwindOpcodeBaseReg uint8 = C.UNWIND_OPCODE_BASE_REG UnwindOpcodeFlagDeref uint8 = C.UNWIND_OPCODEF_DEREF // UnwindCommands from the C header file diff --git a/libpf/nopanicslicereader/nopanicslicereader.go b/nopanicslicereader/nopanicslicereader.go similarity index 100% rename from libpf/nopanicslicereader/nopanicslicereader.go rename to nopanicslicereader/nopanicslicereader.go diff --git a/nopanicslicereader/nopanicslicereader_test.go b/nopanicslicereader/nopanicslicereader_test.go new file mode 100644 index 00000000..fdbc0992 --- /dev/null +++ b/nopanicslicereader/nopanicslicereader_test.go @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package nopanicslicereader + +import ( + "testing" + + "github.com/elastic/otel-profiling-agent/libpf" + + "github.com/stretchr/testify/assert" +) + +func TestSliceReader(t *testing.T) { + data := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} + assert.Equal(t, uint16(0x0403), Uint16(data, 2)) + assert.Equal(t, uint16(0), Uint16(data, 7)) + assert.Equal(t, uint32(0x04030201), Uint32(data, 0)) + assert.Equal(t, uint32(0), Uint32(data, 100)) + assert.Equal(t, uint64(0x0807060504030201), Uint64(data, 0)) + assert.Equal(t, uint64(0), Uint64(data, 1)) + assert.Equal(t, libpf.Address(0x0807060504030201), Ptr(data, 0)) + assert.Equal(t, libpf.Address(0x08070605), PtrDiff32(data, 4)) +} diff --git a/pacmask/pacmask_arm64.go b/pacmask/pacmask_arm64.go index f2e8e5c4..1af91bbf 100644 --- a/pacmask/pacmask_arm64.go +++ b/pacmask/pacmask_arm64.go @@ -33,7 +33,6 @@ func GetPACMask() uint64 { // register set and then dispose of that process. Because using `ptrace` // without a good reason is probably not exactly something that cloud // customers would love us for, this function uses a different approach. - // Extended reasoning for this approach can be found at [2]. // // The alternative approach generates random 64 bit values with the lower 32 // bits randomized, asking the CPU to "sign" them with PAC bits. From the @@ -52,7 +51,6 @@ func GetPACMask() uint64 { // rounding to `1.0`. // // [1]: https://www.kernel.org/doc/html/latest/arm64/pointer-authentication.html - // [2]: https://github.com/elastic/otel-profiling-agent/pull/2000#discussion_r767745539 var mask uint64 for i := 0; i < 64; i++ { diff --git a/libpf/periodiccaller/periodiccaller.go b/periodiccaller/periodiccaller.go similarity index 100% rename from libpf/periodiccaller/periodiccaller.go rename to periodiccaller/periodiccaller.go diff --git a/periodiccaller/periodiccaller_test.go b/periodiccaller/periodiccaller_test.go new file mode 100644 index 00000000..2aacfa63 --- /dev/null +++ b/periodiccaller/periodiccaller_test.go @@ -0,0 +1,250 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +// Package periodiccaller allows periodic calls of functions. +package periodiccaller + +import ( + "context" + "fmt" + "os" + "runtime" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// fetchStackRecords returns all stacks from all Go routines. +func fetchStackRecords(t *testing.T) []runtime.StackRecord { + t.Helper() + + // Explicit GC call to make sure stopped Go routines are cleaned up. + runtime.GC() + + var n int + var ok bool + sr := make([]runtime.StackRecord, os.Getpagesize()) + for { + n, ok = runtime.GoroutineProfile(sr) + if !ok { + // Grow sr + sr = append(sr, make([]runtime.StackRecord, os.Getpagesize())...) + continue + } + return sr[:n] + } +} + +// isSelfOrRuntime returns true if stack is from self or a Go runtime internal stack. +func isSelfOrRuntime(t *testing.T, stack [32]uintptr, self string) bool { + t.Helper() + isRuntimeOnly := true + for _, pc := range stack { + f := runtime.FuncForPC(pc) + if f != nil { + funcName := f.Name() + + if funcName == self { + return true + } + // Go runtime specifc filters + if !strings.HasPrefix(funcName, "runtime.") && + !strings.HasPrefix(funcName, "runtime/") && + !strings.HasPrefix(funcName, "testing.") && + funcName != "main.main" { + isRuntimeOnly = false + } + } + } + return isRuntimeOnly +} + +// checkForGoRoutineLeaks calls panic if Go routines are still running +func checkForGoRoutineLeaks(t *testing.T) { + t.Helper() + + rpc := make([]uintptr, 1) + m := runtime.Callers(1, rpc) + if m < 1 { + t.Fatal("could not determine selfFrame") + } + selfFrame, _ := runtime.CallersFrames(rpc).Next() + sr := fetchStackRecords(t) + + leakedGoRoutines := make([]int, 0) + for i, s := range sr { + if isSelfOrRuntime(t, s.Stack0, selfFrame.Func.Name()) { + continue + } + leakedGoRoutines = append(leakedGoRoutines, i) + } + + if len(leakedGoRoutines) != 0 { + for _, j := range leakedGoRoutines { + for _, k := range sr[j].Stack() { + t.Logf("%s\n", runtime.FuncForPC(k).Name()) + } + t.Log("") + } + panic(fmt.Sprintf("Got %d leaked Go routines", len(leakedGoRoutines))) + } +} + +func TestCheckForGoRoutineLeaks(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var wg sync.WaitGroup + wg.Add(1) + go func() { + wg.Done() + // Block further processing. + <-ctx.Done() + }() + + // Enforce wait to make sure the Go routine exists. + wg.Wait() + + defer func() { + r := recover() + // checkForGoRoutineLeaks is expected to panic. + require.NotNil(t, r) + }() + + checkForGoRoutineLeaks(t) +} + +// TestPeriodicCaller tests periodic calling for all exported periodiccaller functions +func TestPeriodicCaller(t *testing.T) { + defer checkForGoRoutineLeaks(t) + interval := 10 * time.Millisecond + trigger := make(chan bool) + + tests := map[string]func(context.Context, func()) func(){ + "Start": func(ctx context.Context, cb func()) func() { + return Start(ctx, interval, cb) + }, + "StartWithJitter": func(ctx context.Context, cb func()) func() { + return StartWithJitter(ctx, interval, 0.2, cb) + }, + "StartWithManualTrigger": func(ctx context.Context, cb func()) func() { + return StartWithManualTrigger(ctx, interval, trigger, func(bool) { cb() }) + }, + } + + for name, testFunc := range tests { + t.Run(name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + + done := make(chan bool) + var counter atomic.Int32 + + stop := testFunc(ctx, func() { + result := counter.Load() + if result < 2 { + result = counter.Add(1) + if result == 2 { + // done after 2 calls + done <- true + } + } + }) + + // We expect the timer to stop after 2 calls to the callback function + select { + case <-done: + result := counter.Load() + assert.Equal(t, int32(2), result) + case <-ctx.Done(): + // Timeout + assert.Failf(t, "timeout (%s) - periodiccaller not working", name) + } + + cancel() + stop() + }) + } +} + +// TestPeriodicCallerCancellation tests the cancellation functionality for all +// exported periodiccaller functions +func TestPeriodicCallerCancellation(t *testing.T) { + defer checkForGoRoutineLeaks(t) + interval := 1 * time.Millisecond + trigger := make(chan bool) + + tests := map[string]func(context.Context, func()) func(){ + "Start": func(ctx context.Context, cb func()) func() { + return Start(ctx, interval, cb) + }, + "StartWithJitter": func(ctx context.Context, cb func()) func() { + return StartWithJitter(ctx, interval, 0.2, cb) + }, + "StartWithManualTrigger": func(ctx context.Context, cb func()) func() { + return StartWithManualTrigger(ctx, interval, trigger, func(bool) { cb() }) + }, + } + + for name, testFunc := range tests { + t.Run(name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + + executions := make(chan struct{}, 20) + stop := testFunc(ctx, func() { + executions <- struct{}{} + }) + + // wait until timeout occurred + <-ctx.Done() + + // give callback time to execute, if cancellation didn't work + time.Sleep(10 * time.Millisecond) + + assert.NotEmpty(t, executions) + assert.Less(t, len(executions), 12) + + cancel() + stop() + }) + } +} + +// TestPeriodicCallerManualTrigger tests periodic calling with manual trigger +func TestPeriodicCallerManualTrigger(t *testing.T) { + defer checkForGoRoutineLeaks(t) + // Number of manual triggers + numTrigger := 5 + // This should be something larger than time taken to execute triggers + interval := 10 * time.Second + ctx, cancel := context.WithTimeout(context.Background(), interval) + defer cancel() + + var counter atomic.Int32 + trigger := make(chan bool) + done := make(chan bool) + + stop := StartWithManualTrigger(ctx, interval, trigger, func(manualTrigger bool) { + require.True(t, manualTrigger) + n := counter.Add(1) + if n == int32(numTrigger) { + done <- true + } + }) + defer stop() + + for i := 0; i < numTrigger; i++ { + trigger <- true + } + <-done + + numExec := counter.Load() + assert.Equal(t, int(numExec), numTrigger) +} diff --git a/libpf/pfnamespaces/namespaces.go b/pfnamespaces/namespaces.go similarity index 93% rename from libpf/pfnamespaces/namespaces.go rename to pfnamespaces/namespaces.go index 69434050..68bf55da 100644 --- a/libpf/pfnamespaces/namespaces.go +++ b/pfnamespaces/namespaces.go @@ -7,11 +7,10 @@ package pfnamespaces import ( + "errors" "fmt" "syscall" - "go.uber.org/multierr" - "golang.org/x/sys/unix" ) @@ -39,7 +38,7 @@ func EnterNamespace(pid int, nsType string) (int, error) { err = unix.Setns(fd, nsTypeInt) if err != nil { // Close namespace and return the error - return -1, multierr.Combine(err, unix.Close(fd)) + return -1, errors.Join(err, unix.Close(fd)) } return fd, nil diff --git a/proc/proc.go b/proc/proc.go index 216ba5cb..2c42f1f3 100644 --- a/proc/proc.go +++ b/proc/proc.go @@ -16,11 +16,12 @@ import ( "strconv" "strings" - "github.com/elastic/otel-profiling-agent/libpf/stringutil" + log "github.com/sirupsen/logrus" "golang.org/x/sys/unix" "github.com/elastic/otel-profiling-agent/libpf" - log "github.com/sirupsen/logrus" + "github.com/elastic/otel-profiling-agent/stringutil" + "github.com/elastic/otel-profiling-agent/util" ) const defaultMountPoint = "/proc" @@ -72,7 +73,7 @@ func GetKallsyms(kallsymsPath string) (*libpf.SymbolMap, error) { symmap.Finalize() if noSymbols { - return nil, fmt.Errorf( + return nil, errors.New( "all addresses from kallsyms are zero - check process permissions") } @@ -134,9 +135,9 @@ func GetKernelModules(modulesPath string, return &symmap, nil } -// ListPIDs from the proc filesystem mount point and return a list of libpf.PID to be processed -func ListPIDs() ([]libpf.PID, error) { - pids := make([]libpf.PID, 0) +// ListPIDs from the proc filesystem mount point and return a list of util.PID to be processed +func ListPIDs() ([]util.PID, error) { + pids := make([]util.PID, 0) files, err := os.ReadDir(defaultMountPoint) if err != nil { return nil, err @@ -150,7 +151,7 @@ func ListPIDs() ([]libpf.PID, error) { if err != nil { continue } - pids = append(pids, libpf.PID(pid)) + pids = append(pids, util.PID(pid)) } return pids, nil } @@ -158,7 +159,7 @@ func ListPIDs() ([]libpf.PID, error) { // 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) { +func IsPIDLive(pid util.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) diff --git a/proc/proc_test.go b/proc/proc_test.go index f222085a..2d879ac5 100644 --- a/proc/proc_test.go +++ b/proc/proc_test.go @@ -10,38 +10,33 @@ import ( "testing" "github.com/elastic/otel-profiling-agent/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) - if err != nil { - t.Fatalf("symbol '%s', was unexpectedly not found: %v", name, err) - } - if sym.Address != expectedAddress { - t.Fatalf("symbol '%s', expected address 0x%x, got 0x%x", - name, expectedAddress, sym.Address) - } + 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") - if symmap != nil || err == nil { - t.Fatalf("expected an error because symbol address is 0") - } + require.Error(t, err) + require.Nil(t, symmap) // Check parsing invalid file symmap, err = GetKallsyms("testdata/kallsyms_invalid") - if symmap != nil || err == nil { - t.Fatalf("expected an error because file is invalid") - } + require.Error(t, err) + require.Nil(t, symmap) // Happy case symmap, err = GetKallsyms("testdata/kallsyms") - if err != nil { - t.Fatalf("error parsing kallsyms: %v", err) - } + require.NoError(t, err) + require.NotNil(t, symmap) assertSymbol(t, symmap, "cpu_tss_rw", 0x6000) assertSymbol(t, symmap, "hid_add_device", 0xffffffffc033e550) diff --git a/libpf/process/coredump.go b/process/coredump.go similarity index 97% rename from libpf/process/coredump.go rename to process/coredump.go index 1b408b85..695c3e65 100644 --- a/libpf/process/coredump.go +++ b/process/coredump.go @@ -23,6 +23,7 @@ import ( "github.com/elastic/otel-profiling-agent/libpf" "github.com/elastic/otel-profiling-agent/libpf/pfelf" + "github.com/elastic/otel-profiling-agent/util" ) const ( @@ -38,7 +39,7 @@ type CoredumpProcess struct { files map[string]*CoredumpFile // pid the original PID of the coredump - pid libpf.PID + pid util.PID // machineData contains the parsed machine data machineData MachineData @@ -251,7 +252,7 @@ func (cd *CoredumpProcess) MainExecutable() string { } // PID implements the Process interface -func (cd *CoredumpProcess) PID() libpf.PID { +func (cd *CoredumpProcess) PID() util.PID { return cd.pid } @@ -276,10 +277,9 @@ func (cd *CoredumpProcess) OpenMappingFile(_ *Mapping) (ReadAtCloser, error) { return nil, errors.New("coredump does not support opening backing file") } -// GetMappingFile implements the Process interface -func (cd *CoredumpProcess) GetMappingFile(_ *Mapping) string { - // No filesystem level backing file in coredumps - return "" +// GetMappingFileLastModified implements the Process interface +func (cd *CoredumpProcess) GetMappingFileLastModified(_ *Mapping) int64 { + return 0 } // CalculateMappingFileID implements the Process interface @@ -351,14 +351,14 @@ func (cd *CoredumpProcess) parseMappings(desc []byte, entrySize := uint64(unsafe.Sizeof(FileMappingEntry64{})) if uint64(len(desc)) < hdrSize { - return fmt.Errorf("too small NT_FILE section") + return errors.New("too small NT_FILE section") } hdr := (*FileMappingHeader64)(unsafe.Pointer(&desc[0])) offs := hdrSize + hdr.Entries*entrySize // Check that we have at least data for the headers, and a zero terminator // byte for each of the per-entry filenames. if uint64(len(desc)) < offs+hdr.Entries { - return fmt.Errorf("too small NT_FILE section") + return errors.New("too small NT_FILE section") } strs := desc[offs:] for i := uint64(0); i < hdr.Entries; i++ { @@ -449,7 +449,7 @@ type PrpsInfo64 struct { func (cd *CoredumpProcess) parseProcessInfo(desc []byte) error { if len(desc) == int(unsafe.Sizeof(PrpsInfo64{})) { info := (*PrpsInfo64)(unsafe.Pointer(&desc[0])) - cd.pid = libpf.PID(info.PID) + cd.pid = util.PID(info.PID) return nil } return fmt.Errorf("unsupported NT_PRPSINFO size: %d", len(desc)) diff --git a/libpf/process/debug.go b/process/debug.go similarity index 95% rename from libpf/process/debug.go rename to process/debug.go index 00567a93..9599787b 100644 --- a/libpf/process/debug.go +++ b/process/debug.go @@ -15,8 +15,8 @@ import ( "golang.org/x/sys/unix" - "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/remotememory" + "github.com/elastic/otel-profiling-agent/remotememory" + "github.com/elastic/otel-profiling-agent/util" ) type ptraceProcess struct { @@ -46,7 +46,7 @@ func ptraceGetRegset(tid, regset int, data []byte) error { // from one goroutine. If this is not sufficient in future, the implementation // should be refactored to pass all requests via a proxy goroutine through // channels so that the kernel requirements are fulfilled. -func NewPtrace(pid libpf.PID) (Process, error) { +func NewPtrace(pid util.PID) (Process, error) { // Lock this goroutine to the OS thread. It is ptrace API requirement // that all ptrace calls must come from same thread. runtime.LockOSThread() diff --git a/libpf/process/debug_amd64.go b/process/debug_amd64.go similarity index 100% rename from libpf/process/debug_amd64.go rename to process/debug_amd64.go diff --git a/libpf/process/debug_arm64.go b/process/debug_arm64.go similarity index 98% rename from libpf/process/debug_arm64.go rename to process/debug_arm64.go index a4e0ad6b..d61cc7cb 100644 --- a/libpf/process/debug_arm64.go +++ b/process/debug_arm64.go @@ -38,7 +38,7 @@ func (sp *ptraceProcess) getThreadInfo(tid int) (ThreadInfo, error) { return ThreadInfo{ LWP: uint32(tid), - GPRegs: prStatus[:], + GPRegs: prStatus, TPBase: binary.LittleEndian.Uint64(armTLS), }, nil } diff --git a/libpf/process/process.go b/process/process.go similarity index 77% rename from libpf/process/process.go rename to process/process.go index 26fe8bc3..9a472d00 100644 --- a/libpf/process/process.go +++ b/process/process.go @@ -16,16 +16,19 @@ import ( "os" "strings" + "golang.org/x/sys/unix" + "github.com/elastic/otel-profiling-agent/libpf" "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/elastic/otel-profiling-agent/libpf/remotememory" - "github.com/elastic/otel-profiling-agent/libpf/stringutil" + "github.com/elastic/otel-profiling-agent/remotememory" + "github.com/elastic/otel-profiling-agent/stringutil" + "github.com/elastic/otel-profiling-agent/util" ) // systemProcess provides an implementation of the Process interface for a // process that is currently running on this machine. type systemProcess struct { - pid libpf.PID + pid util.PID remoteMemory remotememory.RemoteMemory @@ -35,14 +38,14 @@ type systemProcess struct { var _ Process = &systemProcess{} // New returns an object with Process interface accessing it -func New(pid libpf.PID) Process { +func New(pid util.PID) Process { return &systemProcess{ pid: pid, remoteMemory: remotememory.NewProcessVirtualMemory(pid), } } -func (sp *systemProcess) PID() libpf.PID { +func (sp *systemProcess) PID() util.PID { return sp.pid } @@ -95,16 +98,16 @@ func parseMappings(mapsFile io.Reader) ([]Mapping, error) { flags |= elf.PF_X } - // Ignore non-executable mappings - if flags&elf.PF_X == 0 { + // Ignore non-readable and non-executable mappings + if flags&(elf.PF_R|elf.PF_X) == 0 { continue } - inode := libpf.DecToUint64(fields[4]) + inode := util.DecToUint64(fields[4]) path := fields[5] if stringutil.SplitN(fields[3], ":", devs[:]) < 2 { continue } - device := libpf.HexToUint64(devs[0])<<8 + libpf.HexToUint64(devs[1]) + device := util.HexToUint64(devs[0])<<8 + util.HexToUint64(devs[1]) if inode == 0 { if path == "[vdso]" { @@ -122,12 +125,12 @@ func parseMappings(mapsFile io.Reader) ([]Mapping, error) { path = strings.Clone(path) } - vaddr := libpf.HexToUint64(addrs[0]) + vaddr := util.HexToUint64(addrs[0]) mappings = append(mappings, Mapping{ Vaddr: vaddr, - Length: libpf.HexToUint64(addrs[1]) - vaddr, + Length: util.HexToUint64(addrs[1]) - vaddr, Flags: flags, - FileOffset: libpf.HexToUint64(fields[2]), + FileOffset: util.HexToUint64(fields[2]), Device: device, Inode: inode, Path: path, @@ -184,19 +187,30 @@ func (sp *systemProcess) extractMapping(m *Mapping) (*bytes.Reader, error) { return bytes.NewReader(data), nil } +func (sp *systemProcess) getMappingFile(m *Mapping) string { + if m.IsAnonymous() || m.IsVDSO() { + return "" + } + return fmt.Sprintf("/proc/%v/map_files/%x-%x", sp.pid, m.Vaddr, m.Vaddr+m.Length) +} + func (sp *systemProcess) OpenMappingFile(m *Mapping) (ReadAtCloser, error) { - filename := sp.GetMappingFile(m) + filename := sp.getMappingFile(m) if filename == "" { - return nil, fmt.Errorf("no backing file for anonymous memory") + return nil, errors.New("no backing file for anonymous memory") } return os.Open(filename) } -func (sp *systemProcess) GetMappingFile(m *Mapping) string { - if m.IsAnonymous() { - return "" +func (sp *systemProcess) GetMappingFileLastModified(m *Mapping) int64 { + filename := sp.getMappingFile(m) + if filename != "" { + var st unix.Stat_t + if err := unix.Stat(filename, &st); err != nil { + return st.Mtim.Nano() + } } - return fmt.Sprintf("/proc/%v/map_files/%x-%x", sp.pid, m.Vaddr, m.Vaddr+m.Length) + return 0 } // vdsoFileID caches the VDSO FileID. This assumes there is single instance of @@ -212,14 +226,18 @@ func (sp *systemProcess) CalculateMappingFileID(m *Mapping) (libpf.FileID, error if err != nil { return libpf.FileID{}, fmt.Errorf("failed to extract VDSO: %v", err) } - vdsoFileID, err = pfelf.CalculateIDFromReader(vdso) + vdsoFileID, err = libpf.FileIDFromExecutableReader(vdso) return vdsoFileID, err } - return pfelf.CalculateID(sp.GetMappingFile(m)) + return libpf.FileIDFromExecutableFile(sp.getMappingFile(m)) } func (sp *systemProcess) OpenELF(file string) (*pfelf.File, error) { - // First attempt to open via map_files as it can open deleted files. + // Always open via map_files as it can open deleted files if available. + // No fallback is attempted: + // - if the process exited, the fallback will error also (/proc/>PID> is gone) + // - if the error is due to ELF content, same error will occur in both cases + // - if the process unmapped the ELF, its data is no longer needed if m, ok := sp.fileToMapping[file]; ok { if m.IsVDSO() { vdso, err := sp.extractMapping(m) @@ -228,10 +246,7 @@ func (sp *systemProcess) OpenELF(file string) (*pfelf.File, error) { } return pfelf.NewFile(vdso, 0, false) } - ef, err := pfelf.Open(sp.GetMappingFile(m)) - if err == nil { - return ef, nil - } + return pfelf.Open(sp.getMappingFile(m)) } // Fall back to opening the file using the process specific root diff --git a/libpf/process/process_test.go b/process/process_test.go similarity index 52% rename from libpf/process/process_test.go rename to process/process_test.go index a9c98e3b..3fffbd9b 100644 --- a/libpf/process/process_test.go +++ b/process/process_test.go @@ -12,9 +12,10 @@ import ( "strings" "testing" - "github.com/elastic/otel-profiling-agent/libpf" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/otel-profiling-agent/util" ) //nolint:lll @@ -23,43 +24,58 @@ var testMappings = `55fe82710000-55fe8273c000 r--p 00000000 fd:01 1068432 55fe827be000-55fe82836000 r--p 000ae000 fd:01 1068432 /tmp/usr_bin_seahorse 55fe82836000-55fe8283d000 r--p 00125000 fd:01 1068432 /tmp/usr_bin_seahorse 55fe8283d000-55fe8283e000 rw-p 0012c000 fd:01 1068432 /tmp/usr_bin_seahorse -55fe8283e000-55fe8283f000 rw-p 00000000 00:00 0 -55fe8365d000-55fe839d6000 rw-p 00000000 00:00 0 [heap] -7f63b4000000-7f63b4021000 rw-p 00000000 00:00 0 -7f63b4021000-7f63b8000000 ---p 00000000 00:00 0 -7f63b8000000-7f63b8630000 rw-p 00000000 00:00 0 -7f63b8630000-7f63bc000000 ---p 00000000 00:00 0 -7f63bc000000-7f63bc025000 rw-p 00000000 00:00 0 -7f63bc025000-7f63c0000000 ---p 00000000 00:00 0 -7f63c0000000-7f63c0021000 rw-p 00000000 00:00 0 -7f63c0021000-7f63c4000000 ---p 00000000 00:00 0 -7f63c4000000-7f63c4021000 rw-p 00000000 00:00 0 -7f63c4021000-7f63c8000000 ---p 00000000 00:00 0 -7f63c8bb9000-7f63c8c3e000 r--p 00000000 fd:01 1048922 /tmp/usr_lib_x86_64-linux-gnu_libcrypto.so.1.1 7f63c8c3e000-7f63c8de0000 r-xp 00085000 08:01 1048922 /tmp/usr_lib_x86_64-linux-gnu_libcrypto.so.1.1 -7f63c8de0000-7f63c8e6d000 r--p 00227000 fd:01 1048922 /tmp/usr_lib_x86_64-linux-gnu_libcrypto.so.1.1 -7f63c8e6d000-7f63c8e6e000 ---p 002b4000 fd:01 1048922 /tmp/usr_lib_x86_64-linux-gnu_libcrypto.so.1.1 -7f63c8e6e000-7f63c8e9e000 r--p 002b4000 fd:01 1048922 /tmp/usr_lib_x86_64-linux-gnu_libcrypto.so.1.1 -7f63c8e9e000-7f63c8ea0000 rw-p 002e4000 fd:01 1048922 /tmp/usr_lib_x86_64-linux-gnu_libcrypto.so.1.1 -7f63c8ea0000-7f63c8ea3000 rw-p 00000000 00:00 0 -7f63c8ea3000-7f63c8ebf000 r--p 00000000 1fd:01 1075944 /tmp/usr_lib_x86_64-linux-gnu_libopensc.so.6.0.0 -7f63c8ebf000-7f63c8fef000 r-xp 0001c000 1fd:01 1075944 /tmp/usr_lib_x86_64-linux-gnu_libopensc.so.6.0.0 -7f63c8fef000-7f63c9063000 r--p 0014c000 1fd:01 1075944 /tmp/usr_lib_x86_64-linux-gnu_libopensc.so.6.0.0 -7f63c9063000-7f63c906f000 r--p 001bf000 1fd:01 1075944 /tmp/usr_lib_x86_64-linux-gnu_libopensc.so.6.0.0` +7f63c8ebf000-7f63c8fef000 r-xp 0001c000 1fd:01 1075944 /tmp/usr_lib_x86_64-linux-gnu_libopensc.so.6.0.0` func TestParseMappings(t *testing.T) { mappings, err := parseMappings(strings.NewReader(testMappings)) - assert.Nil(t, err) + require.NoError(t, err) assert.NotNil(t, mappings) expected := []Mapping{ + { + Vaddr: 0x55fe82710000, + Device: 0xfd01, + Flags: elf.PF_R, + Inode: 1068432, + Length: 0x2c000, + FileOffset: 0, + Path: "/tmp/usr_bin_seahorse", + }, { Vaddr: 0x55fe8273c000, Device: 0xfd01, Flags: elf.PF_R + elf.PF_X, Inode: 1068432, Length: 0x82000, - FileOffset: 180224, + FileOffset: 0x2c000, + Path: "/tmp/usr_bin_seahorse", + }, + { + Vaddr: 0x55fe827be000, + Device: 0xfd01, + Flags: elf.PF_R, + Inode: 1068432, + Length: 0x78000, + FileOffset: 0xae000, + Path: "/tmp/usr_bin_seahorse", + }, + { + Vaddr: 0x55fe82836000, + Device: 0xfd01, + Flags: elf.PF_R, + Inode: 1068432, + Length: 0x7000, + FileOffset: 0x125000, + Path: "/tmp/usr_bin_seahorse", + }, + { + Vaddr: 0x55fe8283d000, + Device: 0xfd01, + Flags: elf.PF_R + elf.PF_W, + Inode: 1068432, + Length: 0x1000, + FileOffset: 0x12c000, Path: "/tmp/usr_bin_seahorse", }, { @@ -85,10 +101,10 @@ func TestParseMappings(t *testing.T) { } func TestNewPIDOfSelf(t *testing.T) { - pr := New(libpf.PID(os.Getpid())) + pr := New(util.PID(os.Getpid())) assert.NotNil(t, pr) mappings, err := pr.GetMappings() - assert.Nil(t, err) - assert.Greater(t, len(mappings), 0) + require.NoError(t, err) + assert.NotEmpty(t, mappings) } diff --git a/libpf/process/types.go b/process/types.go similarity index 85% rename from libpf/process/types.go rename to process/types.go index de75522e..8358b0b3 100644 --- a/libpf/process/types.go +++ b/process/types.go @@ -15,7 +15,8 @@ import ( "github.com/elastic/otel-profiling-agent/libpf" "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/elastic/otel-profiling-agent/libpf/remotememory" + "github.com/elastic/otel-profiling-agent/remotememory" + "github.com/elastic/otel-profiling-agent/util" ) // vdsoPathName is the path to use for VDSO mappings @@ -58,8 +59,8 @@ func (m *Mapping) IsVDSO() bool { return m.Path == vdsoPathName } -func (m *Mapping) GetOnDiskFileIdentifier() libpf.OnDiskFileIdentifier { - return libpf.OnDiskFileIdentifier{ +func (m *Mapping) GetOnDiskFileIdentifier() util.OnDiskFileIdentifier { + return util.OnDiskFileIdentifier{ DeviceID: m.Device, InodeNum: m.Inode, } @@ -97,15 +98,15 @@ type ReadAtCloser interface { // GetRemoteMemory object are safe for concurrent use. type Process interface { // PID returns the process identifier - PID() libpf.PID + PID() util.PID // GetMachineData reads machine specific data from the target process GetMachineData() MachineData - // GetMapping reads and parses process memory mappings + // GetMappings reads and parses process memory mappings GetMappings() ([]Mapping, error) - // GetThread reads the process thread states + // GetThreads reads the process thread states GetThreads() ([]ThreadInfo, error) // GetRemoteMemory returns a remote memory reader accessing the target process @@ -114,9 +115,9 @@ type Process interface { // OpenMappingFile returns ReadAtCloser accessing the backing file of the mapping OpenMappingFile(*Mapping) (ReadAtCloser, error) - // GetMappingFile returns the openable file name for the mapping if available. - // Empty string is returned if the mapping file is not accessible via filesystem. - GetMappingFile(*Mapping) string + // GetMappingFileLastModifed returns the timestamp when the backing file was last modified + // or zero if an error occurs or mapping file is not accessible via filesystem + GetMappingFileLastModified(*Mapping) int64 // CalculateMappingFileID calculates FileID of the backing file CalculateMappingFileID(*Mapping) (libpf.FileID, error) diff --git a/processmanager/ebpf/asyncupdate.go b/processmanager/ebpf/asyncupdate.go new file mode 100644 index 00000000..ec928b7b --- /dev/null +++ b/processmanager/ebpf/asyncupdate.go @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package ebpf + +import ( + "context" + "errors" + "unsafe" + + cebpf "github.com/cilium/ebpf" + "github.com/elastic/otel-profiling-agent/host" + log "github.com/sirupsen/logrus" +) + +// asyncMapUpdaterPool is a pool of goroutines for doing non-blocking updates +// to BPF maps of the "map-in-map" type. +// +// This is necessary because BPF map-in-map updates have an unusually high +// latency compared to updates on other map types. They aren't computationally +// expensive, but they cause the kernel to call `synchronize_rcu` to ensure +// that the map update is actually in place before returning to user-land: +// +// https://elixir.bootlin.com/linux/v6.6.2/source/kernel/bpf/syscall.c#L142 +// +// In the simplest terms `synchronize_rcu` can be thought of like a 15-30ms +// sleep that ensures that a change in memory has propagated into the caches +// of all CPU cores. This means that any map-in-map update through the bpf +// syscall will always take about equally long to return, causing significant +// slowdown during startup. +// +// The use case in our profiling agent really doesn't need these strict sync +// guarantees; we are perfectly happy with the update being performed in an +// eventually consistent fashion. We achieve this by spawning N background +// workers and routing update requests based on the key that is supposed to +// be updated. +// +// The partitioned queue design was chosen over a work-stealing queue to ensure +// that updates on individual keys are executed in sequential order. If we +// didn't do this, it could happen that a previously enqueued and delayed +// deletion is executed after an insertion (that we want to persist) or vice +// versa. +type asyncMapUpdaterPool struct { + workers []*asyncUpdateWorker +} + +// newAsyncMapUpdaterPool creates a new worker pool +func newAsyncMapUpdaterPool(ctx context.Context, + numWorkers, workerQueueCapacity int) *asyncMapUpdaterPool { + pool := &asyncMapUpdaterPool{} + for i := 0; i < numWorkers; i++ { + queue := make(chan asyncMapInMapUpdate, workerQueueCapacity) + worker := &asyncUpdateWorker{ctx: ctx, queue: queue} + go worker.serve() + pool.workers = append(pool.workers, worker) + } + return pool +} + +// EnqueueUpdate routes a map update request to a worker in the pool. +// +// Update requests for the same file ID are guaranteed to always be routed to +// the same worker. An `inner` value of `nil` requests deletion. Ownership of +// the given `inner` map is transferred to the worker pool and `inner` is closed +// by the background worker after the update was executed. +func (p *asyncMapUpdaterPool) EnqueueUpdate( + outer *cebpf.Map, fileID host.FileID, inner *cebpf.Map) { + workerIdx := uint64(fileID) % uint64(len(p.workers)) + if err := p.workers[workerIdx].ctx.Err(); err != nil { + log.Warnf("Skipping handling of %v: %v", fileID, err) + return + } + p.workers[workerIdx].queue <- asyncMapInMapUpdate{ + Outer: outer, + FileID: fileID, + Inner: inner, + } +} + +// asyncMapInMapUpdate is an asynchronous update request for a map-in-map BPF map. +type asyncMapInMapUpdate struct { + Outer *cebpf.Map + FileID host.FileID + Inner *cebpf.Map // nil = delete +} + +// asyncUpdateWorker represents a worker in a newAsyncMapUpdaterPool. +type asyncUpdateWorker struct { + ctx context.Context + queue chan asyncMapInMapUpdate +} + +// serve is the main loop of an update worker. +func (w *asyncUpdateWorker) serve() { +WorkerLoop: + for { + var update asyncMapInMapUpdate + select { + case <-w.ctx.Done(): + break WorkerLoop + case update = <-w.queue: + } + + var err error + if update.Inner == nil { + err = update.Outer.Delete(unsafe.Pointer(&update.FileID)) + } else { + fd := uint32(update.Inner.FD()) + err = update.Outer.Update(unsafe.Pointer(&update.FileID), + unsafe.Pointer(&fd), cebpf.UpdateNoExist) + err = errors.Join(err, update.Inner.Close()) + } + + if err != nil { + log.Warnf("Outer map update failure: %v", err) + } + } + + // Shutting down: drain remaining queue capacity & close the inner maps. + for { + select { + case update := <-w.queue: + _ = update.Inner.Close() + default: + return + } + } +} diff --git a/processmanager/ebpf/asyncupdate_integration_test.go b/processmanager/ebpf/asyncupdate_integration_test.go new file mode 100644 index 00000000..8efb5ce8 --- /dev/null +++ b/processmanager/ebpf/asyncupdate_integration_test.go @@ -0,0 +1,86 @@ +//go:build integration && linux + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package ebpf + +import ( + "context" + "testing" + + "github.com/cilium/ebpf" + "golang.org/x/sync/errgroup" + + "github.com/elastic/otel-profiling-agent/host" + "github.com/elastic/otel-profiling-agent/rlimit" + + "github.com/stretchr/testify/require" +) + +func prepareMapInMap(t *testing.T) *ebpf.Map { + t.Helper() + + restoreRlimit, err := rlimit.MaximizeMemlock() + require.NoError(t, err) + defer restoreRlimit() + + outerMapSpec := ebpf.MapSpec{ + Name: "outer_map", + Type: ebpf.HashOfMaps, + KeySize: 8, + ValueSize: 4, + MaxEntries: 3, + InnerMap: &ebpf.MapSpec{ + Name: "inner_map", + Type: ebpf.Hash, + KeySize: 8, + ValueSize: 4, + MaxEntries: 3, + }, + } + + outerMap, err := ebpf.NewMap(&outerMapSpec) + require.NoError(t, err) + return outerMap +} + +func TestAsyncMapUpdaterPool(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + amup := newAsyncMapUpdaterPool(ctx, updatePoolWorkers, updatePoolQueueCap) + + outer := prepareMapInMap(t) + defer outer.Close() + + defer func() { + // If EnqueueUpdate() panics, recover() will return a non nil value. + r := recover() + require.Nil(t, r) + }() + + g, _ := errgroup.WithContext(context.Background()) + // For every worker start a Go routine that tries to send updates. + for i := 0; i < updatePoolWorkers; i++ { + g.Go(func() error { + for j := 0; j < updatePoolQueueCap*42; j++ { + // After some time, cancel the context to stop asyncUpdateWorker. + if j%updatePoolQueueCap == 0 { + cancel() + } + + fileID := host.FileID(j) + // Simulate a delete attempt for fileID after the worker context expired. + amup.EnqueueUpdate(outer, fileID, nil) + } + return nil + }) + } + + err := g.Wait() + require.NoError(t, err) +} diff --git a/processmanager/ebpf/ebpf.go b/processmanager/ebpf/ebpf.go index 13daf5d7..bb9a205b 100644 --- a/processmanager/ebpf/ebpf.go +++ b/processmanager/ebpf/ebpf.go @@ -7,6 +7,7 @@ package ebpf import ( + "context" "errors" "fmt" "math/bits" @@ -14,16 +15,19 @@ import ( "unsafe" cebpf "github.com/cilium/ebpf" + log "github.com/sirupsen/logrus" + "golang.org/x/exp/constraints" + "golang.org/x/sys/unix" + "github.com/elastic/otel-profiling-agent/host" "github.com/elastic/otel-profiling-agent/interpreter" "github.com/elastic/otel-profiling-agent/libpf" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" - "github.com/elastic/otel-profiling-agent/libpf/rlimit" "github.com/elastic/otel-profiling-agent/lpm" "github.com/elastic/otel-profiling-agent/metrics" + sdtypes "github.com/elastic/otel-profiling-agent/nativeunwind/stackdeltatypes" + "github.com/elastic/otel-profiling-agent/rlimit" "github.com/elastic/otel-profiling-agent/support" - log "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" + "github.com/elastic/otel-profiling-agent/util" ) /* @@ -32,6 +36,14 @@ import ( */ import "C" +const ( + // updatePoolWorkers decides how many background workers we spawn to + // process map-in-map updates. + updatePoolWorkers = 16 + // updatePoolQueueCap decides the work queue capacity of each worker. + updatePoolQueueCap = 8 +) + // EbpfHandler provides the functionality to interact with eBPF maps. // nolint:revive type EbpfHandler interface { @@ -39,7 +51,7 @@ type EbpfHandler interface { interpreter.EbpfHandler // RemoveReportedPID removes a PID from the reported_pids eBPF map. - RemoveReportedPID(pid libpf.PID) + RemoveReportedPID(pid util.PID) // UpdateUnwindInfo writes UnwindInfo to given unwind info array index UpdateUnwindInfo(index uint16, info sdtypes.UnwindInfo) error @@ -64,11 +76,11 @@ type EbpfHandler interface { // UpdatePidPageMappingInfo defines a function that updates the eBPF map // pid_page_to_mapping_info with the given pidAndPage and fileIDAndOffset encoded values // as key/value pair. - UpdatePidPageMappingInfo(pid libpf.PID, prefix lpm.Prefix, fileID, bias uint64) error + UpdatePidPageMappingInfo(pid util.PID, prefix lpm.Prefix, fileID, bias uint64) error // DeletePidPageMappingInfo removes the elements specified by prefixes from eBPF map // pid_page_to_mapping_info and returns the number of elements removed. - DeletePidPageMappingInfo(pid libpf.PID, prefixes []lpm.Prefix) (int, error) + DeletePidPageMappingInfo(pid util.PID, prefixes []lpm.Prefix) (int, error) // CollectMetrics returns gathered errors for changes to eBPF maps. CollectMetrics() []metrics.Metric @@ -85,13 +97,14 @@ type EbpfHandler interface { type ebpfMapsImpl struct { // Interpreter related eBPF maps interpreterOffsets *cebpf.Map + dotnetProcs *cebpf.Map perlProcs *cebpf.Map pyProcs *cebpf.Map hotspotProcs *cebpf.Map phpProcs *cebpf.Map - phpJITProcs *cebpf.Map rubyProcs *cebpf.Map v8Procs *cebpf.Map + apmIntProcs *cebpf.Map // Stackdelta and process related eBPF maps exeIDToStackDeltaMaps []*cebpf.Map @@ -105,6 +118,8 @@ type ebpfMapsImpl struct { hasGenericBatchOperations bool hasLPMTrieBatchOperations bool + + updateWorkers *asyncMapUpdaterPool } var outerMapsName = [...]string{ @@ -129,7 +144,10 @@ var _ EbpfHandler = &ebpfMapsImpl{} // LoadMaps checks if the needed maps for the process manager are available // and loads their references into a package-internal structure. -func LoadMaps(maps map[string]*cebpf.Map) (EbpfHandler, error) { +// +// It further spawns background workers for deferred map updates; the given +// context can be used to terminate them on shutdown. +func LoadMaps(ctx context.Context, maps map[string]*cebpf.Map) (EbpfHandler, error) { impl := &ebpfMapsImpl{} impl.errCounter = make(map[metrics.MetricID]int64) @@ -139,6 +157,12 @@ func LoadMaps(maps map[string]*cebpf.Map) (EbpfHandler, error) { } impl.interpreterOffsets = interpreterOffsets + dotnetProcs, ok := maps["dotnet_procs"] + if !ok { + log.Fatalf("Map dotnet_procs is not available") + } + impl.dotnetProcs = dotnetProcs + perlProcs, ok := maps["perl_procs"] if !ok { log.Fatalf("Map perl_procs is not available") @@ -163,12 +187,6 @@ func LoadMaps(maps map[string]*cebpf.Map) (EbpfHandler, error) { } impl.phpProcs = phpProcs - phpJITProcs, ok := maps["php_jit_procs"] - if !ok { - log.Fatalf("Map php_jit_procs is not available") - } - impl.phpJITProcs = phpJITProcs - rubyProcs, ok := maps["ruby_procs"] if !ok { log.Fatalf("Map ruby_procs is not available") @@ -181,6 +199,12 @@ func LoadMaps(maps map[string]*cebpf.Map) (EbpfHandler, error) { } impl.v8Procs = v8Procs + apmIntProcs, ok := maps["apm_int_procs"] + if !ok { + log.Fatalf("Map apm_int_procs is not available") + } + impl.apmIntProcs = apmIntProcs + impl.stackDeltaPageToInfo, ok = maps["stack_delta_page_to_info"] if !ok { log.Fatalf("Map stack_delta_page_to_info is not available") @@ -211,24 +235,26 @@ func LoadMaps(maps map[string]*cebpf.Map) (EbpfHandler, error) { impl.exeIDToStackDeltaMaps[i-support.StackDeltaBucketSmallest] = deltasMap } - if probeBatchOperations(cebpf.Hash) { + if err := probeBatchOperations(cebpf.Hash); err == nil { log.Infof("Supports generic eBPF map batch operations") impl.hasGenericBatchOperations = true } - if probeBatchOperations(cebpf.LPMTrie) { + if err := probeBatchOperations(cebpf.LPMTrie); err == nil { log.Infof("Supports LPM trie eBPF map batch operations") impl.hasLPMTrieBatchOperations = true } + impl.updateWorkers = newAsyncMapUpdaterPool(ctx, updatePoolWorkers, updatePoolQueueCap) + return impl, nil } // UpdateInterpreterOffsets adds the given moduleRanges to the eBPF map interpreterOffsets. func (impl *ebpfMapsImpl) UpdateInterpreterOffsets(ebpfProgIndex uint16, fileID host.FileID, - offsetRanges []libpf.Range) error { + offsetRanges []util.Range) error { if offsetRanges == nil { - return fmt.Errorf("offsetRanges is nil") + return errors.New("offsetRanges is nil") } for _, offsetRange := range offsetRanges { // The keys of this map are executable-id-and-offset-into-text entries, and @@ -252,8 +278,10 @@ func (impl *ebpfMapsImpl) UpdateInterpreterOffsets(ebpfProgIndex uint16, fileID // getInterpreterTypeMap returns the eBPF map for the given typ // or an error if typ is not supported. -func (impl *ebpfMapsImpl) getInterpreterTypeMap(typ libpf.InterpType) (*cebpf.Map, error) { +func (impl *ebpfMapsImpl) getInterpreterTypeMap(typ libpf.InterpreterType) (*cebpf.Map, error) { switch typ { + case libpf.Dotnet: + return impl.dotnetProcs, nil case libpf.Perl: return impl.perlProcs, nil case libpf.Python: @@ -262,19 +290,19 @@ func (impl *ebpfMapsImpl) getInterpreterTypeMap(typ libpf.InterpType) (*cebpf.Ma return impl.hotspotProcs, nil case libpf.PHP: return impl.phpProcs, nil - case libpf.PHPJIT: - return impl.phpJITProcs, nil case libpf.Ruby: return impl.rubyProcs, nil case libpf.V8: return impl.v8Procs, nil + case libpf.APMInt: + return impl.apmIntProcs, nil default: return nil, fmt.Errorf("type %d is not (yet) supported", typ) } } // UpdateProcData adds the given PID specific data to the specified interpreter data eBPF map. -func (impl *ebpfMapsImpl) UpdateProcData(typ libpf.InterpType, pid libpf.PID, +func (impl *ebpfMapsImpl) UpdateProcData(typ libpf.InterpreterType, pid util.PID, data unsafe.Pointer) error { log.Debugf("Loading symbol addresses into eBPF map for PID %d type %d", pid, typ) @@ -291,7 +319,7 @@ func (impl *ebpfMapsImpl) UpdateProcData(typ libpf.InterpType, pid libpf.PID, } // DeleteProcData removes the given PID specific data of the specified interpreter data eBPF map. -func (impl *ebpfMapsImpl) DeleteProcData(typ libpf.InterpType, pid libpf.PID) error { +func (impl *ebpfMapsImpl) DeleteProcData(typ libpf.InterpreterType, pid util.PID) error { log.Debugf("Removing symbol addresses from eBPF map for PID %d type %d", pid, typ) ebpfMap, err := impl.getInterpreterTypeMap(typ) @@ -308,7 +336,7 @@ func (impl *ebpfMapsImpl) DeleteProcData(typ libpf.InterpType, pid libpf.PID) er // UpdatePidInterpreterMapping updates the eBPF map pidPageToMappingInfo with the // data required to call the correct interpreter unwinder for that memory region. -func (impl *ebpfMapsImpl) UpdatePidInterpreterMapping(pid libpf.PID, prefix lpm.Prefix, +func (impl *ebpfMapsImpl) UpdatePidInterpreterMapping(pid util.PID, prefix lpm.Prefix, interpreterProgram uint8, fileID host.FileID, bias uint64) error { // pidPageToMappingInfo is a LPM trie and expects the pid and page // to be in big endian format. @@ -338,7 +366,7 @@ func (impl *ebpfMapsImpl) UpdatePidInterpreterMapping(pid libpf.PID, prefix lpm. // mapping size from the eBPF map pidPageToMappingInfo. It is normally used when an // interpreter process dies or a region that formerly required interpreter-based unwinding is no // longer needed. -func (impl *ebpfMapsImpl) DeletePidInterpreterMapping(pid libpf.PID, prefix lpm.Prefix) error { +func (impl *ebpfMapsImpl) DeletePidInterpreterMapping(pid util.PID, prefix lpm.Prefix) error { // pidPageToMappingInfo is a LPM trie and expects the pid and page // to be in big endian format. bePid := bits.ReverseBytes32(uint32(pid)) @@ -390,7 +418,7 @@ var poolPIDPage = sync.Pool{ } // getPIDPage initializes a C.PIDPage instance. -func getPIDPage(pid libpf.PID, prefix lpm.Prefix) C.PIDPage { +func getPIDPage(pid util.PID, prefix lpm.Prefix) C.PIDPage { // pid_page_to_mapping_info is an LPM trie and expects the pid and page // to be in big endian format. return C.PIDPage{ @@ -402,7 +430,7 @@ func getPIDPage(pid libpf.PID, prefix lpm.Prefix) C.PIDPage { // getPIDPagePooled returns a heap-allocated and initialized C.PIDPage instance. // After usage, put the instance back into the pool with poolPIDPage.Put(). -func getPIDPagePooled(pid libpf.PID, prefix lpm.Prefix) *C.PIDPage { +func getPIDPagePooled(pid util.PID, prefix lpm.Prefix) *C.PIDPage { cPIDPage := poolPIDPage.Get().(*C.PIDPage) *cPIDPage = getPIDPage(pid, prefix) return cPIDPage @@ -426,60 +454,68 @@ func getPIDPageMappingInfo(fileID, biasAndUnwindProgram uint64) *C.PIDPageMappin return cInfo } -// probeBatchOperations tests if the BPF syscall accepts batch operations. -func probeBatchOperations(mapType cebpf.MapType) bool { +// probeBatchOperations tests if the BPF syscall accepts batch operations. It +// returns nil if batch operations are supported for mapType or an error otherwise. +func probeBatchOperations(mapType cebpf.MapType) error { restoreRlimit, err := rlimit.MaximizeMemlock() - if errors.Is(err, unix.EPERM) { + if err != nil { // In environment like github action runners, we can not adjust rlimit. // Therefore we just return false here and do not use batch operations. - log.Errorf("Failed to adjust rlimit") - return false - } else if err != nil { - log.Fatalf("Error adjusting rlimit: %v", err) + return fmt.Errorf("failed to adjust rlimit: %w", err) } defer restoreRlimit() updates := 5 - probeMap, err := cebpf.NewMap(&cebpf.MapSpec{ + mapSpec := &cebpf.MapSpec{ Type: mapType, KeySize: 8, ValueSize: 8, MaxEntries: uint32(updates), Flags: unix.BPF_F_NO_PREALLOC, - }) + } + + var keys any + switch mapType { + case cebpf.Array: + // KeySize for Array maps always needs to be 4. + mapSpec.KeySize = 4 + // Array maps are always preallocated. + mapSpec.Flags = 0 + keys = generateSlice[uint32](updates) + default: + keys = generateSlice[uint64](updates) + } + + probeMap, err := cebpf.NewMap(mapSpec) if err != nil { - log.Errorf("Failed to create %s map for batch probing: %v", + return fmt.Errorf("failed to create %s map for batch probing: %v", mapType, err) - return false } defer probeMap.Close() - keys := make([]uint64, updates) - values := make([]uint64, updates) + values := generateSlice[uint64](updates) - for k := range keys { - keys[k] = uint64(k) - } - - n, err := probeMap.BatchUpdate(ptrCastMarshaler[uint64](keys), - ptrCastMarshaler[uint64](values), nil) - if errors.Is(err, cebpf.ErrNotSupported) { + n, err := probeMap.BatchUpdate(keys, values, nil) + if err != nil { // Older kernel do not support batch operations on maps. // This is just fine and we return here. - return false + return err } - if n != updates || err != nil { - log.Errorf("Unexpected batch update error: %v", err) - return false + if n != updates { + return fmt.Errorf("unexpected batch update return: expected %d but got %d", + updates, n) } // Remove the probe entries from the map. - m, err := probeMap.BatchDelete(ptrCastMarshaler[uint64](keys), nil) - if m != updates || err != nil { - log.Errorf("Unexpected batch delete error: %v", err) - return false + m, err := probeMap.BatchDelete(keys, nil) + if err != nil { + return err } - return true + if m != updates { + return fmt.Errorf("unexpected batch delete return: expected %d but got %d", + updates, m) + } + return nil } // getMapID returns the mapID number to use for given number of stack deltas. @@ -506,7 +542,7 @@ func (impl *ebpfMapsImpl) getOuterMap(mapID uint16) *cebpf.Map { // RemoveReportedPID removes a PID from the reported_pids eBPF map. The kernel component will // place a PID in this map before it reports it to Go for further processing. -func (impl *ebpfMapsImpl) RemoveReportedPID(pid libpf.PID) { +func (impl *ebpfMapsImpl) RemoveReportedPID(pid util.PID) { key := uint32(pid) _ = impl.reportedPIDs.Delete(unsafe.Pointer(&key)) } @@ -565,14 +601,19 @@ func (impl *ebpfMapsImpl) UpdateExeIDToStackDeltas(fileID host.FileID, deltas [] } }() - fID := uint64(fileID) - fd := uint32(innerMap.FD()) - if err = outerMap.Update(unsafe.Pointer(&fID), unsafe.Pointer(&fd), - cebpf.UpdateNoExist); err != nil { - return 0, impl.trackMapError(metrics.IDExeIDToStackDeltasUpdate, - fmt.Errorf("failed to update outer map with inner map: %v", err)) + // We continue updating the inner map after enqueueing the update to the + // outer map. Both the async update pool and our code below need an open + // file descriptor to work, and we don't know which will complete first. + // We thus clone the FD, transfer ownership of the clone to the update + // pool and continue using our original FD whose lifetime is now no longer + // tied to the FD used in the updater pool. + innerMapCloned, err := innerMap.Clone() + if err != nil { + return 0, fmt.Errorf("failed to clone inner map: %v", err) } + impl.updateWorkers.EnqueueUpdate(outerMap, fileID, innerMapCloned) + if impl.hasGenericBatchOperations { innerKeys := make([]uint32, numDeltas) stackDeltas := make([]C.StackDelta, numDeltas) @@ -623,9 +664,9 @@ func (impl *ebpfMapsImpl) DeleteExeIDToStackDeltas(fileID host.FileID, mapID uin // Deleting the entry from the outer maps deletes also the entries of the inner // map associated with this outer key. - fID := uint64(fileID) - return impl.trackMapError(metrics.IDExeIDToStackDeltasDelete, - outerMap.Delete(unsafe.Pointer(&fID))) + impl.updateWorkers.EnqueueUpdate(outerMap, fileID, nil) + + return nil } // UpdateStackDeltaPages adds fileID/page with given information to eBPF map. If the entry exists, @@ -685,7 +726,7 @@ func (impl *ebpfMapsImpl) DeleteStackDeltaPage(fileID host.FileID, page uint64) // the fileID of the text section that is mapped at this virtual address, and the offset into the // text section that this page can be found at on disk. // If the key/value pair already exists it will return an error. -func (impl *ebpfMapsImpl) UpdatePidPageMappingInfo(pid libpf.PID, prefix lpm.Prefix, +func (impl *ebpfMapsImpl) UpdatePidPageMappingInfo(pid util.PID, prefix lpm.Prefix, fileID, bias uint64) error { biasAndUnwindProgram, err := support.EncodeBiasAndUnwindProgram(bias, support.ProgUnwindNative) if err != nil { @@ -705,7 +746,7 @@ func (impl *ebpfMapsImpl) UpdatePidPageMappingInfo(pid libpf.PID, prefix lpm.Pre // DeletePidPageMappingInfo removes the elements specified by prefixes from eBPF map // pid_page_to_mapping_info and returns the number of elements removed. -func (impl *ebpfMapsImpl) DeletePidPageMappingInfo(pid libpf.PID, prefixes []lpm.Prefix) (int, +func (impl *ebpfMapsImpl) DeletePidPageMappingInfo(pid util.PID, prefixes []lpm.Prefix) (int, error) { if impl.hasLPMTrieBatchOperations { return impl.DeletePidPageMappingInfoBatch(pid, prefixes) @@ -713,7 +754,7 @@ func (impl *ebpfMapsImpl) DeletePidPageMappingInfo(pid libpf.PID, prefixes []lpm return impl.DeletePidPageMappingInfoSingle(pid, prefixes) } -func (impl *ebpfMapsImpl) DeletePidPageMappingInfoSingle(pid libpf.PID, prefixes []lpm.Prefix) (int, +func (impl *ebpfMapsImpl) DeletePidPageMappingInfoSingle(pid util.PID, prefixes []lpm.Prefix) (int, error) { var cKey = &C.PIDPage{} var deleted int @@ -730,7 +771,7 @@ func (impl *ebpfMapsImpl) DeletePidPageMappingInfoSingle(pid libpf.PID, prefixes return deleted, combinedErrors } -func (impl *ebpfMapsImpl) DeletePidPageMappingInfoBatch(pid libpf.PID, prefixes []lpm.Prefix) (int, +func (impl *ebpfMapsImpl) DeletePidPageMappingInfoBatch(pid util.PID, prefixes []lpm.Prefix) (int, error) { // Prepare all keys based on the given prefixes. cKeys := make([]C.PIDPage, 0, len(prefixes)) @@ -800,3 +841,12 @@ type ptrCastMarshaler[T any] []T func (r ptrCastMarshaler[T]) MarshalBinary() (data []byte, err error) { return libpf.SliceFrom(r), nil } + +// generateSlice returns a slice of type T and populates every value with its index. +func generateSlice[T constraints.Unsigned](num int) ptrCastMarshaler[T] { + keys := make([]T, num) + for k := range keys { + keys[k] = T(k) + } + return keys +} diff --git a/processmanager/ebpf/ebpf_integration_test.go b/processmanager/ebpf/ebpf_integration_test.go index 57e9779a..4ccbe1b2 100644 --- a/processmanager/ebpf/ebpf_integration_test.go +++ b/processmanager/ebpf/ebpf_integration_test.go @@ -12,30 +12,28 @@ import ( "testing" cebpf "github.com/cilium/ebpf" - "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/rlimit" + "github.com/elastic/otel-profiling-agent/lpm" + "github.com/elastic/otel-profiling-agent/rlimit" "github.com/elastic/otel-profiling-agent/support" + "github.com/elastic/otel-profiling-agent/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func loadTracers(t *testing.T) *ebpfMapsImpl { t.Helper() coll, err := support.LoadCollectionSpec() - if err != nil { - t.Fatalf("Failed to load specification for tracers: %v", err) - } + require.NoError(t, err) restoreRlimit, err := rlimit.MaximizeMemlock() - if err != nil { - t.Fatalf("failed to adjust rlimit: %v", err) - } + require.NoError(t, err) defer restoreRlimit() pidPageToMappingInfo, err := cebpf.NewMap(coll.Maps["pid_page_to_mapping_info"]) - if err != nil { - t.Fatalf("failed to load 'pid_page_to_mapping_info': %v", err) - } + require.NoError(t, err) return &ebpfMapsImpl{ pidPageToMappingInfo: pidPageToMappingInfo, @@ -44,7 +42,7 @@ func loadTracers(t *testing.T) *ebpfMapsImpl { func TestLPM(t *testing.T) { tests := map[string]struct { - pid libpf.PID + pid util.PID page uint64 pageBits uint32 rip uint64 @@ -65,29 +63,28 @@ func TestLPM(t *testing.T) { Key: test.page, Length: test.pageBits, } - if err := impl.UpdatePidPageMappingInfo(test.pid, prefix, test.fileID, test.bias); err != nil { - t.Fatalf("failed to insert value into eBPF map: %v", err) - } - if fileID, bias, err := impl.LookupPidPageInformation(uint32(test.pid), test.rip); err != nil { - t.Errorf("failed to lookup element: %v", err) - } else { - if uint64(fileID) != test.fileID { - t.Fatalf("expected fileID 0x%x but got 0x%x", test.fileID, fileID) - } - if bias != test.bias { - t.Fatalf("expected bias 0x%x but got 0x%x", test.bias, bias) - } - } - if _, err := impl.DeletePidPageMappingInfo(test.pid, []lpm.Prefix{prefix}); err != nil { - t.Fatalf("failed to delete value from eBPF map: %v", err) + err := impl.UpdatePidPageMappingInfo(test.pid, prefix, test.fileID, test.bias) + require.NoError(t, err) + + fileID, bias, err := impl.LookupPidPageInformation(uint32(test.pid), test.rip) + if assert.NoError(t, err) { + assert.Equal(t, test.fileID, uint64(fileID)) + assert.Equal(t, test.bias, bias) } + + _, err = impl.DeletePidPageMappingInfo(test.pid, []lpm.Prefix{prefix}) + require.NoError(t, err) }) } } func TestBatchOperations(t *testing.T) { for _, mapType := range []cebpf.MapType{cebpf.Hash, cebpf.Array, cebpf.LPMTrie} { - ok := probeBatchOperations(mapType) - t.Logf("Batch operations are supported for %s: %v", mapType, ok) + t.Run(mapType.String(), func(t *testing.T) { + err := probeBatchOperations(mapType) + if err != nil { + require.ErrorIs(t, err, cebpf.ErrNotSupported) + } + }) } } diff --git a/processmanager/ebpf/ebpf_test.go b/processmanager/ebpf/ebpf_test.go index 75f8978c..da5fc55d 100644 --- a/processmanager/ebpf/ebpf_test.go +++ b/processmanager/ebpf/ebpf_test.go @@ -14,6 +14,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMapID(t *testing.T) { @@ -34,16 +35,12 @@ func TestMapID(t *testing.T) { expectedShift := expectedShift t.Run(fmt.Sprintf("deltas %d", numStackDeltas), func(t *testing.T) { shift, err := getMapID(numStackDeltas) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) assert.Equal(t, expectedShift, shift, fmt.Sprintf("wrong map name for %d deltas", numStackDeltas)) }) } _, err := getMapID(1 << 22) - if err == nil { - t.Fatalf("expected an error") - } + require.Error(t, err) } diff --git a/processmanager/execinfomanager/manager.go b/processmanager/execinfomanager/manager.go index 2c99be22..fa2a72f7 100644 --- a/processmanager/execinfomanager/manager.go +++ b/processmanager/execinfomanager/manager.go @@ -10,28 +10,33 @@ import ( "errors" "fmt" "os" + "time" + + "github.com/elastic/otel-profiling-agent/libpf" + log "github.com/sirupsen/logrus" + + lru "github.com/elastic/go-freelru" "github.com/elastic/otel-profiling-agent/config" "github.com/elastic/otel-profiling-agent/host" "github.com/elastic/otel-profiling-agent/interpreter" + "github.com/elastic/otel-profiling-agent/interpreter/apmint" + "github.com/elastic/otel-profiling-agent/interpreter/dotnet" "github.com/elastic/otel-profiling-agent/interpreter/hotspot" "github.com/elastic/otel-profiling-agent/interpreter/nodev8" "github.com/elastic/otel-profiling-agent/interpreter/perl" "github.com/elastic/otel-profiling-agent/interpreter/php" - "github.com/elastic/otel-profiling-agent/interpreter/php/phpjit" "github.com/elastic/otel-profiling-agent/interpreter/python" "github.com/elastic/otel-profiling-agent/interpreter/ruby" - "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/nativeunwind" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" "github.com/elastic/otel-profiling-agent/libpf/pfelf" "github.com/elastic/otel-profiling-agent/libpf/xsync" "github.com/elastic/otel-profiling-agent/metrics" + "github.com/elastic/otel-profiling-agent/nativeunwind" + sdtypes "github.com/elastic/otel-profiling-agent/nativeunwind/stackdeltatypes" pmebpf "github.com/elastic/otel-profiling-agent/processmanager/ebpf" "github.com/elastic/otel-profiling-agent/support" "github.com/elastic/otel-profiling-agent/tpbase" - log "github.com/sirupsen/logrus" - "go.uber.org/multierr" + "github.com/elastic/otel-profiling-agent/util" ) const ( @@ -39,6 +44,19 @@ const ( // recorded. Currently reflects the V8 binary blob size, in which // the gap size is >= 512kB. minimumMemoizableGapSize = 512 * 1024 + + // deferredFileIDSize defines the maximum size of the deferredFileIDs LRU + // cache that contains file IDs for which stack delta extraction is deferred + // to avoid busy loops. + deferredFileIDSize = 8192 + // TTL of entries in the deferredFileIDs LRU cache. + deferredFileIDTimeout = 90 * time.Second +) + +var ( + // ErrDeferredFileID indicates that handling of stack deltas for a file ID failed + // and should only be tried again at a later point. + ErrDeferredFileID = errors.New("deferred FileID") ) // ExecutableInfo stores information about an executable (ELF file). @@ -74,34 +92,50 @@ type ExecutableInfoManager struct { // state bundles up all mutable state of the manager. state xsync.RWMutex[executableInfoManagerState] + + // deferredFileIDs caches file IDs for which stack delta extraction failed and + // retrying extraction of stack deltas should be deferred for some time. + deferredFileIDs *lru.SyncedLRU[host.FileID, libpf.Void] } // NewExecutableInfoManager creates a new instance of the executable info manager. func NewExecutableInfoManager( sdp nativeunwind.StackDeltaProvider, ebpf pmebpf.EbpfHandler, - includeTracers []bool, -) *ExecutableInfoManager { + includeTracers config.IncludedTracers, +) (*ExecutableInfoManager, error) { // Initialize interpreter loaders. interpreterLoaders := make([]interpreter.Loader, 0) - if includeTracers[config.PerlTracer] { + if includeTracers.Has(config.PerlTracer) { interpreterLoaders = append(interpreterLoaders, perl.Loader) } - if includeTracers[config.PythonTracer] { + if includeTracers.Has(config.PythonTracer) { interpreterLoaders = append(interpreterLoaders, python.Loader) } - if includeTracers[config.PHPTracer] { - interpreterLoaders = append(interpreterLoaders, php.Loader, phpjit.Loader) + if includeTracers.Has(config.PHPTracer) { + interpreterLoaders = append(interpreterLoaders, php.Loader, php.OpcacheLoader) } - if includeTracers[config.HotspotTracer] { + if includeTracers.Has(config.HotspotTracer) { interpreterLoaders = append(interpreterLoaders, hotspot.Loader) } - if includeTracers[config.RubyTracer] { + if includeTracers.Has(config.RubyTracer) { interpreterLoaders = append(interpreterLoaders, ruby.Loader) } - if includeTracers[config.V8Tracer] { + if includeTracers.Has(config.V8Tracer) { interpreterLoaders = append(interpreterLoaders, nodev8.Loader) } + if includeTracers.Has(config.DotnetTracer) { + interpreterLoaders = append(interpreterLoaders, dotnet.Loader) + } + + interpreterLoaders = append(interpreterLoaders, apmint.Loader) + + deferredFileIDs, err := lru.NewSynced[host.FileID, libpf.Void](deferredFileIDSize, + func(id host.FileID) uint32 { return uint32(id) }) + if err != nil { + return nil, err + } + deferredFileIDs.SetLifetime(deferredFileIDTimeout) return &ExecutableInfoManager{ sdp: sdp, @@ -111,7 +145,8 @@ func NewExecutableInfoManager( unwindInfoIndex: map[sdtypes.UnwindInfo]uint16{}, ebpf: ebpf, }), - } + deferredFileIDs: deferredFileIDs, + }, nil } // AddOrIncRef either adds information about an executable to the internal cache (when first @@ -121,11 +156,14 @@ func NewExecutableInfoManager( // of getters and more complicated locking semantics. func (mgr *ExecutableInfoManager) AddOrIncRef(fileID host.FileID, elfRef *pfelf.Reference) (ExecutableInfo, error) { + if _, exists := mgr.deferredFileIDs.Get(fileID); exists { + return ExecutableInfo{}, ErrDeferredFileID + } var ( intervalData sdtypes.IntervalData tsdInfo *tpbase.TSDInfo ref mapRef - gaps []libpf.Range + gaps []util.Range err error ) @@ -143,6 +181,9 @@ func (mgr *ExecutableInfoManager) AddOrIncRef(fileID host.FileID, mgr.state.WUnlock(&state) if err = mgr.sdp.GetIntervalStructuresForFile(fileID, elfRef, &intervalData); err != nil { + if !errors.Is(err, os.ErrNotExist) { + mgr.deferredFileIDs.Add(fileID, libpf.Void{}) + } return ExecutableInfo{}, fmt.Errorf("failed to extract interval data: %w", err) } @@ -165,6 +206,7 @@ func (mgr *ExecutableInfoManager) AddOrIncRef(fileID host.FileID, // Load the data into BPF maps. ref, gaps, err = state.loadDeltas(fileID, intervalData.Deltas) if err != nil { + mgr.deferredFileIDs.Add(fileID, libpf.Void{}) return ExecutableInfo{}, fmt.Errorf("failed to load deltas: %w", err) } @@ -195,7 +237,7 @@ func (mgr *ExecutableInfoManager) AddSynthIntervalData( defer mgr.state.WUnlock(&state) if _, exists := state.executables[fileID]; exists { - return fmt.Errorf("AddSynthIntervalData: mapping already exists") + return errors.New("AddSynthIntervalData: mapping already exists") } ref, _, err := state.loadDeltas(fileID, data.Deltas) @@ -233,7 +275,7 @@ func (mgr *ExecutableInfoManager) RemoveOrDecRef(fileID host.FileID) error { delete(state.executables, fileID) case 0: // This should be unreachable. - return fmt.Errorf("state corruption in ExecutableInfoManager: encountered 0 RC") + return errors.New("state corruption in ExecutableInfoManager: encountered 0 RC") default: info.rc-- } @@ -260,10 +302,8 @@ func (mgr *ExecutableInfoManager) UpdateMetricSummary(summary metrics.Summary) { mgr.state.RUnlock(&state) deltaProviderStatistics := mgr.sdp.GetAndResetStatistics() - summary[metrics.IDStackDeltaProviderCacheHit] = - metrics.MetricValue(deltaProviderStatistics.Hit) - summary[metrics.IDStackDeltaProviderCacheMiss] = - metrics.MetricValue(deltaProviderStatistics.Miss) + summary[metrics.IDStackDeltaProviderSuccess] = + metrics.MetricValue(deltaProviderStatistics.Success) summary[metrics.IDStackDeltaProviderExtractionError] = metrics.MetricValue(deltaProviderStatistics.ExtractionErrors) } @@ -329,11 +369,11 @@ func (state *executableInfoManagerState) detectAndLoadInterpData( func (state *executableInfoManagerState) loadDeltas( fileID host.FileID, deltas []sdtypes.StackDelta, -) (ref mapRef, gaps []libpf.Range, err error) { +) (ref mapRef, gaps []util.Range, err error) { numDeltas := len(deltas) if numDeltas == 0 { // If no deltas are extracted, cache the result but don't reserve memory in BPF maps. - return mapRef{MapID: 0}, []libpf.Range{}, nil + return mapRef{MapID: 0}, []util.Range{}, nil } firstPage := deltas[0].Address >> support.StackDeltaPageBits @@ -359,7 +399,7 @@ func (state *executableInfoManagerState) loadDeltas( nextDeltaAddr-delta.Address >= minimumMemoizableGapSize { // Remember large gaps so ProcessManager plugins can // later use them to find precompiled blobs without deltas. - gaps = append(gaps, libpf.Range{ + gaps = append(gaps, util.Range{ Start: delta.Address, End: nextDeltaAddr}) } @@ -464,15 +504,13 @@ func (state *executableInfoManagerState) unloadDeltas( var err error for i := uint64(0); i < uint64(ref.NumPages); i++ { pageAddr := ref.StartPage + i< 0, eim: em, interpreters: interpreters, - exitEvents: make(map[libpf.PID]libpf.KTime), - pidToProcessInfo: make(map[libpf.PID]*processInfo), + exitEvents: make(map[util.PID]util.KTime), + pidToProcessInfo: make(map[util.PID]*processInfo), ebpf: ebpf, FileIDMapper: fileIDMapper, elfInfoCache: elfInfoCache, @@ -186,7 +194,7 @@ func (pm *ProcessManager) symbolizeFrame(frame int, trace *host.Trace, defer pm.mu.Unlock() if len(pm.interpreters[trace.PID]) == 0 { - return fmt.Errorf("interpreter process gone") + return errors.New("interpreter process gone") } for _, instance := range pm.interpreters[trace.PID] { @@ -263,7 +271,7 @@ func (pm *ProcessManager) ConvertTrace(trace *host.Trace) (newTrace *libpf.Trace // add %rax, %rbx <- address of frame 1 == return address of frame 0 relativeRIP := frame.Lineno - if i > 0 || frame.Type.IsInterpType(libpf.Kernel) { + if frame.ReturnAddress { relativeRIP-- } fileID, ok := pm.FileIDMapper.Get(frame.File) @@ -292,11 +300,32 @@ func (pm *ProcessManager) ConvertTrace(trace *host.Trace) (newTrace *libpf.Trace return newTrace } -func (pm *ProcessManager) SymbolizationComplete(traceCaptureKTime libpf.KTime) { +func (pm *ProcessManager) MaybeNotifyAPMAgent( + rawTrace *host.Trace, umTraceHash libpf.TraceHash, count uint16) string { + pidInterp, ok := pm.interpreters[rawTrace.PID] + if !ok { + return "" + } + + var serviceName string + for _, mapping := range pidInterp { + if apm, ok := mapping.(*apmint.Instance); ok { + apm.NotifyAPMAgent(rawTrace.PID, rawTrace, umTraceHash, count) + + // It's pretty unusual for there to be more than one APM agent in a + // single process, but in case there is, just pick the last one. + serviceName = apm.APMServiceName() + } + } + + return serviceName +} + +func (pm *ProcessManager) SymbolizationComplete(traceCaptureKTime util.KTime) { pm.mu.Lock() defer pm.mu.Unlock() - nowKTime := libpf.GetKTime() + nowKTime := util.GetKTime() for pid, pidExitKTime := range pm.exitEvents { if pidExitKTime > traceCaptureKTime { @@ -315,6 +344,9 @@ func (pm *ProcessManager) SymbolizationComplete(traceCaptureKTime libpf.KTime) { } } +// Compile time check to make sure we satisfy the interface. +var _ tracehandler.TraceProcessor = (*ProcessManager)(nil) + // AddSynthIntervalData adds synthetic stack deltas to the manager. This is useful for cases where // populating the information via the stack delta provider isn't viable, for example because the // `.eh_frame` section for a binary is broken. If `AddSynthIntervalData` was called for a given diff --git a/processmanager/manager_test.go b/processmanager/manager_test.go index 73798550..e14cb495 100644 --- a/processmanager/manager_test.go +++ b/processmanager/manager_test.go @@ -14,7 +14,6 @@ import ( "fmt" "math/rand" "os" - "reflect" "testing" "time" "unsafe" @@ -23,23 +22,28 @@ import ( "github.com/elastic/otel-profiling-agent/host" "github.com/elastic/otel-profiling-agent/interpreter" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/nativeunwind" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/elastic/otel-profiling-agent/libpf/process" - "github.com/elastic/otel-profiling-agent/libpf/remotememory" - "github.com/elastic/otel-profiling-agent/libpf/traceutil" "github.com/elastic/otel-profiling-agent/lpm" "github.com/elastic/otel-profiling-agent/metrics" + "github.com/elastic/otel-profiling-agent/nativeunwind" + sdtypes "github.com/elastic/otel-profiling-agent/nativeunwind/stackdeltatypes" + "github.com/elastic/otel-profiling-agent/process" pmebpf "github.com/elastic/otel-profiling-agent/processmanager/ebpf" + "github.com/elastic/otel-profiling-agent/remotememory" + "github.com/elastic/otel-profiling-agent/reporter" + "github.com/elastic/otel-profiling-agent/traceutil" + "github.com/elastic/otel-profiling-agent/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // dummyProcess implements pfelf.Process for testing purposes type dummyProcess struct { - pid libpf.PID + pid util.PID } -func (d *dummyProcess) PID() libpf.PID { +func (d *dummyProcess) PID() util.PID { return d.pid } @@ -59,12 +63,12 @@ func (d *dummyProcess) GetRemoteMemory() remotememory.RemoteMemory { return remotememory.RemoteMemory{} } -func (d *dummyProcess) GetMappingFile(_ *process.Mapping) string { - return "" +func (d *dummyProcess) GetMappingFileLastModified(_ *process.Mapping) int64 { + return 0 } func (d *dummyProcess) CalculateMappingFileID(m *process.Mapping) (libpf.FileID, error) { - return pfelf.CalculateID(m.Path) + return libpf.FileIDFromExecutableFile(m.Path) } func (d *dummyProcess) OpenMappingFile(m *process.Mapping) (process.ReadAtCloser, error) { @@ -79,7 +83,7 @@ func (d *dummyProcess) Close() error { return nil } -func newTestProcess(pid libpf.PID) process.Process { +func newTestProcess(pid util.PID) process.Process { return &dummyProcess{pid: pid} } @@ -124,18 +128,15 @@ func generateDummyFiles(t *testing.T, num int) []string { for i := 0; i < num; i++ { name := fmt.Sprintf("dummy%d", i) tmpfile, err := os.CreateTemp("", "*"+name) - if err != nil { - t.Fatalf("Failed to create dummy file %s: %v", name, err) - } + require.NoError(t, err) + // The generated fileID is based on the content of the file. // So we write the pseudo random name to the file as content. content := []byte(tmpfile.Name()) - if _, err := tmpfile.Write(content); err != nil { - t.Fatalf("Failed to write dummy content to file: %v", err) - } - if err := tmpfile.Close(); err != nil { - t.Fatalf("Failed to close temporary file: %v", err) - } + _, err = tmpfile.Write(content) + require.NoError(t, err) + tmpfile.Close() + require.NoError(t, err) files = append(files, tmpfile.Name()) } return files @@ -145,7 +146,7 @@ func generateDummyFiles(t *testing.T, num int) []string { // for the tests. type mappingArgs struct { // pid represents the simulated process ID. - pid libpf.PID + pid util.PID // vaddr represents the simulated start of the mapped memory. vaddr uint64 // bias is the load bias to simulate and verify. @@ -172,29 +173,31 @@ type ebpfMapsMockup struct { var _ interpreter.EbpfHandler = &ebpfMapsMockup{} -func (mockup *ebpfMapsMockup) RemoveReportedPID(libpf.PID) { +func (mockup *ebpfMapsMockup) RemoveReportedPID(util.PID) { } -func (mockup *ebpfMapsMockup) UpdateInterpreterOffsets(uint16, host.FileID, []libpf.Range) error { +func (mockup *ebpfMapsMockup) UpdateInterpreterOffsets(uint16, host.FileID, + []util.Range) error { return nil } -func (mockup *ebpfMapsMockup) UpdateProcData(libpf.InterpType, libpf.PID, unsafe.Pointer) error { +func (mockup *ebpfMapsMockup) UpdateProcData(libpf.InterpreterType, util.PID, + unsafe.Pointer) error { mockup.updateProcCount++ return nil } -func (mockup *ebpfMapsMockup) DeleteProcData(libpf.InterpType, libpf.PID) error { +func (mockup *ebpfMapsMockup) DeleteProcData(libpf.InterpreterType, util.PID) error { mockup.deleteProcCount++ return nil } -func (mockup *ebpfMapsMockup) UpdatePidInterpreterMapping(libpf.PID, +func (mockup *ebpfMapsMockup) UpdatePidInterpreterMapping(util.PID, lpm.Prefix, uint8, host.FileID, uint64) error { return nil } -func (mockup *ebpfMapsMockup) DeletePidInterpreterMapping(libpf.PID, lpm.Prefix) error { +func (mockup *ebpfMapsMockup) DeletePidInterpreterMapping(util.PID, lpm.Prefix) error { return nil } @@ -223,7 +226,7 @@ func (mockup *ebpfMapsMockup) DeleteStackDeltaPage(host.FileID, uint64) error { return nil } -func (mockup *ebpfMapsMockup) UpdatePidPageMappingInfo(pid libpf.PID, prefix lpm.Prefix, +func (mockup *ebpfMapsMockup) UpdatePidPageMappingInfo(pid util.PID, prefix lpm.Prefix, fileID uint64, bias uint64) error { if prefix.Key == 0 && fileID == 0 && bias == 0 { // If all provided values are 0 the hook was called to create @@ -241,7 +244,7 @@ func (mockup *ebpfMapsMockup) setExpectedBias(expected uint64) { mockup.expectedBias = expected } -func (mockup *ebpfMapsMockup) DeletePidPageMappingInfo(_ libpf.PID, prefixes []lpm.Prefix) (int, +func (mockup *ebpfMapsMockup) DeletePidPageMappingInfo(_ util.PID, prefixes []lpm.Prefix) (int, error) { mockup.deletePidPageMappingCount += uint8(len(prefixes)) return len(prefixes), nil @@ -251,9 +254,23 @@ func (mockup *ebpfMapsMockup) CollectMetrics() []metrics.Metric { return []m func (mockup *ebpfMapsMockup) SupportsGenericBatchOperations() bool { return false } func (mockup *ebpfMapsMockup) SupportsLPMTrieBatchOperations() bool { return false } +type symbolReporterMockup struct{} + +func (s *symbolReporterMockup) ReportFallbackSymbol(_ libpf.FrameID, _ string) {} + +func (s *symbolReporterMockup) ExecutableMetadata(_ context.Context, _ libpf.FileID, _, _ string) { +} + +func (s *symbolReporterMockup) FrameMetadata(_ libpf.FileID, _ libpf.AddressOrLineno, + _ util.SourceLineno, _ uint32, _, _ string) { +} + +var _ reporter.SymbolReporter = (*symbolReporterMockup)(nil) + func TestInterpreterConvertTrace(t *testing.T) { partialNativeFrameFileID := uint64(0xabcdbeef) nativeFrameLineno := libpf.AddressOrLineno(0x1234) + pythonAndNativeTrace := &host.Trace{ Frames: []host.Frame{{ // This represents a native frame @@ -287,31 +304,29 @@ func TestInterpreterConvertTrace(t *testing.T) { mapper.Set(testcase.trace.Frames[i].File, testcase.expect.Files[i]) } - interpreters := make([]bool, config.MaxTracers) + // For this test do not include interpreters. + noIinterpreters, _ := config.ParseTracers("") ctx, cancel := context.WithCancel(context.Background()) defer cancel() // To test ConvertTrace we do not require all parts of processmanager. manager, err := New(ctx, - interpreters, + noIinterpreters, 1*time.Second, nil, nil, - nil, + &symbolReporterMockup{}, nil, true) - if err != nil { - t.Fatalf("Failed to initialize new process manager: %v", err) - } + require.NoError(t, err) newTrace := manager.ConvertTrace(testcase.trace) testcase.expect.Hash = traceutil.HashTrace(testcase.expect) - if (!reflect.DeepEqual(testcase.expect.Linenos, newTrace.Linenos) || - !reflect.DeepEqual(testcase.expect.Files, newTrace.Files)) && - testcase.expect.Hash == newTrace.Hash { - t.Fatalf("Trace %v does not match expected trace %v", newTrace, testcase.expect) + if testcase.expect.Hash == newTrace.Hash { + assert.Equal(t, testcase.expect.Linenos, newTrace.Linenos) + assert.Equal(t, testcase.expect.Files, newTrace.Files) } }) } @@ -332,6 +347,7 @@ func getExpectedTrace(origTrace *host.Trace, linenos []libpf.AddressOrLineno) *l newTrace.Linenos = append(newTrace.Linenos, frame.Lineno) } } + if linenos != nil { newTrace.Linenos = linenos } @@ -366,17 +382,14 @@ func TestNewMapping(t *testing.T) { } cacheDir, err := os.MkdirTemp("", "*_cacheDir") - if err != nil { - t.Fatalf("Failed to create cache directory: %v", err) - } + require.NoError(t, err) defer os.RemoveAll(cacheDir) - if err = config.SetConfiguration(&config.Config{ + err = config.SetConfiguration(&config.Config{ ProjectID: 42, CacheDirectory: cacheDir, - SecretToken: "secret"}); err != nil { - t.Fatalf("failed to set temporary config: %s", err) - } + SecretToken: "secret"}) + require.NoError(t, err) for name, testcase := range tests { testcase := testcase @@ -385,9 +398,10 @@ func TestNewMapping(t *testing.T) { // so we replace the stack delta provider. dummyProvider := dummyStackDeltaProvider{} ebpfMockup := &ebpfMapsMockup{} + symRepMockup := &symbolReporterMockup{} // For this test do not include interpreters. - noInterpreters := make([]bool, config.MaxTracers) + noInterpreters, _ := config.ParseTracers("") ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -397,12 +411,10 @@ func TestNewMapping(t *testing.T) { 1*time.Second, ebpfMockup, NewMapFileIDMapper(), - nil, + symRepMockup, &dummyProvider, true) - if err != nil { - t.Fatalf("Failed to initialize new process manager: %v", err) - } + require.NoError(t, err) // Replace the internal hooks for the tests. These hooks catch the // updates of the eBPF maps and let us compare the results. @@ -445,16 +457,10 @@ func TestNewMapping(t *testing.T) { Length: 0x10, }, elfRef) elfRef.Close() - if err != nil { - t.Fatalf("Failed to add new mapping: %v", err) - } + require.NoError(t, err) } - if len(ebpfMockup.stackDeltaMemory) != testcase.expectedStackDeltas { - t.Fatalf("Expected %d entries in big_stack_deltas but got %d", - testcase.expectedStackDeltas, - len(ebpfMockup.stackDeltaMemory)) - } + assert.Len(t, ebpfMockup.stackDeltaMemory, testcase.expectedStackDeltas) }) } } @@ -464,7 +470,7 @@ func populateManager(t *testing.T, pm *ProcessManager) { t.Helper() data := []struct { - pid libpf.PID + pid util.PID mapping Mapping }{ @@ -534,16 +540,15 @@ func populateManager(t *testing.T, pm *ProcessManager) { mockup.setExpectedBias(c.mapping.Bias) pr := newTestProcess(c.pid) elfRef := pfelf.NewReference("", pr) - if err := pm.handleNewMapping(pr, &c.mapping, elfRef); err != nil { - t.Fatalf("Failed to populate manager with process: %v", err) - } + err := pm.handleNewMapping(pr, &c.mapping, elfRef) + require.NoError(t, err) } } func TestProcExit(t *testing.T) { tests := map[string]struct { // pid represents the ID of a process. - pid libpf.PID + pid util.PID // deletePidPageMappingCount reflects the number of times // the deletePidPageMappingHook to update the eBPF map was called. deletePidPageMappingCount uint8 @@ -584,9 +589,10 @@ func TestProcExit(t *testing.T) { // so we replace the stack delta provider. dummyProvider := dummyStackDeltaProvider{} ebpfMockup := &ebpfMapsMockup{} + repMockup := &symbolReporterMockup{} // For this test do not include interpreters. - noInterpreters := make([]bool, config.MaxTracers) + noInterpreters, _ := config.ParseTracers("") ctx, cancel := context.WithCancel(context.Background()) @@ -595,12 +601,10 @@ func TestProcExit(t *testing.T) { 1*time.Second, ebpfMockup, NewMapFileIDMapper(), - nil, + repMockup, &dummyProvider, true) - if err != nil { - t.Fatalf("Failed to initialize new process manager: %v", err) - } + require.NoError(t, err) defer cancel() // Replace the internal hooks for the tests. These hooks catch the @@ -614,23 +618,12 @@ func TestProcExit(t *testing.T) { populateManager(t, manager) _ = manager.ProcessPIDExit(testcase.pid) - if testcase.deletePidPageMappingCount != ebpfMockup.deletePidPageMappingCount { - t.Fatalf("Calls of deletePidPageMappingHook. Expected: %d\tGot: %d", - testcase.deletePidPageMappingCount, - ebpfMockup.deletePidPageMappingCount) - } - - if testcase.deleteStackDeltaRangesCount != ebpfMockup.deleteStackDeltaPage { - t.Fatalf("Calls of DeleteStackDeltaPage. Expected: %d\tGot: %d", - testcase.deleteStackDeltaRangesCount, - ebpfMockup.deleteStackDeltaPage) - } - - if testcase.deleteStackDeltaRangesCount != ebpfMockup.deleteStackDeltaRangesCount { - t.Fatalf("Calls of deleteStackDeltaRangesCountHook. Expected: %d\tGot: %d", - testcase.deleteStackDeltaRangesCount, - ebpfMockup.deleteStackDeltaRangesCount) - } + assert.Equal(t, testcase.deletePidPageMappingCount, + ebpfMockup.deletePidPageMappingCount) + assert.Equal(t, testcase.deleteStackDeltaRangesCount, + ebpfMockup.deleteStackDeltaPage) + assert.Equal(t, testcase.deleteStackDeltaRangesCount, + ebpfMockup.deleteStackDeltaRangesCount) }) } } diff --git a/processmanager/processinfo.go b/processmanager/processinfo.go index 82103cd8..495ec8d4 100644 --- a/processmanager/processinfo.go +++ b/processmanager/processinfo.go @@ -23,25 +23,24 @@ import ( "syscall" "time" - "golang.org/x/sys/unix" + log "github.com/sirupsen/logrus" "github.com/elastic/otel-profiling-agent/host" "github.com/elastic/otel-profiling-agent/interpreter" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/memorydebug" "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/elastic/otel-profiling-agent/libpf/process" "github.com/elastic/otel-profiling-agent/lpm" + "github.com/elastic/otel-profiling-agent/memorydebug" "github.com/elastic/otel-profiling-agent/proc" + "github.com/elastic/otel-profiling-agent/process" eim "github.com/elastic/otel-profiling-agent/processmanager/execinfomanager" "github.com/elastic/otel-profiling-agent/tpbase" - - log "github.com/sirupsen/logrus" + "github.com/elastic/otel-profiling-agent/util" ) // 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) { +func (pm *ProcessManager) assignTSDInfo(pid util.PID, tsdInfo *tpbase.TSDInfo) { if tsdInfo == nil { return } @@ -68,7 +67,7 @@ func (pm *ProcessManager) assignTSDInfo(pid libpf.PID, tsdInfo *tpbase.TSDInfo) // getTSDInfo retrieves the TSDInfo of given PID // Caller must hold pm.mu read lock. -func (pm *ProcessManager) getTSDInfo(pid libpf.PID) *tpbase.TSDInfo { +func (pm *ProcessManager) getTSDInfo(pid util.PID) *tpbase.TSDInfo { if info, ok := pm.pidToProcessInfo[pid]; ok { return info.tsdInfo } @@ -81,7 +80,7 @@ func (pm *ProcessManager) getTSDInfo(pid libpf.PID) *tpbase.TSDInfo { // already exists, it returns true. Otherwise false or an error. // // Caller must hold pm.mu write lock. -func (pm *ProcessManager) updatePidInformation(pid libpf.PID, m *Mapping) (bool, error) { +func (pm *ProcessManager) updatePidInformation(pid util.PID, m *Mapping) (bool, error) { info, ok := pm.pidToProcessInfo[pid] if !ok { // We don't have information for this pid, so we first need to @@ -133,7 +132,7 @@ func (pm *ProcessManager) updatePidInformation(pid libpf.PID, m *Mapping) (bool, // deletePIDAddress removes the mapping at addr from pid from the internal structure of the // process manager instance as well as from the eBPF maps. // Caller must hold pm.mu write lock. -func (pm *ProcessManager) deletePIDAddress(pid libpf.PID, addr libpf.Address) error { +func (pm *ProcessManager) deletePIDAddress(pid util.PID, addr libpf.Address) error { info, ok := pm.pidToProcessInfo[pid] if !ok { return fmt.Errorf("unknown PID %d: %w", pid, errUnknownPID) @@ -162,12 +161,12 @@ func (pm *ProcessManager) deletePIDAddress(pid libpf.PID, addr libpf.Address) er } // assignInterpreter will update the interpreters maps with given interpreter.Instance. -func (pm *ProcessManager) assignInterpreter(pid libpf.PID, key libpf.OnDiskFileIdentifier, +func (pm *ProcessManager) assignInterpreter(pid util.PID, key util.OnDiskFileIdentifier, instance interpreter.Instance) { if _, ok := pm.interpreters[pid]; !ok { // This is the very first interpreter entry for this process. // So we need to initialize the structure first. - pm.interpreters[pid] = make(map[libpf.OnDiskFileIdentifier]interpreter.Instance) + pm.interpreters[pid] = make(map[util.OnDiskFileIdentifier]interpreter.Instance) } pm.interpreters[pid][key] = instance } @@ -247,19 +246,8 @@ func (pm *ProcessManager) handleNewMapping(pr process.Process, m *Mapping, func (pm *ProcessManager) getELFInfo(pr process.Process, mapping *process.Mapping, elfRef *pfelf.Reference) elfInfo { - var lastModified int64 - - mappingFile := pr.GetMappingFile(mapping) - if mappingFile != "" { - var st unix.Stat_t - if err := unix.Stat(mappingFile, &st); err != nil { - return elfInfo{err: err} - } - lastModified = st.Mtim.Nano() - } - key := mapping.GetOnDiskFileIdentifier() - + lastModified := pr.GetMappingFileLastModified(mapping) if info, ok := pm.elfInfoCache.Get(key); ok && info.lastModified == lastModified { // Cached data ok pm.elfInfoCacheHit.Add(1) @@ -290,7 +278,7 @@ func (pm *ProcessManager) getELFInfo(pr process.Process, mapping *process.Mappin return info } - hostFileID := host.CalculateKernelFileID(fileID) + hostFileID := host.FileIDFromLibpf(fileID) info.fileID = hostFileID info.addressMapper = ef.GetAddressMapper() if mapping.IsVDSO() { @@ -359,15 +347,19 @@ func (pm *ProcessManager) processNewExecMapping(pr process.Process, mapping *pro Inode: mapping.Inode, FileOffset: mapping.FileOffset, }, elfRef); err != nil { - log.Errorf("Failed to handle mapping for PID %d, file %s: %v", - pr.PID(), mapping.Path, err) + // Same as above, ignore the errors related to process having exited. + // Also ignore errors of deferred file IDs. + if !errors.Is(err, os.ErrNotExist) && !errors.Is(err, eim.ErrDeferredFileID) { + log.Errorf("Failed to handle mapping for PID %d, file %s: %v", + pr.PID(), mapping.Path, err) + } } } // processRemovedMappings removes listed memory mappings and loaded interpreters from // the internal structures and eBPF maps. -func (pm *ProcessManager) processRemovedMappings(pid libpf.PID, mappings []libpf.Address, - interpretersValid libpf.Set[libpf.OnDiskFileIdentifier]) { +func (pm *ProcessManager) processRemovedMappings(pid util.PID, mappings []libpf.Address, + interpretersValid libpf.Set[util.OnDiskFileIdentifier]) { pm.mu.Lock() defer pm.mu.Unlock() @@ -423,7 +415,7 @@ func (pm *ProcessManager) synchronizeMappings(pr process.Process, mpAdd := make(map[libpf.Address]*process.Mapping, len(mappings)) mpRemove := make([]libpf.Address, 0) - interpretersValid := make(libpf.Set[libpf.OnDiskFileIdentifier]) + interpretersValid := make(libpf.Set[util.OnDiskFileIdentifier]) for idx := range mappings { m := &mappings[idx] if !m.IsExecutable() || m.IsAnonymous() { @@ -497,8 +489,8 @@ func (pm *ProcessManager) synchronizeMappings(pr process.Process, // is stored for later processing in SymbolizationComplete when all traces have been collected. // There can be a race condition if we can not clean up the references for this process // fast enough and this particular pid is reused again by the system. -// NOTE: Exported only for tracer/. -func (pm *ProcessManager) ProcessPIDExit(pid libpf.PID) bool { +// NOTE: Exported only for tracer. +func (pm *ProcessManager) ProcessPIDExit(pid util.PID) bool { log.Debugf("- PID: %v", pid) defer pm.ebpf.RemoveReportedPID(pid) @@ -506,7 +498,7 @@ func (pm *ProcessManager) ProcessPIDExit(pid libpf.PID) bool { defer pm.mu.Unlock() symbolize := false - exitKTime := libpf.GetKTime() + exitKTime := util.GetKTime() if pm.interpreterTracerEnabled { if len(pm.interpreters[pid]) > 0 { pm.exitEvents[pid] = exitKTime @@ -590,7 +582,7 @@ func (pm *ProcessManager) SynchronizeProcess(pr process.Process) { return } - libpf.AtomicUpdateMaxUint32(&pm.mappingStats.maxProcParseUsec, uint32(elapsed.Microseconds())) + util.AtomicUpdateMaxUint32(&pm.mappingStats.maxProcParseUsec, uint32(elapsed.Microseconds())) pm.mappingStats.totalProcParseUsec.Add(uint32(elapsed.Microseconds())) if pm.synchronizeMappings(pr, mappings) { @@ -611,9 +603,9 @@ func (pm *ProcessManager) SynchronizeProcess(pr process.Process) { } // CleanupPIDs executes a periodic synchronization of pidToProcessInfo table with system processes. -// NOTE: Exported only for tracer/. +// NOTE: Exported only for tracer. func (pm *ProcessManager) CleanupPIDs() { - deadPids := make([]libpf.PID, 0, 16) + deadPids := make([]util.PID, 0, 16) pm.mu.RLock() for pid := range pm.pidToProcessInfo { diff --git a/processmanager/synthdeltas_arm64.go b/processmanager/synthdeltas_arm64.go index 26416e01..056d29a1 100644 --- a/processmanager/synthdeltas_arm64.go +++ b/processmanager/synthdeltas_arm64.go @@ -12,8 +12,8 @@ import ( "fmt" "github.com/elastic/otel-profiling-agent/host" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" "github.com/elastic/otel-profiling-agent/libpf/pfelf" + sdtypes "github.com/elastic/otel-profiling-agent/nativeunwind/stackdeltatypes" ) // createVDSOSyntheticRecord creates a generated stack-delta record spanning the entire vDSO binary, @@ -31,8 +31,11 @@ func createVDSOSyntheticRecord(ef *pfelf.File) sdtypes.IntervalData { deltas = append(deltas, sdtypes.StackDelta{Address: 0, Info: useLR}) if sym, err := ef.LookupSymbol("__kernel_rt_sigreturn"); err == nil { addr := uint64(sym.Address) - deltas = append(deltas, sdtypes.StackDelta{Address: addr, Info: sdtypes.UnwindInfoSignal}) - deltas = append(deltas, sdtypes.StackDelta{Address: addr + uint64(sym.Size), Info: useLR}) + deltas = append( + deltas, + sdtypes.StackDelta{Address: addr, Info: sdtypes.UnwindInfoSignal}, + sdtypes.StackDelta{Address: addr + uint64(sym.Size), Info: useLR}, + ) } return sdtypes.IntervalData{Deltas: deltas} } diff --git a/processmanager/types.go b/processmanager/types.go index 5ba7420e..de51a5be 100644 --- a/processmanager/types.go +++ b/processmanager/types.go @@ -21,6 +21,7 @@ import ( eim "github.com/elastic/otel-profiling-agent/processmanager/execinfomanager" "github.com/elastic/otel-profiling-agent/reporter" "github.com/elastic/otel-profiling-agent/tpbase" + "github.com/elastic/otel-profiling-agent/util" ) // elfInfo contains cached data from an executable needed for processing mappings. @@ -49,14 +50,14 @@ type ProcessManager struct { // process exits, and various other situations needing interpreter specific attention. // The key of the first map is a process ID, while the key of the second map is // the unique on-disk identifier of the interpreter DSO. - interpreters map[libpf.PID]map[libpf.OnDiskFileIdentifier]interpreter.Instance + interpreters map[util.PID]map[util.OnDiskFileIdentifier]interpreter.Instance // pidToProcessInfo keeps track of the executable memory mappings in addressSpace // for each pid. - pidToProcessInfo map[libpf.PID]*processInfo + pidToProcessInfo map[util.PID]*processInfo // exitEvents records the pid exit time and is a list of pending exit events to be handled. - exitEvents map[libpf.PID]libpf.KTime + exitEvents map[util.PID]util.KTime // ebpf contains the interface to manipulate ebpf maps ebpf pmebpf.EbpfHandler @@ -84,7 +85,7 @@ type ProcessManager struct { // elfInfoCache provides a cache to quickly retrieve the ELF info and fileID for a particular // executable. It caches results based on iNode number and device ID. Locked LRU. - elfInfoCache *lru.LRU[libpf.OnDiskFileIdentifier, elfInfo] + elfInfoCache *lru.LRU[util.OnDiskFileIdentifier, elfInfo] // reporter is the interface to report symbolization information reporter reporter.SymbolReporter @@ -130,8 +131,8 @@ type Mapping struct { } // GetOnDiskFileIdentifier returns the OnDiskFileIdentifier for the mapping -func (m *Mapping) GetOnDiskFileIdentifier() libpf.OnDiskFileIdentifier { - return libpf.OnDiskFileIdentifier{ +func (m *Mapping) GetOnDiskFileIdentifier() util.OnDiskFileIdentifier { + return util.OnDiskFileIdentifier{ DeviceID: m.Device, InodeNum: m.Inode, } diff --git a/proto/.gitignore b/proto/.gitignore deleted file mode 100644 index a245b848..00000000 --- a/proto/.gitignore +++ /dev/null @@ -1 +0,0 @@ -experiments diff --git a/proto/buildpb.sh b/proto/buildpb.sh deleted file mode 100755 index 0806cffc..00000000 --- a/proto/buildpb.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash - -set -e - -OUTDIR="experiments" - -mkdir -p $OUTDIR - -# OTel profiles signal -protoc --proto_path=. \ - --go_out=$OUTDIR --go_opt=paths=source_relative \ - opentelemetry/proto/profiles/v1/alternatives/pprofextended/pprofextended.proto - -protoc --proto_path=. \ - --go_out=$OUTDIR --go_opt=paths=source_relative \ - opentelemetry/proto/profiles/v1/profiles.proto - -# Manually fix import paths -sed -i 's/go.opentelemetry.io\/proto\/otlp\/profiles\/v1\/alternatives\/pprofextended/github.com\/elastic\/otel-profiling-agent\/proto\/experiments\/opentelemetry\/proto\/profiles\/v1\/alternatives\/pprofextended/' experiments/opentelemetry/proto/profiles/v1/profiles.pb.go - -# OTel profiles service -protoc --proto_path=. \ - --go_out=$OUTDIR --go_opt=paths=source_relative \ - --go-grpc_out=$OUTDIR --go-grpc_opt=paths=source_relative \ - opentelemetry/proto/collector/profiles/v1/profiles_service.proto - -# Manually fix import paths -sed -i 's/go.opentelemetry.io\/proto\/otlp\/profiles\/v1/github.com\/elastic\/otel-profiling-agent\/proto\/experiments\/opentelemetry\/proto\/profiles\/v1/' experiments/opentelemetry/proto/collector/profiles/v1/profiles_service.pb.go diff --git a/proto/opentelemetry/proto/collector/profiles/v1/profiles_service.proto b/proto/opentelemetry/proto/collector/profiles/v1/profiles_service.proto deleted file mode 100644 index 1e0258e5..00000000 --- a/proto/opentelemetry/proto/collector/profiles/v1/profiles_service.proto +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2019, OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -// // This protofile is copied from -// https://github.com/open-telemetry/opentelemetry-proto-profile/blob/154f8715345b18bac436e4c55e014272cb0fd723/opentelemetry/proto/collector/profiles/v1 - -package opentelemetry.proto.collector.profiles.v1; - -import "opentelemetry/proto/profiles/v1/profiles.proto"; - -option csharp_namespace = "OpenTelemetry.Proto.Collector.Profiles.V1"; -option java_multiple_files = true; -option java_package = "io.opentelemetry.proto.collector.profiles.v1"; -option java_outer_classname = "ProfilesServiceProto"; -option go_package = "go.opentelemetry.io/proto/otlp/collector/profiles/v1"; - -// Service that can be used to push profiles between one Application instrumented with -// OpenTelemetry and a collector, or between a collector and a central collector (in this -// case spans are sent/received to/from multiple Applications). -service ProfilesService { - // For performance reasons, it is recommended to keep this RPC - // alive for the entire life of the application. - rpc Export(ExportProfilesServiceRequest) returns (ExportProfilesServiceResponse) {} -} - -message ExportProfilesServiceRequest { - // An array of ResourceProfiles. - // For data coming from a single resource this array will typically contain one - // element. Intermediary nodes (such as OpenTelemetry Collector) that receive - // data from multiple origins typically batch the data before forwarding further and - // in that case this array will contain multiple elements. - repeated opentelemetry.proto.profiles.v1.ResourceProfiles resource_profiles = 1; -} - -message ExportProfilesServiceResponse { - // The details of a partially successful export request. - // - // If the request is only partially accepted - // (i.e. when the server accepts only parts of the data and rejects the rest) - // the server MUST initialize the `partial_success` field and MUST - // set the `rejected_` with the number of items it rejected. - // - // Servers MAY also make use of the `partial_success` field to convey - // warnings/suggestions to senders even when the request was fully accepted. - // In such cases, the `rejected_` MUST have a value of `0` and - // the `error_message` MUST be non-empty. - // - // A `partial_success` message with an empty value (rejected_ = 0 and - // `error_message` = "") is equivalent to it not being set/present. Senders - // SHOULD interpret it the same way as in the full success case. - ExportProfilesPartialSuccess partial_success = 1; -} - -message ExportProfilesPartialSuccess { - // The number of rejected profiles. - // - // A `rejected_` field holding a `0` value indicates that the - // request was fully accepted. - int64 rejected_profiles = 1; - - // A developer-facing human-readable message in English. It should be used - // either to explain why the server rejected parts of the data during a partial - // success or to convey warnings/suggestions during a full success. The message - // should offer guidance on how users can address such issues. - // - // error_message is an optional field. An error_message with an empty value - // is equivalent to it not being set. - string error_message = 2; -} - diff --git a/proto/opentelemetry/proto/common/v1/common.proto b/proto/opentelemetry/proto/common/v1/common.proto deleted file mode 100644 index 42bf3a3f..00000000 --- a/proto/opentelemetry/proto/common/v1/common.proto +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2019, OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package opentelemetry.proto.common.v1; - -option csharp_namespace = "OpenTelemetry.Proto.Common.V1"; -option java_multiple_files = true; -option java_package = "io.opentelemetry.proto.common.v1"; -option java_outer_classname = "CommonProto"; -option go_package = "go.opentelemetry.io/proto/otlp/common/v1"; - -// AnyValue is used to represent any type of attribute value. AnyValue may contain a -// primitive value such as a string or integer or it may contain an arbitrary nested -// object containing arrays, key-value lists and primitives. -message AnyValue { - // The value is one of the listed fields. It is valid for all values to be unspecified - // in which case this AnyValue is considered to be "empty". - oneof value { - string string_value = 1; - bool bool_value = 2; - int64 int_value = 3; - double double_value = 4; - ArrayValue array_value = 5; - KeyValueList kvlist_value = 6; - bytes bytes_value = 7; - } -} - -// ArrayValue is a list of AnyValue messages. We need ArrayValue as a message -// since oneof in AnyValue does not allow repeated fields. -message ArrayValue { - // Array of values. The array may be empty (contain 0 elements). - repeated AnyValue values = 1; -} - -// KeyValueList is a list of KeyValue messages. We need KeyValueList as a message -// since `oneof` in AnyValue does not allow repeated fields. Everywhere else where we need -// a list of KeyValue messages (e.g. in Span) we use `repeated KeyValue` directly to -// avoid unnecessary extra wrapping (which slows down the protocol). The 2 approaches -// are semantically equivalent. -message KeyValueList { - // A collection of key/value pairs of key-value pairs. The list may be empty (may - // contain 0 elements). - // The keys MUST be unique (it is not allowed to have more than one - // value with the same key). - repeated KeyValue values = 1; -} - -// KeyValue is a key-value pair that is used to store Span attributes, Link -// attributes, etc. -message KeyValue { - string key = 1; - AnyValue value = 2; -} - -// InstrumentationScope is a message representing the instrumentation scope information -// such as the fully qualified name and version. -message InstrumentationScope { - // An empty instrumentation scope name means the name is unknown. - string name = 1; - string version = 2; - - // Additional attributes that describe the scope. [Optional]. - // Attribute keys MUST be unique (it is not allowed to have more than one - // attribute with the same key). - repeated KeyValue attributes = 3; - uint32 dropped_attributes_count = 4; -} - diff --git a/proto/opentelemetry/proto/profiles/v1/alternatives/pprofextended/pprofextended.proto b/proto/opentelemetry/proto/profiles/v1/alternatives/pprofextended/pprofextended.proto deleted file mode 100644 index 1d2b5710..00000000 --- a/proto/opentelemetry/proto/profiles/v1/alternatives/pprofextended/pprofextended.proto +++ /dev/null @@ -1,326 +0,0 @@ -// Profile is a common stacktrace profile format. -// -// Measurements represented with this format should follow the -// following conventions: -// -// - Consumers should treat unset optional fields as if they had been -// set with their default value. -// -// - When possible, measurements should be stored in "unsampled" form -// that is most useful to humans. There should be enough -// information present to determine the original sampled values. -// -// - On-disk, the serialized proto must be gzip-compressed. -// -// - The profile is represented as a set of samples, where each sample -// references a sequence of locations, and where each location belongs -// to a mapping. -// - There is a N->1 relationship from sample.location_id entries to -// locations. For every sample.location_id entry there must be a -// unique Location with that index. -// - There is an optional N->1 relationship from locations to -// mappings. For every nonzero Location.mapping_id there must be a -// unique Mapping with that index. -syntax = "proto3"; - -// This protofile is copied from https://github.com/open-telemetry/oteps/pull/239. - -package opentelemetry.proto.profiles.v1.alternatives.pprofextended; -import "opentelemetry/proto/common/v1/common.proto"; -option csharp_namespace = "OpenTelemetry.Proto.Profiles.V1.Alternatives.PprofExtended"; -option go_package = "go.opentelemetry.io/proto/otlp/profiles/v1/alternatives/pprofextended"; -// Represents a complete profile, including sample types, samples, -// mappings to binaries, locations, functions, string table, and additional metadata. -message Profile { - // A description of the samples associated with each Sample.value. - // For a cpu profile this might be: - // [["cpu","nanoseconds"]] or [["wall","seconds"]] or [["syscall","count"]] - // For a heap profile, this might be: - // [["allocations","count"], ["space","bytes"]], - // If one of the values represents the number of events represented - // by the sample, by convention it should be at index 0 and use - // sample_type.unit == "count". - repeated ValueType sample_type = 1; - // The set of samples recorded in this profile. - repeated Sample sample = 2; - // Mapping from address ranges to the image/binary/library mapped - // into that address range. mapping[0] will be the main binary. - repeated Mapping mapping = 3; - // Locations referenced by samples via location_indices. - repeated Location location = 4; - // Array of locations referenced by samples. - repeated int64 location_indices = 15; - // Functions referenced by locations. - repeated Function function = 5; - // Lookup table for attributes. - repeated opentelemetry.proto.common.v1.KeyValue attribute_table = 16; - // Represents a mapping between Attribute Keys and Units. - repeated AttributeUnit attribute_units = 17; - // Lookup table for links. - repeated Link link_table = 18; - // A common table for strings referenced by various messages. - // string_table[0] must always be "". - repeated string string_table = 6; - // frames with Function.function_name fully matching the following - // regexp will be dropped from the samples, along with their successors. - int64 drop_frames = 7; // Index into string table. - // frames with Function.function_name fully matching the following - // regexp will be kept, even if it matches drop_frames. - int64 keep_frames = 8; // Index into string table. - // The following fields are informational, do not affect - // interpretation of results. - // Time of collection (UTC) represented as nanoseconds past the epoch. - int64 time_nanos = 9; - // Duration of the profile, if a duration makes sense. - int64 duration_nanos = 10; - // The kind of events between sampled occurrences. - // e.g [ "cpu","cycles" ] or [ "heap","bytes" ] - ValueType period_type = 11; - // The number of events between sampled occurrences. - int64 period = 12; - // Free-form text associated with the profile. The text is displayed as is - // to the user by the tools that read profiles (e.g. by pprof). This field - // should not be used to store any machine-readable information, it is only - // for human-friendly content. The profile must stay functional if this field - // is cleaned. - repeated int64 comment = 13; // Indices into string table. - // Index into the string table of the type of the preferred sample - // value. If unset, clients should default to the last sample value. - int64 default_sample_type = 14; -} -// Represents a mapping between Attribute Keys and Units. -message AttributeUnit { - // Index into string table. - int64 attribute_key = 1; - // Index into string table. - int64 unit = 2; -} -// A pointer from a profile Sample to a trace Span. -// Connects a profile sample to a trace span, identified by unique trace and span IDs. -message Link { - // A unique identifier of a trace that this linked span is part of. The ID is a - // 16-byte array. - bytes trace_id = 1; - // A unique identifier for the linked span. The ID is an 8-byte array. - bytes span_id = 2; -} -// Specifies the method of aggregating metric values, either DELTA (change since last report) -// or CUMULATIVE (total since a fixed start time). -enum AggregationTemporality { - /* UNSPECIFIED is the default AggregationTemporality, it MUST not be used. */ - AGGREGATION_TEMPORALITY_UNSPECIFIED = 0; - /** DELTA is an AggregationTemporality for a profiler which reports - changes since last report time. Successive metrics contain aggregation of - values from continuous and non-overlapping intervals. - The values for a DELTA metric are based only on the time interval - associated with one measurement cycle. There is no dependency on - previous measurements like is the case for CUMULATIVE metrics. - For example, consider a system measuring the number of requests that - it receives and reports the sum of these requests every second as a - DELTA metric: - 1. The system starts receiving at time=t_0. - 2. A request is received, the system measures 1 request. - 3. A request is received, the system measures 1 request. - 4. A request is received, the system measures 1 request. - 5. The 1 second collection cycle ends. A metric is exported for the - number of requests received over the interval of time t_0 to - t_0+1 with a value of 3. - 6. A request is received, the system measures 1 request. - 7. A request is received, the system measures 1 request. - 8. The 1 second collection cycle ends. A metric is exported for the - number of requests received over the interval of time t_0+1 to - t_0+2 with a value of 2. */ - AGGREGATION_TEMPORALITY_DELTA = 1; - /** CUMULATIVE is an AggregationTemporality for a profiler which - reports changes since a fixed start time. This means that current values - of a CUMULATIVE metric depend on all previous measurements since the - start time. Because of this, the sender is required to retain this state - in some form. If this state is lost or invalidated, the CUMULATIVE metric - values MUST be reset and a new fixed start time following the last - reported measurement time sent MUST be used. - For example, consider a system measuring the number of requests that - it receives and reports the sum of these requests every second as a - CUMULATIVE metric: - 1. The system starts receiving at time=t_0. - 2. A request is received, the system measures 1 request. - 3. A request is received, the system measures 1 request. - 4. A request is received, the system measures 1 request. - 5. The 1 second collection cycle ends. A metric is exported for the - number of requests received over the interval of time t_0 to - t_0+1 with a value of 3. - 6. A request is received, the system measures 1 request. - 7. A request is received, the system measures 1 request. - 8. The 1 second collection cycle ends. A metric is exported for the - number of requests received over the interval of time t_0 to - t_0+2 with a value of 5. - 9. The system experiences a fault and loses state. - 10. The system recovers and resumes receiving at time=t_1. - 11. A request is received, the system measures 1 request. - 12. The 1 second collection cycle ends. A metric is exported for the - number of requests received over the interval of time t_1 to - t_0+1 with a value of 1. - Note: Even though, when reporting changes since last report time, using - CUMULATIVE is valid, it is not recommended. */ - AGGREGATION_TEMPORALITY_CUMULATIVE = 2; -} -// ValueType describes the type and units of a value, with an optional aggregation temporality. -message ValueType { - int64 type = 1; // Index into string table. - int64 unit = 2; // Index into string table. - AggregationTemporality aggregation_temporality = 3; -} -// Each Sample records values encountered in some program -// context. The program context is typically a stack trace, perhaps -// augmented with auxiliary information like the thread-id, some -// indicator of a higher level request being handled etc. -message Sample { - // The indices recorded here correspond to locations in Profile.location. - // The leaf is at location_index[0]. [deprecated, superseded by locations_start_index / locations_length] - repeated uint64 location_index = 1; - // locations_start_index along with locations_length refers to to a slice of locations in Profile.location. - // Supersedes location_index. - uint64 locations_start_index = 7; - // locations_length along with locations_start_index refers to a slice of locations in Profile.location. - // Supersedes location_index. - uint64 locations_length = 8; - // A 128bit id that uniquely identifies this stacktrace, globally. Index into string table. [optional] - uint32 stacktrace_id_index = 9; - // The type and unit of each value is defined by the corresponding - // entry in Profile.sample_type. All samples must have the same - // number of values, the same as the length of Profile.sample_type. - // When aggregating multiple samples into a single sample, the - // result has a list of values that is the element-wise sum of the - // lists of the originals. - repeated int64 value = 2; - // label includes additional context for this sample. It can include - // things like a thread id, allocation size, etc. - // - // NOTE: While possible, having multiple values for the same label key is - // strongly discouraged and should never be used. Most tools (e.g. pprof) do - // not have good (or any) support for multi-value labels. And an even more - // discouraged case is having a string label and a numeric label of the same - // name on a sample. Again, possible to express, but should not be used. - // [deprecated, superseded by attributes] - repeated Label label = 3; - // References to attributes in Profile.attribute_table. [optional] - repeated uint64 attributes = 10; - // Reference to link in Profile.link_table. [optional] - uint64 link = 12; - // Timestamps associated with Sample represented in ms. These timestamps are expected - // to fall within the Profile's time range. [optional] - repeated uint64 timestamps = 13; -} -// Provides additional context for a sample, -// such as thread ID or allocation size, with optional units. [deprecated] -message Label { - int64 key = 1; // Index into string table - // At most one of the following must be present - int64 str = 2; // Index into string table - int64 num = 3; - // Should only be present when num is present. - // Specifies the units of num. - // Use arbitrary string (for example, "requests") as a custom count unit. - // If no unit is specified, consumer may apply heuristic to deduce the unit. - // Consumers may also interpret units like "bytes" and "kilobytes" as memory - // units and units like "seconds" and "nanoseconds" as time units, - // and apply appropriate unit conversions to these. - int64 num_unit = 4; // Index into string table -} -// Indicates the semantics of the build_id field. -enum BuildIdKind { - // Linker-generated build ID, stored in the ELF binary notes. - BUILD_ID_LINKER = 0; - // Build ID based on the content hash of the binary. Currently no particular - // hashing approach is standardized, so a given producer needs to define it - // themselves and thus unlike BUILD_ID_LINKER this kind of hash is producer-specific. - // We may choose to provide a standardized stable hash recommendation later. - BUILD_ID_BINARY_HASH = 1; -} -// Describes the mapping of a binary in memory, including its address range, -// file offset, and metadata like build ID -message Mapping { - // Unique nonzero id for the mapping. [deprecated] - uint64 id = 1; - // Address at which the binary (or DLL) is loaded into memory. - uint64 memory_start = 2; - // The limit of the address range occupied by this mapping. - uint64 memory_limit = 3; - // Offset in the binary that corresponds to the first mapped address. - uint64 file_offset = 4; - // The object this entry is loaded from. This can be a filename on - // disk for the main binary and shared libraries, or virtual - // abstractions like "[vdso]". - int64 filename = 5; // Index into string table - // A string that uniquely identifies a particular program version - // with high probability. E.g., for binaries generated by GNU tools, - // it could be the contents of the .note.gnu.build-id field. - int64 build_id = 6; // Index into string table - // Specifies the kind of build id. See BuildIdKind enum for more details [optional] - BuildIdKind build_id_kind = 11; - // References to attributes in Profile.attribute_table. [optional] - repeated uint64 attributes = 12; - // The following fields indicate the resolution of symbolic info. - bool has_functions = 7; - bool has_filenames = 8; - bool has_line_numbers = 9; - bool has_inline_frames = 10; -} -// Describes function and line table debug information. -message Location { - // Unique nonzero id for the location. A profile could use - // instruction addresses or any integer sequence as ids. [deprecated] - uint64 id = 1; - // The index of the corresponding profile.Mapping for this location. - // It can be unset if the mapping is unknown or not applicable for - // this profile type. - uint64 mapping_index = 2; - // The instruction address for this location, if available. It - // should be within [Mapping.memory_start...Mapping.memory_limit] - // for the corresponding mapping. A non-leaf address may be in the - // middle of a call instruction. It is up to display tools to find - // the beginning of the instruction if necessary. - uint64 address = 3; - // Multiple line indicates this location has inlined functions, - // where the last entry represents the caller into which the - // preceding entries were inlined. - // - // E.g., if memcpy() is inlined into printf: - // line[0].function_name == "memcpy" - // line[1].function_name == "printf" - repeated Line line = 4; - // Provides an indication that multiple symbols map to this location's - // address, for example due to identical code folding by the linker. In that - // case the line information above represents one of the multiple - // symbols. This field must be recomputed when the symbolization state of the - // profile changes. - bool is_folded = 5; - // Type of frame (e.g. kernel, native, python, hotspot, php). Index into string table. - uint32 type_index = 6; - // References to attributes in Profile.attribute_table. [optional] - repeated uint64 attributes = 7; -} -// Details a specific line in a source code, linked to a function. -message Line { - // The index of the corresponding profile.Function for this line. - uint64 function_index = 1; - // Line number in source code. - int64 line = 2; - // Column number in source code. - int64 column = 3; -} -// Describes a function, including its human-readable name, system name, -// source file, and starting line number in the source. -message Function { - // Unique nonzero id for the function. [deprecated] - uint64 id = 1; - // Name of the function, in human-readable form if available. - int64 name = 2; // Index into string table - // Name of the function, as identified by the system. - // For instance, it can be a C++ mangled name. - int64 system_name = 3; // Index into string table - // Source file containing the function. - int64 filename = 4; // Index into string table - // Line number in source file. - int64 start_line = 5; -} - diff --git a/proto/opentelemetry/proto/profiles/v1/profiles.proto b/proto/opentelemetry/proto/profiles/v1/profiles.proto deleted file mode 100644 index d73699ae..00000000 --- a/proto/opentelemetry/proto/profiles/v1/profiles.proto +++ /dev/null @@ -1,153 +0,0 @@ -syntax = "proto3"; - -// This protofile is copied from https://github.com/open-telemetry/oteps/pull/239. - -package opentelemetry.proto.profiles.v1; -import "opentelemetry/proto/common/v1/common.proto"; -import "opentelemetry/proto/resource/v1/resource.proto"; -import "opentelemetry/proto/profiles/v1/alternatives/pprofextended/pprofextended.proto"; -option csharp_namespace = "OpenTelemetry.Proto.Profiles.V1"; -option java_multiple_files = true; -option java_package = "io.opentelemetry.proto.profiles.v1"; -option java_outer_classname = "ProfilesProto"; -option go_package = "go.opentelemetry.io/proto/otlp/profiles/v1"; -// Relationships Diagram -// -// ┌──────────────────┐ LEGEND -// │ ProfilesData │ -// └──────────────────┘ ─────▶ embedded -// │ -// │ 1-n ─────▷ referenced by index -// ▼ -// ┌──────────────────┐ -// │ ResourceProfiles │ -// └──────────────────┘ -// │ -// │ 1-n -// ▼ -// ┌──────────────────┐ -// │ ScopeProfiles │ -// └──────────────────┘ -// │ -// │ 1-n -// ▼ -// ┌──────────────────┐ -// │ ProfileContainer │ -// └──────────────────┘ -// │ -// │ 1-1 -// ▼ -// ┌──────────────────┐ -// │ Profile │ -// └──────────────────┘ -// │ 1-n -// │ 1-n ┌───────────────────────────────────────┐ -// ▼ │ ▽ -// ┌──────────────────┐ 1-n ┌──────────────┐ ┌──────────┐ -// │ Sample │ ──────▷ │ KeyValue │ │ Link │ -// └──────────────────┘ └──────────────┘ └──────────┘ -// │ 1-n △ △ -// │ 1-n ┌─────────────────┘ │ 1-n -// ▽ │ │ -// ┌──────────────────┐ n-1 ┌──────────────┐ -// │ Location │ ──────▷ │ Mapping │ -// └──────────────────┘ └──────────────┘ -// │ -// │ 1-n -// ▼ -// ┌──────────────────┐ -// │ Line │ -// └──────────────────┘ -// │ -// │ 1-1 -// ▽ -// ┌──────────────────┐ -// │ Function │ -// └──────────────────┘ -// -// ProfilesData represents the profiles data that can be stored in persistent storage, -// OR can be embedded by other protocols that transfer OTLP profiles data but do not -// implement the OTLP protocol. -// -// The main difference between this message and collector protocol is that -// in this message there will not be any "control" or "metadata" specific to -// OTLP protocol. -// -// When new fields are added into this message, the OTLP request MUST be updated -// as well. -message ProfilesData { - // An array of ResourceProfiles. - // For data coming from a single resource this array will typically contain - // one element. Intermediary nodes that receive data from multiple origins - // typically batch the data before forwarding further and in that case this - // array will contain multiple elements. - repeated ResourceProfiles resource_profiles = 1; -} -// A collection of ScopeProfiles from a Resource. -message ResourceProfiles { - reserved 1000; - // The resource for the profiles in this message. - // If this field is not set then no resource info is known. - opentelemetry.proto.resource.v1.Resource resource = 1; - // A list of ScopeProfiles that originate from a resource. - repeated ScopeProfiles scope_profiles = 2; - // This schema_url applies to the data in the "resource" field. It does not apply - // to the data in the "scope_profiles" field which have their own schema_url field. - string schema_url = 3; -} -// A collection of Profiles produced by an InstrumentationScope. -message ScopeProfiles { - // The instrumentation scope information for the profiles in this message. - // Semantically when InstrumentationScope isn't set, it is equivalent with - // an empty instrumentation scope name (unknown). - opentelemetry.proto.common.v1.InstrumentationScope scope = 1; - // A list of ProfileContainers that originate from an instrumentation scope. - repeated ProfileContainer profiles = 2; - // This schema_url applies to all profiles and profile events in the "profiles" field. - string schema_url = 3; -} -// A ProfileContainer represents a single profile. It wraps pprof profile with OpenTelemetry specific metadata. -message ProfileContainer { - // A unique identifier for a profile. The ID is a 16-byte array. An ID with - // all zeroes is considered invalid. - // - // This field is required. - bytes profile_id = 1; - // start_time_unix_nano is the start time of the profile. - // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. - // - // This field is semantically required and it is expected that end_time >= start_time. - fixed64 start_time_unix_nano = 2; - // end_time_unix_nano is the end time of the profile. - // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. - // - // This field is semantically required and it is expected that end_time >= start_time. - fixed64 end_time_unix_nano = 3; - // attributes is a collection of key/value pairs. Note, global attributes - // like server name can be set using the resource API. Examples of attributes: - // - // "/http/user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" - // "/http/server_latency": 300 - // "abc.com/myattribute": true - // "abc.com/score": 10.239 - // - // The OpenTelemetry API specification further restricts the allowed value types: - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/README.md#attribute - // Attribute keys MUST be unique (it is not allowed to have more than one - // attribute with the same key). - repeated opentelemetry.proto.common.v1.KeyValue attributes = 4; - // dropped_attributes_count is the number of attributes that were discarded. Attributes - // can be discarded because their keys are too long or because there are too many - // attributes. If this value is 0, then no attributes were dropped. - uint32 dropped_attributes_count = 5; - // Specifies format of the original payload. Common values are defined in semantic conventions. [required if original_payload is present] - string original_payload_format = 6; - // Original payload can be stored in this field. This can be useful for users who want to get the original payload. - // Formats such as JFR are highly extensible and can contain more information than what is defined in this spec. - // Inclusion of original payload should be configurable by the user. Default behavior should be to not include the original payload. - // If the original payload is in pprof format, it SHOULD not be included in this field. - // The field is optional, however if it is present `profile` MUST be present and contain the same profiling information. - bytes original_payload = 7; - // This is a reference to a pprof profile. Required, even when original_payload is present. - opentelemetry.proto.profiles.v1.alternatives.pprofextended.Profile profile = 8; -} diff --git a/proto/opentelemetry/proto/resource/v1/resource.proto b/proto/opentelemetry/proto/resource/v1/resource.proto deleted file mode 100644 index bc397dde..00000000 --- a/proto/opentelemetry/proto/resource/v1/resource.proto +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2019, OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package opentelemetry.proto.resource.v1; - -import "opentelemetry/proto/common/v1/common.proto"; - -option csharp_namespace = "OpenTelemetry.Proto.Resource.V1"; -option java_multiple_files = true; -option java_package = "io.opentelemetry.proto.resource.v1"; -option java_outer_classname = "ResourceProto"; -option go_package = "go.opentelemetry.io/proto/otlp/resource/v1"; - -// Resource information. -message Resource { - // Set of attributes that describe the resource. - // Attribute keys MUST be unique (it is not allowed to have more than one - // attribute with the same key). - repeated opentelemetry.proto.common.v1.KeyValue attributes = 1; - - // dropped_attributes_count is the number of dropped attributes. If the value is 0, then - // no attributes were dropped. - uint32 dropped_attributes_count = 2; -} - diff --git a/libpf/remotememory/remotememory.go b/remotememory/remotememory.go similarity index 71% rename from libpf/remotememory/remotememory.go rename to remotememory/remotememory.go index 4d7af6f4..39ba2aec 100644 --- a/libpf/remotememory/remotememory.go +++ b/remotememory/remotememory.go @@ -18,6 +18,7 @@ import ( "golang.org/x/sys/unix" "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/util" ) // RemoteMemory implements a set of convenience functions to access the remote memory @@ -125,59 +126,10 @@ func (rm RemoteMemory) StringPtr(addr libpf.Address) string { return rm.String(addr) } -// RecordingReader allows reading data from the remote process using io.ReadByte interface. -// It provides basic buffering by reading memory in pieces of 'chunk' bytes and it also -// records all read memory in a backing buffer to be later stored as a whole. -type RecordingReader struct { - // rm is the RemoteMemory from which we are reading the data from - rm *RemoteMemory - // buf contains all data read from the target process - buf []byte - // addr is the target virtual address to continue reading from - addr libpf.Address - // i is the index to the buf[] byte which is to be returned next in ReadByte() - i int - // chunk is the number of bytes to read from target process when mora data is needed - chunk int -} - -// ReadByte implements io.ByteReader interface to read memory single byte at a time. -func (rr *RecordingReader) ReadByte() (byte, error) { - // Readahead to buffer if needed - if rr.i >= len(rr.buf) { - buf := make([]byte, len(rr.buf)+rr.chunk) - copy(buf, rr.buf) - err := rr.rm.Read(rr.addr, buf[len(rr.buf):]) - if err != nil { - return 0, err - } - rr.addr += libpf.Address(rr.chunk) - rr.buf = buf - } - // Return byte from buffer - b := rr.buf[rr.i] - rr.i++ - return b, nil -} - -// GetBuffer returns all the data so far as a single slice. -func (rr *RecordingReader) GetBuffer() []byte { - return rr.buf[0:rr.i] -} - -// Reader returns a RecordingReader to read and record data from given start. -func (rm RemoteMemory) Reader(addr libpf.Address, chunkSize uint) *RecordingReader { - return &RecordingReader{ - rm: &rm, - addr: addr, - chunk: int(chunkSize), - } -} - // ProcessVirtualMemory implements RemoteMemory by using process_vm_readv syscalls // to read the remote memory. type ProcessVirtualMemory struct { - pid libpf.PID + pid util.PID } func (vm ProcessVirtualMemory) ReadAt(p []byte, off int64) (int, error) { @@ -198,6 +150,6 @@ func (vm ProcessVirtualMemory) ReadAt(p []byte, off int64) (int, error) { } // NewRemoteMemory returns ProcessVirtualMemory implementation of RemoteMemory. -func NewProcessVirtualMemory(pid libpf.PID) RemoteMemory { +func NewProcessVirtualMemory(pid util.PID) RemoteMemory { return RemoteMemory{ReaderAt: ProcessVirtualMemory{pid}} } diff --git a/libpf/remotememory/remotememory_test.go b/remotememory/remotememory_test.go similarity index 50% rename from libpf/remotememory/remotememory_test.go rename to remotememory/remotememory_test.go index f1bf5c36..a1ea0e8b 100644 --- a/libpf/remotememory/remotememory_test.go +++ b/remotememory/remotememory_test.go @@ -10,21 +10,16 @@ import ( "bytes" "errors" "os" - "reflect" "syscall" "testing" "unsafe" "github.com/elastic/otel-profiling-agent/libpf" -) + "github.com/elastic/otel-profiling-agent/util" -func assertEqual(t *testing.T, a, b any) { - if a == b { - return - } - t.Errorf("Received %v (type %v), expected %v (type %v)", - a, reflect.TypeOf(a), b, reflect.TypeOf(b)) -} + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) func RemoteMemTests(t *testing.T, rm RemoteMemory) { data := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} @@ -36,30 +31,16 @@ func RemoteMemTests(t *testing.T, rm RemoteMemory) { foo := make([]byte, len(data)) err := rm.Read(libpf.Address(uintptr(unsafe.Pointer(&data))), foo) - if err != nil { - if errors.Is(err, syscall.ENOSYS) { - t.Skipf("skipping due to error: %v", err) - } - t.Fatalf("%v", err) - } - - assertEqual(t, rm.Uint32(dataPtr), uint32(0x04030201)) - assertEqual(t, rm.Ptr(dataPtr), libpf.Address(0x0807060504030201)) - assertEqual(t, rm.String(strPtr), string(str[:len(str)-1])) - assertEqual(t, rm.String(longStrPtr), string(longStr[:len(longStr)-1])) - - rr := rm.Reader(dataPtr, 2) - for i := 0; i < len(data)-1; i++ { - if b, err := rr.ReadByte(); err == nil { - assertEqual(t, b, data[i]) - } else { - t.Errorf("recordingreader error: %v", err) - break - } + if errors.Is(err, syscall.ENOSYS) { + t.Skipf("skipping due to error: %v", err) } - assertEqual(t, len(rr.GetBuffer()), len(data)-1) + require.NoError(t, err) + assert.Equal(t, uint32(0x04030201), rm.Uint32(dataPtr)) + assert.Equal(t, libpf.Address(0x0807060504030201), rm.Ptr(dataPtr)) + assert.Equal(t, string(str[:len(str)-1]), rm.String(strPtr)) + assert.Equal(t, string(longStr[:len(longStr)-1]), rm.String(longStrPtr)) } func TestProcessVirtualMemory(t *testing.T) { - RemoteMemTests(t, NewProcessVirtualMemory(libpf.PID(os.Getpid()))) + RemoteMemTests(t, NewProcessVirtualMemory(util.PID(os.Getpid()))) } diff --git a/reporter/config.go b/reporter/config.go new file mode 100644 index 00000000..98bfaeca --- /dev/null +++ b/reporter/config.go @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package reporter + +import "google.golang.org/grpc" + +type Config struct { + // Name defines the name of the agent. + Name string + + // Version defines the vesion of the agent. + Version string + + // CollAgentAddr defines the destination of the backend connection. + CollAgentAddr string + + // MaxRPCMsgSize defines the maximum size of a gRPC message. + MaxRPCMsgSize int + + // ExecMetadataMaxQueue defines the maximum size for the queue which holds + // data of type collectionagent.ExecutableMetadata. + ExecMetadataMaxQueue uint32 + // CountsForTracesMaxQueue defines the maximum size for the queue which holds + // data of type libpf.TraceAndCounts. + CountsForTracesMaxQueue uint32 + // MetricsMaxQueue defines the maximum size for the queue which holds + // data of type collectionagent.Metric. + MetricsMaxQueue uint32 + // FramesForTracesMaxQueue defines the maximum size for the queue which holds + // data of type libpf.Trace. + FramesForTracesMaxQueue uint32 + // FrameMetadataMaxQueue defines the maximum size for the queue which holds + // data of type collectionagent.FrameMetadata. + FrameMetadataMaxQueue uint32 + // HostMetadataMaxQueue defines the maximum size for the queue which holds + // data of type collectionagent.HostMetadata. + HostMetadataMaxQueue uint32 + // FallbackSymbolsMaxQueue defines the maximum size for the queue which holds + // data of type collectionagent.FallbackSymbol. + FallbackSymbolsMaxQueue uint32 + // Disable secure communication with Collection Agent. + DisableTLS bool + // Number of connection attempts to the collector after which we give up retrying. + MaxGRPCRetries uint32 + + Times Times + + // gRPCInterceptor is the client gRPC interceptor, e.g., for sending gRPC metadata. + GRPCClientInterceptor grpc.UnaryClientInterceptor +} diff --git a/reporter/fifo.go b/reporter/fifo.go index 60d7cdc6..24b38e40 100644 --- a/reporter/fifo.go +++ b/reporter/fifo.go @@ -13,8 +13,8 @@ import ( log "github.com/sirupsen/logrus" ) -// fifoRingBuffer implements a first-in-first-out ring buffer that is safe for concurrent access. -type fifoRingBuffer[T any] struct { +// FifoRingBuffer implements a first-in-first-out ring buffer that is safe for concurrent access. +type FifoRingBuffer[T any] struct { // nolint:gocritic sync.Mutex // data holds the actual data. @@ -44,7 +44,7 @@ type fifoRingBuffer[T any] struct { overwriteCount uint32 } -func (q *fifoRingBuffer[T]) initFifo(size uint32, name string) error { +func (q *FifoRingBuffer[T]) InitFifo(size uint32, name string) error { if size == 0 { return fmt.Errorf("unsupported size of fifo: %d", size) } @@ -62,16 +62,16 @@ func (q *fifoRingBuffer[T]) initFifo(size uint32, name string) error { // zeroFifo re-initializes the ring buffer and clears the data array, making previously // stored elements available for GC. -func (q *fifoRingBuffer[T]) zeroFifo() { - if err := q.initFifo(q.size, q.name); err != nil { +func (q *FifoRingBuffer[T]) zeroFifo() { + if err := q.InitFifo(q.size, q.name); err != nil { // Should never happen panic(err) } } -// append adds element v to the fifoRingBuffer. it overwrites existing elements if there is no +// Append adds element v to the FifoRingBuffer. it overwrites existing elements if there is no // space left. -func (q *fifoRingBuffer[T]) append(v T) { +func (q *FifoRingBuffer[T]) Append(v T) { q.Lock() defer q.Unlock() @@ -94,8 +94,8 @@ func (q *fifoRingBuffer[T]) append(v T) { } } -// readAll returns all elements from the fifoRingBuffer. -func (q *fifoRingBuffer[T]) readAll() []T { +// ReadAll returns all elements from the FifoRingBuffer. +func (q *FifoRingBuffer[T]) ReadAll() []T { q.Lock() defer q.Unlock() @@ -115,7 +115,7 @@ func (q *fifoRingBuffer[T]) readAll() []T { return data } -func (q *fifoRingBuffer[T]) getOverwriteCount() uint32 { +func (q *FifoRingBuffer[T]) GetOverwriteCount() uint32 { q.Lock() defer q.Unlock() diff --git a/reporter/fifo_test.go b/reporter/fifo_test.go index b9f560e9..65d078f2 100644 --- a/reporter/fifo_test.go +++ b/reporter/fifo_test.go @@ -9,8 +9,8 @@ package reporter import ( "testing" - "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFifo(t *testing.T) { @@ -26,10 +26,9 @@ func TestFifo(t *testing.T) { var retIntegersShared []int retIntegersShared = append(retIntegersShared, 8, 9, 10, 11, 12) - sharedFifo := &fifoRingBuffer[int]{} - if err := sharedFifo.initFifo(5, t.Name()); err != nil { - t.Fatalf("unexpected error: %v", err) - } + sharedFifo := &FifoRingBuffer[int]{} + err := sharedFifo.InitFifo(5, t.Name()) + require.NoError(t, err) // nolint:lll tests := map[string]struct { @@ -75,7 +74,7 @@ func TestFifo(t *testing.T) { for name, testcase := range tests { name := name testcase := testcase - var fifo *fifoRingBuffer[int] + var fifo *FifoRingBuffer[int] t.Run(name, func(t *testing.T) { if testcase.parallel { @@ -85,57 +84,38 @@ func TestFifo(t *testing.T) { if testcase.sharedFifo { fifo = sharedFifo } else { - fifo = &fifoRingBuffer[int]{} - if err := fifo.initFifo(testcase.size, t.Name()); err != nil { - if testcase.err { - // We expected an error and received it. - // So we can continue. - return - } - t.Fatalf("unexpected error: %v", err) + fifo = &FifoRingBuffer[int]{} + err := fifo.InitFifo(testcase.size, t.Name()) + if testcase.err { + require.Error(t, err) + return } + require.NoError(t, err) } - empty := fifo.readAll() - if len(empty) != 0 { - t.Fatalf("Nothing was added to fifo but fifo returned %d elements", len(empty)) - } + empty := fifo.ReadAll() + require.Empty(t, empty) for _, v := range testcase.data { - fifo.append(v) + fifo.Append(v) } - data := fifo.readAll() + data := fifo.ReadAll() for i := uint32(0); i < fifo.size; i++ { - if fifo.data[i] != 0 { - t.Errorf("fifo not empty after readAll(), idx: %d", i) - } - } - - if diff := cmp.Diff(testcase.returned, data); diff != "" { - t.Errorf("returned data (%d) mismatch (-want +got):\n%s", len(data), diff) - } - - overwriteCount := fifo.getOverwriteCount() - if overwriteCount != testcase.overwriteCount { - t.Fatalf("expected an overwrite count %d but got %d", testcase.overwriteCount, - overwriteCount) - } - overwriteCount = fifo.getOverwriteCount() - if overwriteCount != 0 { - t.Fatalf( - "after retrieving the overwriteCount, it should be reset to 0 but got %d", - overwriteCount) + assert.Equalf(t, 0, fifo.data[i], "fifo not empty after ReadAll(), idx: %d", i) } + assert.Equal(t, testcase.returned, data) + assert.Equal(t, testcase.overwriteCount, fifo.GetOverwriteCount(), "overwrite count") + assert.Zero(t, fifo.GetOverwriteCount(), "overwrite count not reset") }) } } func TestFifo_isWritableWhenZeroed(t *testing.T) { - fifo := &fifoRingBuffer[int]{} - assert.Nil(t, fifo.initFifo(1, t.Name())) + fifo := &FifoRingBuffer[int]{} + require.NoError(t, fifo.InitFifo(1, t.Name())) fifo.zeroFifo() assert.NotPanics(t, func() { - fifo.append(123) + fifo.Append(123) }) } diff --git a/reporter/helper.go b/reporter/helper.go deleted file mode 100644 index 53a589f4..00000000 --- a/reporter/helper.go +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -package reporter - -import ( - "context" - "crypto/tls" - "os" - "time" - - "github.com/elastic/otel-profiling-agent/libpf" - log "github.com/sirupsen/logrus" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/status" -) - -// setupGrpcConnection sets up a gRPC connection instrumented with our auth interceptor -func setupGrpcConnection(parent context.Context, c *Config, - statsHandler *statsHandlerImpl) (*grpc.ClientConn, error) { - // authGrpcInterceptor intercepts gRPC operations, adds metadata to each operation and - // checks for authentication errors. If an authentication error is encountered, a - // process exit is triggered. - authGrpcInterceptor := func(ctx context.Context, method string, req, reply any, - cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { - err := invoker(ctx, method, req, reply, cc, opts...) - if err != nil { - if st, ok := status.FromError(err); ok { - code := st.Code() - if code == codes.Unauthenticated || - code == codes.FailedPrecondition { - log.Errorf("Setup gRPC: %v", err) - //nolint:errcheck - libpf.SleepWithJitterAndContext(parent, - c.Times.GRPCAuthErrorDelay(), 0.3) - os.Exit(1) - } - } - } - return err - } - - opts := []grpc.DialOption{grpc.WithBlock(), - grpc.WithStatsHandler(statsHandler), - grpc.WithUnaryInterceptor(authGrpcInterceptor), - grpc.WithDefaultCallOptions( - grpc.MaxCallRecvMsgSize(c.MaxRPCMsgSize), - grpc.MaxCallSendMsgSize(c.MaxRPCMsgSize)), - grpc.WithReturnConnectionError(), - } - - if c.DisableTLS { - opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) - } else { - opts = append(opts, - grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ - // Support only TLS1.3+ with valid CA certificates - MinVersion: tls.VersionTLS13, - InsecureSkipVerify: false, - }))) - } - - ctx, cancel := context.WithTimeout(parent, c.Times.GRPCConnectionTimeout()) - defer cancel() - return grpc.DialContext(ctx, c.CollAgentAddr, opts...) -} - -// When we are not able to connect immediately to the backend, -// we will wait forever until a connection happens and we receive a response, -// or the operation is canceled. -func waitGrpcEndpoint(ctx context.Context, c *Config, - statsHandler *statsHandlerImpl) (*grpc.ClientConn, error) { - // Sleep with a fixed backoff time added of +/- 20% jitter - tick := time.NewTicker(libpf.AddJitter(c.Times.GRPCStartupBackoffTime(), 0.2)) - defer tick.Stop() - - var retries uint32 - for { - if collAgentConn, err := setupGrpcConnection(ctx, c, statsHandler); err != nil { - if retries >= c.MaxGRPCRetries { - return nil, err - } - retries++ - - log.Warnf( - "Failed to setup gRPC connection (try %d of %d): %v", - retries, - c.MaxGRPCRetries, - err, - ) - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-tick.C: - continue - } - } else { - return collAgentConn, nil - } - } -} diff --git a/reporter/iface.go b/reporter/iface.go index 18281201..e78a4e28 100644 --- a/reporter/iface.go +++ b/reporter/iface.go @@ -10,23 +10,10 @@ import ( "context" "time" - "github.com/elastic/otel-profiling-agent/config" "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/util" ) -// Compile time check to make sure config.Times satisfies the interfaces. -var _ Times = (*config.Times)(nil) - -// Times is a subset of config.IntervalsAndTimers. -type Times interface { - ReportInterval() time.Duration - ReportMetricsInterval() time.Duration - GRPCConnectionTimeout() time.Duration - GRPCOperationTimeout() time.Duration - GRPCStartupBackoffTime() time.Duration - GRPCAuthErrorDelay() time.Duration -} - // Reporter is the top-level interface implemented by a full reporter. type Reporter interface { TraceReporter @@ -47,8 +34,18 @@ type TraceReporter interface { // ReportCountForTrace accepts a hash of a trace with a corresponding count and // caches this information before a periodic reporting to the backend. - ReportCountForTrace(traceHash libpf.TraceHash, timestamp libpf.UnixTime32, - count uint16, comm, podName, containerName string) + ReportCountForTrace(traceHash libpf.TraceHash, timestamp libpf.UnixTime64, + count uint16, comm, podName, containerName, apmServiceName string) + + // ReportTraceEvent accepts a trace event (trace metadata with frames and counts) + // and caches it for reporting to the backend. It returns true if the event was + // enqueued for reporting, and false if the event was ignored. + ReportTraceEvent(trace *libpf.Trace, timestamp libpf.UnixTime64, + comm, podName, containerName, apmServiceName string) + + // SupportsReportTraceEvent returns true if the reporter supports reporting trace events + // via ReportTraceEvent(). + SupportsReportTraceEvent() bool } type SymbolReporter interface { @@ -62,7 +59,7 @@ type SymbolReporter interface { // FrameMetadata accepts metadata associated with a frame and caches this information before // a periodic reporting to the backend. FrameMetadata(fileID libpf.FileID, addressOrLine libpf.AddressOrLineno, - lineNumber libpf.SourceLineno, functionOffset uint32, functionName, filePath string) + lineNumber util.SourceLineno, functionOffset uint32, functionName, filePath string) } type HostMetadataReporter interface { diff --git a/reporter/metrics.go b/reporter/metrics.go index c9d1ec7c..7ca9f43c 100644 --- a/reporter/metrics.go +++ b/reporter/metrics.go @@ -16,7 +16,7 @@ import ( "github.com/elastic/otel-profiling-agent/libpf/xsync" ) -type statsHandlerImpl struct { +type StatsHandlerImpl struct { // Total number of uncompressed bytes in/out numRPCBytesOut atomic.Int64 numRPCBytesIn atomic.Int64 @@ -35,16 +35,16 @@ type statsHandlerImpl struct { } // Make sure that the handler implements stats.Handler. -var _ stats.Handler = (*statsHandlerImpl)(nil) +var _ stats.Handler = (*StatsHandlerImpl)(nil) // keyRPCTagInfo is the context key for our state. // // This is in a global to avoid having to allocate a new string on every call. var keyRPCTagInfo = "RPCTagInfo" -// newStatsHandler creates a new statistics handler. -func newStatsHandler() *statsHandlerImpl { - return &statsHandlerImpl{ +// NewStatsHandler creates a new statistics handler. +func NewStatsHandler() *StatsHandlerImpl { + return &StatsHandlerImpl{ rpcBytesOut: xsync.NewRWMutex(map[string]uint64{}), rpcBytesIn: xsync.NewRWMutex(map[string]uint64{}), wireBytesOut: xsync.NewRWMutex(map[string]uint64{}), @@ -53,17 +53,17 @@ func newStatsHandler() *statsHandlerImpl { } // TagRPC implements the stats.Handler interface. -func (sh *statsHandlerImpl) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context { +func (sh *StatsHandlerImpl) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context { return context.WithValue(ctx, &keyRPCTagInfo, info) } // TagConn implements the stats.Handler interface. -func (sh *statsHandlerImpl) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context { +func (sh *StatsHandlerImpl) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context { return ctx } // HandleConn implements the stats.Handler interface. -func (sh *statsHandlerImpl) HandleConn(context.Context, stats.ConnStats) { +func (sh *StatsHandlerImpl) HandleConn(context.Context, stats.ConnStats) { } func rpcMethodFromContext(ctx context.Context) (string, error) { @@ -75,7 +75,7 @@ func rpcMethodFromContext(ctx context.Context) (string, error) { } // HandleRPC implements the stats.Handler interface. -func (sh *statsHandlerImpl) HandleRPC(ctx context.Context, s stats.RPCStats) { +func (sh *StatsHandlerImpl) HandleRPC(ctx context.Context, s stats.RPCStats) { var wireBytesIn, wireBytesOut, rpcBytesIn, rpcBytesOut int64 switch s := s.(type) { @@ -120,24 +120,24 @@ func (sh *statsHandlerImpl) HandleRPC(ctx context.Context, s stats.RPCStats) { } } -func (sh *statsHandlerImpl) getWireBytesOut() int64 { +func (sh *StatsHandlerImpl) GetWireBytesOut() int64 { return sh.numWireBytesOut.Swap(0) } -func (sh *statsHandlerImpl) getWireBytesIn() int64 { +func (sh *StatsHandlerImpl) GetWireBytesIn() int64 { return sh.numWireBytesIn.Swap(0) } -func (sh *statsHandlerImpl) getRPCBytesOut() int64 { +func (sh *StatsHandlerImpl) GetRPCBytesOut() int64 { return sh.numRPCBytesOut.Swap(0) } -func (sh *statsHandlerImpl) getRPCBytesIn() int64 { +func (sh *StatsHandlerImpl) GetRPCBytesIn() int64 { return sh.numRPCBytesIn.Swap(0) } // nolint:unused -func (sh *statsHandlerImpl) getMethodRPCBytesOut() map[string]uint64 { +func (sh *StatsHandlerImpl) getMethodRPCBytesOut() map[string]uint64 { rpcOut := sh.rpcBytesOut.RLock() defer sh.rpcBytesOut.RUnlock(&rpcOut) res := make(map[string]uint64, len(*rpcOut)) @@ -148,7 +148,7 @@ func (sh *statsHandlerImpl) getMethodRPCBytesOut() map[string]uint64 { } // nolint:unused -func (sh *statsHandlerImpl) getMethodRPCBytesIn() map[string]uint64 { +func (sh *StatsHandlerImpl) getMethodRPCBytesIn() map[string]uint64 { rpcIn := sh.rpcBytesIn.RLock() defer sh.rpcBytesIn.RUnlock(&rpcIn) res := make(map[string]uint64, len(*rpcIn)) @@ -159,7 +159,7 @@ func (sh *statsHandlerImpl) getMethodRPCBytesIn() map[string]uint64 { } // nolint:unused -func (sh *statsHandlerImpl) getMethodWireBytesOut() map[string]uint64 { +func (sh *StatsHandlerImpl) getMethodWireBytesOut() map[string]uint64 { wireOut := sh.wireBytesOut.RLock() defer sh.wireBytesOut.RUnlock(&wireOut) res := make(map[string]uint64, len(*wireOut)) @@ -170,7 +170,7 @@ func (sh *statsHandlerImpl) getMethodWireBytesOut() map[string]uint64 { } // nolint:unused -func (sh *statsHandlerImpl) getMethodWireBytesIn() map[string]uint64 { +func (sh *StatsHandlerImpl) getMethodWireBytesIn() map[string]uint64 { wireIn := sh.wireBytesIn.RLock() defer sh.wireBytesIn.RUnlock(&wireIn) res := make(map[string]uint64, len(*wireIn)) @@ -194,19 +194,3 @@ type Metrics struct { WireBytesOutCount int64 WireBytesInCount int64 } - -func (r *GRPCReporter) GetMetrics() Metrics { - return Metrics{ - CountsForTracesOverwriteCount: r.countsForTracesQueue.getOverwriteCount(), - ExeMetadataOverwriteCount: r.execMetadataQueue.getOverwriteCount(), - FrameMetadataOverwriteCount: r.frameMetadataQueue.getOverwriteCount(), - FramesForTracesOverwriteCount: r.framesForTracesQueue.getOverwriteCount(), - HostMetadataOverwriteCount: r.hostMetadataQueue.getOverwriteCount(), - MetricsOverwriteCount: r.metricsQueue.getOverwriteCount(), - FallbackSymbolsOverwriteCount: r.fallbackSymbolsQueue.getOverwriteCount(), - RPCBytesOutCount: r.rpcStats.getRPCBytesOut(), - RPCBytesInCount: r.rpcStats.getRPCBytesIn(), - WireBytesOutCount: r.rpcStats.getWireBytesOut(), - WireBytesInCount: r.rpcStats.getWireBytesIn(), - } -} diff --git a/reporter/otlp_reporter.go b/reporter/otlp_reporter.go index c75baa1b..92f8be13 100644 --- a/reporter/otlp_reporter.go +++ b/reporter/otlp_reporter.go @@ -8,56 +8,43 @@ package reporter import ( "context" - "fmt" + "crypto/rand" + "crypto/tls" + "maps" + "slices" + "strconv" "time" + lru "github.com/elastic/go-freelru" "github.com/elastic/otel-profiling-agent/config" - "github.com/elastic/otel-profiling-agent/libpf/vc" - otlpcollector "github.com/elastic/otel-profiling-agent/proto/experiments/opentelemetry/proto/collector/profiles/v1" - profiles "github.com/elastic/otel-profiling-agent/proto/experiments/opentelemetry/proto/profiles/v1" - "github.com/elastic/otel-profiling-agent/proto/experiments/opentelemetry/proto/profiles/v1/alternatives/pprofextended" - - "github.com/elastic/otel-profiling-agent/debug/log" "github.com/elastic/otel-profiling-agent/libpf" - + "github.com/elastic/otel-profiling-agent/libpf/xsync" + "github.com/elastic/otel-profiling-agent/util" + log "github.com/sirupsen/logrus" + "github.com/zeebo/xxh3" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.25.0" + otlpcollector "go.opentelemetry.io/proto/otlp/collector/profiles/v1experimental" common "go.opentelemetry.io/proto/otlp/common/v1" + profiles "go.opentelemetry.io/proto/otlp/profiles/v1experimental" resource "go.opentelemetry.io/proto/otlp/resource/v1" - - lru "github.com/elastic/go-freelru" - "github.com/zeebo/xxh3" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" ) // Assert that we implement the full Reporter interface. var _ Reporter = (*OTLPReporter)(nil) -// traceInfo holds static information about a trace. -type traceInfo struct { - files []libpf.FileID - linenos []libpf.AddressOrLineno - frameTypes []libpf.FrameType - comm string - podName string - containerName string - apmServiceName string -} - -// sample holds dynamic information about traces. -type sample struct { - // In most cases OTEP/profiles requests timestamps in a uint64 format - // and use nanosecond precision - https://github.com/open-telemetry/oteps/issues/253 - timestamps []uint64 - count uint32 -} - // execInfo enriches an executable with additional metadata. type execInfo struct { fileName string buildID string } -// sourceInfo allows to map a frame to its source origin. +// sourceInfo allows mapping a frame to its source origin. type sourceInfo struct { - lineNumber libpf.SourceLineno + lineNumber util.SourceLineno functionOffset uint32 functionName string filePath string @@ -69,8 +56,26 @@ type funcInfo struct { fileName string } +// traceFramesCounts holds known information about a trace. +type traceFramesCounts struct { + files []libpf.FileID + linenos []libpf.AddressOrLineno + frameTypes []libpf.FrameType + comm string + podName string + containerName string + apmServiceName string + timestamps []uint64 // in nanoseconds +} + // OTLPReporter receives and transforms information to be OTLP/profiles compliant. type OTLPReporter struct { + // name is the ScopeProfile's name. + name string + + // version is the ScopeProfile's version. + version string + // client for the connection to the receiver. client otlpcollector.ProfilesServiceClient @@ -78,21 +83,15 @@ type OTLPReporter struct { stopSignal chan libpf.Void // rpcStats stores gRPC related statistics. - rpcStats *statsHandlerImpl + rpcStats *StatsHandlerImpl // To fill in the OTLP/profiles signal with the relevant information, - // this structure holds in long term storage information that might + // this structure holds in long-term storage information that might // be duplicated in other places but not accessible for OTLPReporter. // hostmetadata stores metadata that is sent out with every request. hostmetadata *lru.SyncedLRU[string, string] - // traces stores static information needed for samples. - traces *lru.SyncedLRU[libpf.TraceHash, traceInfo] - - // samples holds a map of currently encountered traces. - samples *lru.SyncedLRU[libpf.TraceHash, sample] - // fallbackSymbols keeps track of FrameID to their symbol. fallbackSymbols *lru.SyncedLRU[libpf.FrameID, string] @@ -100,71 +99,55 @@ type OTLPReporter struct { executables *lru.SyncedLRU[libpf.FileID, execInfo] // frames maps frame information to its source location. - frames *lru.SyncedLRU[libpf.FileID, map[libpf.AddressOrLineno]sourceInfo] + frames *lru.SyncedLRU[libpf.FileID, xsync.RWMutex[map[libpf.AddressOrLineno]sourceInfo]] + + // traceEvents stores reported trace events (trace metadata with frames and counts) + traceEvents xsync.RWMutex[map[libpf.TraceHash]traceFramesCounts] + + // pkgGRPCOperationTimeout sets the time limit for GRPC requests. + pkgGRPCOperationTimeout time.Duration } // hashString is a helper function for LRUs that use string as a key. -// xxh3 turned out to be the fastest hash function for strings in the FreeLRU benchmarks. +// Xxh3 turned out to be the fastest hash function for strings in the FreeLRU benchmarks. // It was only outperformed by the AES hash function, which is implemented in Plan9 assembly. func hashString(s string) uint32 { return uint32(xxh3.HashString(s)) } -// ReportFramesForTrace accepts a trace with the corresponding frames -// and caches this information. -func (r *OTLPReporter) ReportFramesForTrace(trace *libpf.Trace) { - if v, exists := r.traces.Peek(trace.Hash); exists { - // As traces is filled from two different API endpoints, - // some information for the trace might be available already. - // For simplicty, the just received information overwrites the - // the existing one. - v.files = trace.Files - v.linenos = trace.Linenos - v.frameTypes = trace.FrameTypes - - r.traces.Add(trace.Hash, v) - } else { - r.traces.Add(trace.Hash, traceInfo{ - files: trace.Files, - linenos: trace.Linenos, - frameTypes: trace.FrameTypes, - }) +func (r *OTLPReporter) SupportsReportTraceEvent() bool { return true } + +// ReportTraceEvent enqueues reported trace events for the OTLP reporter. +func (r *OTLPReporter) ReportTraceEvent(trace *libpf.Trace, + timestamp libpf.UnixTime64, comm, podName, + containerName, apmServiceName string) { + traceEvents := r.traceEvents.WLock() + defer r.traceEvents.WUnlock(&traceEvents) + + if tr, exists := (*traceEvents)[trace.Hash]; exists { + tr.timestamps = append(tr.timestamps, uint64(timestamp)) + (*traceEvents)[trace.Hash] = tr + return } -} -// ReportCountForTrace accepts a hash of a trace with a corresponding count and -// caches this information. -func (r *OTLPReporter) ReportCountForTrace(traceHash libpf.TraceHash, timestamp libpf.UnixTime32, - count uint16, comm, podName, containerName string) { - if v, exists := r.traces.Peek(traceHash); exists { - // As traces is filled from two different API endpoints, - // some information for the trace might be available already. - // For simplicty, the just received information overwrites the - // the existing one. - v.comm = comm - v.podName = podName - v.containerName = containerName - - r.traces.Add(traceHash, v) - } else { - r.traces.Add(traceHash, traceInfo{ - comm: comm, - podName: podName, - containerName: containerName, - }) + (*traceEvents)[trace.Hash] = traceFramesCounts{ + files: trace.Files, + linenos: trace.Linenos, + frameTypes: trace.FrameTypes, + comm: comm, + podName: podName, + containerName: containerName, + apmServiceName: apmServiceName, + timestamps: []uint64{uint64(timestamp)}, } +} - if v, ok := r.samples.Peek(traceHash); ok { - v.count += uint32(count) - v.timestamps = append(v.timestamps, uint64(timestamp)) +// ReportFramesForTrace is a NOP for OTLPReporter. +func (r *OTLPReporter) ReportFramesForTrace(_ *libpf.Trace) {} - r.samples.Add(traceHash, v) - } else { - r.samples.Add(traceHash, sample{ - count: uint32(count), - timestamps: []uint64{uint64(timestamp)}, - }) - } +// ReportCountForTrace is a NOP for OTLPReporter. +func (r *OTLPReporter) ReportCountForTrace(_ libpf.TraceHash, _ libpf.UnixTime64, + _ uint16, _, _, _, _ string) { } // ReportFallbackSymbol enqueues a fallback symbol for reporting, for a given frame. @@ -187,21 +170,26 @@ func (r *OTLPReporter) ExecutableMetadata(_ context.Context, // FrameMetadata accepts metadata associated with a frame and caches this information. func (r *OTLPReporter) FrameMetadata(fileID libpf.FileID, addressOrLine libpf.AddressOrLineno, - lineNumber libpf.SourceLineno, functionOffset uint32, functionName, filePath string) { - if v, exists := r.frames.Get(fileID); exists { + lineNumber util.SourceLineno, functionOffset uint32, functionName, filePath string) { + if frameMapLock, exists := r.frames.Get(fileID); exists { + frameMap := frameMapLock.WLock() + defer frameMapLock.WUnlock(&frameMap) + if filePath == "" { // The new filePath may be empty, and we don't want to overwrite // an existing filePath with it. - if s, exists := v[addressOrLine]; exists { + if s, exists := (*frameMap)[addressOrLine]; exists { filePath = s.filePath } } - v[addressOrLine] = sourceInfo{ + + (*frameMap)[addressOrLine] = sourceInfo{ lineNumber: lineNumber, functionOffset: functionOffset, functionName: functionName, filePath: filePath, } + return } @@ -212,7 +200,7 @@ func (r *OTLPReporter) FrameMetadata(fileID libpf.FileID, addressOrLine libpf.Ad functionName: functionName, filePath: filePath, } - r.frames.Add(fileID, v) + r.frames.Add(fileID, xsync.NewRWMutex(v)) } // ReportHostMetadata enqueues host metadata. @@ -245,27 +233,16 @@ func (r *OTLPReporter) Stop() { // GetMetrics returns internal metrics of OTLPReporter. func (r *OTLPReporter) GetMetrics() Metrics { return Metrics{ - RPCBytesOutCount: r.rpcStats.getRPCBytesOut(), - RPCBytesInCount: r.rpcStats.getRPCBytesIn(), - WireBytesOutCount: r.rpcStats.getWireBytesOut(), - WireBytesInCount: r.rpcStats.getWireBytesIn(), + RPCBytesOutCount: r.rpcStats.GetRPCBytesOut(), + RPCBytesInCount: r.rpcStats.GetRPCBytesIn(), + WireBytesOutCount: r.rpcStats.GetWireBytesOut(), + WireBytesInCount: r.rpcStats.GetWireBytesIn(), } } -// StartOTLP sets up and manages the reporting connection to a OTLP backend. -func StartOTLP(mainCtx context.Context, c *Config) (Reporter, error) { +// Start sets up and manages the reporting connection to a OTLP backend. +func Start(mainCtx context.Context, cfg *Config) (Reporter, error) { cacheSize := config.TraceCacheEntries() - - traces, err := lru.NewSynced[libpf.TraceHash, traceInfo](cacheSize, libpf.TraceHash.Hash32) - if err != nil { - return nil, err - } - - samples, err := lru.NewSynced[libpf.TraceHash, sample](cacheSize, libpf.TraceHash.Hash32) - if err != nil { - return nil, err - } - fallbackSymbols, err := lru.NewSynced[libpf.FrameID, string](cacheSize, libpf.FrameID.Hash32) if err != nil { return nil, err @@ -277,13 +254,13 @@ func StartOTLP(mainCtx context.Context, c *Config) (Reporter, error) { } frames, err := lru.NewSynced[libpf.FileID, - map[libpf.AddressOrLineno]sourceInfo](cacheSize, libpf.FileID.Hash32) + xsync.RWMutex[map[libpf.AddressOrLineno]sourceInfo]](cacheSize, libpf.FileID.Hash32) if err != nil { return nil, err } // Next step: Dynamically configure the size of this LRU. - // Currently we use the length of the JSON array in + // Currently, we use the length of the JSON array in // hostmetadata/hostmetadata.json. hostmetadata, err := lru.NewSynced[string, string](115, hashString) if err != nil { @@ -291,15 +268,17 @@ func StartOTLP(mainCtx context.Context, c *Config) (Reporter, error) { } r := &OTLPReporter{ - stopSignal: make(chan libpf.Void), - client: nil, - rpcStats: newStatsHandler(), - traces: traces, - samples: samples, - fallbackSymbols: fallbackSymbols, - executables: executables, - frames: frames, - hostmetadata: hostmetadata, + name: cfg.Name, + version: cfg.Version, + stopSignal: make(chan libpf.Void), + pkgGRPCOperationTimeout: cfg.Times.GRPCOperationTimeout(), + client: nil, + rpcStats: NewStatsHandler(), + fallbackSymbols: fallbackSymbols, + executables: executables, + frames: frames, + hostmetadata: hostmetadata, + traceEvents: xsync.NewRWMutex(map[libpf.TraceHash]traceFramesCounts{}), } // Create a child context for reporting features @@ -308,7 +287,7 @@ func StartOTLP(mainCtx context.Context, c *Config) (Reporter, error) { // Establish the gRPC connection before going on, waiting for a response // from the collectionAgent endpoint. // Use grpc.WithBlock() in setupGrpcConnection() for this to work. - otlpGrpcConn, err := waitGrpcEndpoint(ctx, c, r.rpcStats) + otlpGrpcConn, err := waitGrpcEndpoint(ctx, cfg, r.rpcStats) if err != nil { cancelReporting() close(r.stopSignal) @@ -317,7 +296,7 @@ func StartOTLP(mainCtx context.Context, c *Config) (Reporter, error) { r.client = otlpcollector.NewProfilesServiceClient(otlpGrpcConn) go func() { - tick := time.NewTicker(c.Times.ReportInterval()) + tick := time.NewTicker(cfg.Times.ReportInterval()) defer tick.Stop() for { select { @@ -329,7 +308,7 @@ func StartOTLP(mainCtx context.Context, c *Config) (Reporter, error) { if err := r.reportOTLPProfile(ctx); err != nil { log.Errorf("Request failed: %v", err) } - tick.Reset(libpf.AddJitter(c.Times.ReportInterval(), 0.2)) + tick.Reset(libpf.AddJitter(cfg.Times.ReportInterval(), 0.2)) } } }() @@ -358,13 +337,9 @@ func (r *OTLPReporter) reportOTLPProfile(ctx context.Context) error { } pc := []*profiles.ProfileContainer{{ - // Next step: not sure about the value of ProfileId - // Discussion around this field and its requirements started with - // https://github.com/open-telemetry/oteps/pull/239#discussion_r1491546899 - // As an ID with all zeros is considered invalid, we write ELASTIC here. - ProfileId: []byte("ELASTIC"), - StartTimeUnixNano: uint64(time.Unix(int64(startTS), 0).UnixNano()), - EndTimeUnixNano: uint64(time.Unix(int64(endTS), 0).UnixNano()), + ProfileId: mkProfileID(), + StartTimeUnixNano: startTS, + EndTimeUnixNano: endTS, // Attributes - Optional element we do not use. // DroppedAttributesCount - Optional element we do not use. // OriginalPayloadFormat - Optional element we do not use. @@ -375,80 +350,83 @@ func (r *OTLPReporter) reportOTLPProfile(ctx context.Context) error { scopeProfiles := []*profiles.ScopeProfiles{{ Profiles: pc, Scope: &common.InstrumentationScope{ - Name: "Elastic-Universal-Profiling", - Version: fmt.Sprintf("%s@%s", vc.Version(), vc.Revision()), + Name: r.name, + Version: r.version, }, - // SchemaUrl - This element is not well defined yet. Therefore we skip it. + // SchemaUrl - This element is not well-defined yet. Therefore, we skip it. }} resourceProfiles := []*profiles.ResourceProfiles{{ Resource: r.getResource(), ScopeProfiles: scopeProfiles, - // SchemaUrl - This element is not well defined yet. Therefore we skip it. + // SchemaUrl - This element is not well-defined yet. Therefore, we skip it. }} req := otlpcollector.ExportProfilesServiceRequest{ ResourceProfiles: resourceProfiles, } - _, err := r.client.Export(ctx, &req) + reqCtx, ctxCancel := context.WithTimeout(ctx, r.pkgGRPCOperationTimeout) + defer ctxCancel() + _, err := r.client.Export(reqCtx, &req) return err } +// mkProfileID creates a random profile ID. +func mkProfileID() []byte { + profileID := make([]byte, 16) + _, err := rand.Read(profileID) + if err != nil { + return []byte("otel-profiling-agent") + } + return profileID +} + // getResource returns the OTLP resource information of the origin of the profiles. // Next step: maybe extend this information with go.opentelemetry.io/otel/sdk/resource. func (r *OTLPReporter) getResource() *resource.Resource { keys := r.hostmetadata.Keys() - attributes := make([]*common.KeyValue, len(keys)) - i := 0 - for _, k := range keys { - v, ok := r.hostmetadata.Get(k) - if !ok { - continue - } - attributes[i] = &common.KeyValue{ - Key: k, + attributes := make([]*common.KeyValue, 0, len(keys)) + + addAttr := func(k attribute.Key, v string) { + attributes = append(attributes, &common.KeyValue{ + Key: string(k), Value: &common.AnyValue{Value: &common.AnyValue_StringValue{StringValue: v}}, - } - i++ - } - origin := &resource.Resource{ - Attributes: attributes, + }) } - return origin -} -// getProfile returns an OTLP profile containing all collected samples up to this moment. -func (r *OTLPReporter) getProfile() (profile *pprofextended.Profile, startTS uint64, endTS uint64) { - // Avoid overlapping locks by copying its content. - sampleKeys := r.samples.Keys() - samplesCpy := make(map[libpf.TraceHash]sample, len(sampleKeys)) - for _, k := range sampleKeys { - v, ok := r.samples.Get(k) - if !ok { - continue + // Add hostmedata to the attributes. + for _, k := range keys { + if v, ok := r.hostmetadata.Get(k); ok { + addAttr(attribute.Key(k), v) } - samplesCpy[k] = v - r.samples.Remove(k) } - var samplesWoTraceinfo []libpf.TraceHash + // Add event specific attributes. + // These attributes are also included in the host metadata, but with different names/keys. + // That makes our hostmetadata attributes incompatible with OTEL collectors. + // TODO: Make a final decision about project id. + addAttr("profiling.project.id", strconv.FormatUint(uint64(config.ProjectID()), 10)) + addAttr(semconv.HostIDKey, strconv.FormatUint(config.HostID(), 10)) + addAttr(semconv.HostIPKey, config.IPAddress()) + addAttr(semconv.HostNameKey, config.Hostname()) + addAttr(semconv.ServiceVersionKey, r.version) + addAttr("os.kernel", config.KernelVersion()) - for trace := range samplesCpy { - if _, exists := r.traces.Peek(trace); !exists { - samplesWoTraceinfo = append(samplesWoTraceinfo, trace) - } + return &resource.Resource{ + Attributes: attributes, } +} - if len(samplesWoTraceinfo) != 0 { - log.Debugf("Missing trace information for %d samples", len(samplesWoTraceinfo)) - // Return samples for which relevant information is not available yet. - for _, trace := range samplesWoTraceinfo { - r.samples.Add(trace, samplesCpy[trace]) - delete(samplesCpy, trace) - } +// getProfile returns an OTLP profile containing all collected samples up to this moment. +func (r *OTLPReporter) getProfile() (profile *profiles.Profile, startTS uint64, endTS uint64) { + traceEvents := r.traceEvents.WLock() + samples := maps.Clone(*traceEvents) + for key := range *traceEvents { + delete(*traceEvents, key) } + r.traceEvents.WUnlock(&traceEvents) // stringMap is a temporary helper that will build the StringTable. // By specification, the first element should be empty. @@ -460,10 +438,19 @@ func (r *OTLPReporter) getProfile() (profile *pprofextended.Profile, startTS uin funcMap := make(map[funcInfo]uint64) funcMap[funcInfo{name: "", fileName: ""}] = 0 - numSamples := len(samplesCpy) - profile = &pprofextended.Profile{ + numSamples := len(samples) + profile = &profiles.Profile{ // SampleType - Next step: Figure out the correct SampleType. - Sample: make([]*pprofextended.Sample, 0, numSamples), + Sample: make([]*profiles.Sample, 0, numSamples), + SampleType: []*profiles.ValueType{{ + Type: int64(getStringMapIndex(stringMap, "samples")), + Unit: int64(getStringMapIndex(stringMap, "count")), + }}, + PeriodType: &profiles.ValueType{ + Type: int64(getStringMapIndex(stringMap, "cpu")), + Unit: int64(getStringMapIndex(stringMap, "nanoseconds")), + }, + Period: 1e9 / int64(config.SamplesPerSecond()), // LocationIndices - Optional element we do not use. // AttributeTable - Optional element we do not use. // AttributeUnits - Optional element we do not use. @@ -484,54 +471,47 @@ func (r *OTLPReporter) getProfile() (profile *pprofextended.Profile, startTS uin fileIDtoMapping := make(map[libpf.FileID]uint64) frameIDtoFunction := make(map[libpf.FrameID]uint64) - for traceHash, sampleInfo := range samplesCpy { - sample := &pprofextended.Sample{} + for traceHash, traceInfo := range samples { + sample := &profiles.Sample{} sample.LocationsStartIndex = locationIndex - // Earlier we peeked into traces for traceHash and know it exists. - trace, _ := r.traces.Get(traceHash) - sample.StacktraceIdIndex = getStringMapIndex(stringMap, - traceHash.StringNoQuotes()) - - sample.Timestamps = make([]uint64, 0, len(sampleInfo.timestamps)) - for _, ts := range sampleInfo.timestamps { - sample.Timestamps = append(sample.Timestamps, - uint64(time.Unix(int64(ts), 0).UnixMilli())) - if ts < startTS || startTS == 0 { - startTS = ts - continue - } - if ts > endTS { - endTS = ts - } - } + traceHash.Base64()) + + timestamps, values := dedupSlice(traceInfo.timestamps) + // dedupTimestamps returns a sorted list of timestamps, so + // startTs and endTs can be used directly. + startTS = timestamps[0] + endTS = timestamps[len(timestamps)-1] + + sample.TimestampsUnixNano = timestamps + sample.Value = values // Walk every frame of the trace. - for i := range trace.frameTypes { - loc := &pprofextended.Location{ + for i := range traceInfo.frameTypes { + loc := &profiles.Location{ // Id - Optional element we do not use. TypeIndex: getStringMapIndex(stringMap, - trace.frameTypes[i].String()), - Address: uint64(trace.linenos[i]), + traceInfo.frameTypes[i].String()), + Address: uint64(traceInfo.linenos[i]), // IsFolded - Optional element we do not use. // Attributes - Optional element we do not use. } - switch frameKind := trace.frameTypes[i]; frameKind { + switch frameKind := traceInfo.frameTypes[i]; frameKind { case libpf.NativeFrame: // As native frames are resolved in the backend, we use Mapping to // report these frames. var locationMappingIndex uint64 - if tmpMappingIndex, exists := fileIDtoMapping[trace.files[i]]; exists { + if tmpMappingIndex, exists := fileIDtoMapping[traceInfo.files[i]]; exists { locationMappingIndex = tmpMappingIndex } else { idx := uint64(len(fileIDtoMapping)) - fileIDtoMapping[trace.files[i]] = idx + fileIDtoMapping[traceInfo.files[i]] = idx locationMappingIndex = idx - execInfo, exists := r.executables.Get(trace.files[i]) + execInfo, exists := r.executables.Get(traceInfo.files[i]) // Next step: Select a proper default value, // if the name of the executable is not known yet. @@ -540,15 +520,15 @@ func (r *OTLPReporter) getProfile() (profile *pprofextended.Profile, startTS uin fileName = execInfo.fileName } - profile.Mapping = append(profile.Mapping, &pprofextended.Mapping{ + profile.Mapping = append(profile.Mapping, &profiles.Mapping{ // Id - Optional element we do not use. // MemoryStart - Optional element we do not use. // MemoryLImit - Optional element we do not use. - FileOffset: uint64(trace.linenos[i]), + FileOffset: uint64(traceInfo.linenos[i]), Filename: int64(getStringMapIndex(stringMap, fileName)), BuildId: int64(getStringMapIndex(stringMap, - trace.files[i].StringNoQuotes())), - BuildIdKind: *pprofextended.BuildIdKind_BUILD_ID_BINARY_HASH.Enum(), + traceInfo.files[i].StringNoQuotes())), + BuildIdKind: *profiles.BuildIdKind_BUILD_ID_BINARY_HASH.Enum(), // Attributes - Optional element we do not use. // HasFunctions - Optional element we do not use. // HasFilenames - Optional element we do not use. @@ -559,9 +539,9 @@ func (r *OTLPReporter) getProfile() (profile *pprofextended.Profile, startTS uin loc.MappingIndex = locationMappingIndex case libpf.KernelFrame: // Reconstruct frameID - frameID := libpf.NewFrameID(trace.files[i], trace.linenos[i]) - // Store Kernel frame information as Line message: - line := &pprofextended.Line{} + frameID := libpf.NewFrameID(traceInfo.files[i], traceInfo.linenos[i]) + // Store Kernel frame information as a Line message: + line := &profiles.Line{} if tmpFunctionIndex, exists := frameIDtoFunction[frameID]; exists { line.FunctionIndex = tmpFunctionIndex @@ -572,35 +552,38 @@ func (r *OTLPReporter) getProfile() (profile *pprofextended.Profile, startTS uin // reported yet. symbol = "UNKNOWN" } + + // Indicates "no source filename" for kernel frames. line.FunctionIndex = createFunctionEntry(funcMap, - symbol, "vmlinux") + symbol, "") } loc.Line = append(loc.Line, line) - // To be compliant with the protocol generate a dummy mapping entry. + // To be compliant with the protocol, generate a placeholder mapping entry. loc.MappingIndex = getDummyMappingIndex(fileIDtoMapping, stringMap, - profile, trace.files[i]) + profile, traceInfo.files[i]) case libpf.AbortFrame: // Next step: Figure out how the OTLP protocol // could handle artificial frames, like AbortFrame, - // that are not originate from a native or interpreted + // that are not originated from a native or interpreted // program. default: - // Store interpreted frame information as Line message: - line := &pprofextended.Line{} + // Store interpreted frame information as a Line message: + line := &profiles.Line{} - fileIDInfo, exists := r.frames.Get(trace.files[i]) + fileIDInfoLock, exists := r.frames.Get(traceInfo.files[i]) if !exists { // At this point, we do not have enough information for the frame. // Therefore, we report a dummy entry and use the interpreter as filename. line.FunctionIndex = createFunctionEntry(funcMap, "UNREPORTED", frameKind.String()) } else { - si, exists := fileIDInfo[trace.linenos[i]] + fileIDInfo := fileIDInfoLock.RLock() + si, exists := (*fileIDInfo)[traceInfo.linenos[i]] if !exists { // At this point, we do not have enough information for the frame. // Therefore, we report a dummy entry and use the interpreter as filename. - // To differentiate this case with the case where no information about + // To differentiate this case from the case where no information about // the file ID is available at all, we use a different name for reported // function. line.FunctionIndex = createFunctionEntry(funcMap, @@ -611,50 +594,55 @@ func (r *OTLPReporter) getProfile() (profile *pprofextended.Profile, startTS uin line.FunctionIndex = createFunctionEntry(funcMap, si.functionName, si.filePath) } + fileIDInfoLock.RUnlock(&fileIDInfo) } loc.Line = append(loc.Line, line) - // To be compliant with the protocol generate a dummy mapping entry. + // To be compliant with the protocol, generate a dummy mapping entry. loc.MappingIndex = getDummyMappingIndex(fileIDtoMapping, stringMap, - profile, trace.files[i]) + profile, traceInfo.files[i]) } profile.Location = append(profile.Location, loc) } - sample.Label = getTraceLabels(stringMap, trace) - sample.LocationsLength = uint64(len(trace.frameTypes)) + sample.Attributes = getSampleAttributes(profile, traceInfo) + sample.LocationsLength = uint64(len(traceInfo.frameTypes)) locationIndex += sample.LocationsLength + profile.SampleType = append(profile.SampleType, setOnCPUValueType(stringMap)) profile.Sample = append(profile.Sample, sample) } log.Debugf("Reporting OTLP profile with %d samples", len(profile.Sample)) // Populate the deduplicated functions into profile. - funcTable := make([]*pprofextended.Function, len(funcMap)) + funcTable := make([]*profiles.Function, len(funcMap)) for v, idx := range funcMap { - funcTable[idx] = &pprofextended.Function{ + funcTable[idx] = &profiles.Function{ Name: int64(getStringMapIndex(stringMap, v.name)), Filename: int64(getStringMapIndex(stringMap, v.fileName)), } } profile.Function = append(profile.Function, funcTable...) - // When ranging over stringMap the order will be according to the + // When ranging over stringMap, the order will be according to the // hash value of the key. To get the correct order for profile.StringTable, - // put the values in stringMap in the correct array order. + // put the values in stringMap, in the correct array order. stringTable := make([]string, len(stringMap)) for v, idx := range stringMap { stringTable[idx] = v } profile.StringTable = append(profile.StringTable, stringTable...) - // profile.LocationIndices is not optional and we only write elements into - // profile.Location that are referenced by sample. + // profile.LocationIndices is not optional, and we only write elements into + // profile.Location that at least one sample references. profile.LocationIndices = make([]int64, len(profile.Location)) for i := int64(0); i < int64(len(profile.Location)); i++ { profile.LocationIndices[i] = i } + profile.DurationNanos = int64(endTS - startTS) + profile.TimeNanos = int64(startTS) + return profile, startTS, endTS } @@ -687,56 +675,29 @@ func createFunctionEntry(funcMap map[funcInfo]uint64, return idx } -// getTraceLabels builds OTEP/Label(s) from traceInfo. -func getTraceLabels(stringMap map[string]uint32, i traceInfo) []*pprofextended.Label { - var labels []*pprofextended.Label - - if i.comm != "" { - commIdx := getStringMapIndex(stringMap, "comm") - commValueIdx := getStringMapIndex(stringMap, i.comm) - - labels = append(labels, &pprofextended.Label{ - Key: int64(commIdx), - Str: int64(commValueIdx), - }) - } - - if i.podName != "" { - podNameIdx := getStringMapIndex(stringMap, "podName") - podNameValueIdx := getStringMapIndex(stringMap, i.podName) +// getSampleAttributes builds a sample-specific list of attributes. +func getSampleAttributes(profile *profiles.Profile, i traceFramesCounts) []uint64 { + indices := make([]uint64, 0, 4) - labels = append(labels, &pprofextended.Label{ - Key: int64(podNameIdx), - Str: int64(podNameValueIdx), - }) - } - - if i.containerName != "" { - containerNameIdx := getStringMapIndex(stringMap, "containerName") - containerNameValueIdx := getStringMapIndex(stringMap, i.containerName) - - labels = append(labels, &pprofextended.Label{ - Key: int64(containerNameIdx), - Str: int64(containerNameValueIdx), + addAttr := func(k attribute.Key, v string) { + indices = append(indices, uint64(len(profile.AttributeTable))) + profile.AttributeTable = append(profile.AttributeTable, &common.KeyValue{ + Key: string(k), + Value: &common.AnyValue{Value: &common.AnyValue_StringValue{StringValue: v}}, }) } - if i.apmServiceName != "" { - apmServiceNameIdx := getStringMapIndex(stringMap, "apmServiceName") - apmServiceNameValueIdx := getStringMapIndex(stringMap, i.apmServiceName) - - labels = append(labels, &pprofextended.Label{ - Key: int64(apmServiceNameIdx), - Str: int64(apmServiceNameValueIdx), - }) - } + addAttr(semconv.K8SPodNameKey, i.podName) + addAttr(semconv.ContainerNameKey, i.containerName) + addAttr(semconv.ThreadNameKey, i.comm) + addAttr(semconv.ServiceNameKey, i.apmServiceName) - return labels + return indices } -// getDummyMappingIndex inserts or looks up a dummy entry for interpreted FileIDs. +// getDummyMappingIndex inserts or looks up an entry for interpreted FileIDs. func getDummyMappingIndex(fileIDtoMapping map[libpf.FileID]uint64, - stringMap map[string]uint32, profile *pprofextended.Profile, + stringMap map[string]uint32, profile *profiles.Profile, fileID libpf.FileID) uint64 { var locationMappingIndex uint64 if tmpMappingIndex, exists := fileIDtoMapping[fileID]; exists { @@ -746,14 +707,107 @@ func getDummyMappingIndex(fileIDtoMapping map[libpf.FileID]uint64, fileIDtoMapping[fileID] = idx locationMappingIndex = idx - fileName := "DUMMY" - - profile.Mapping = append(profile.Mapping, &pprofextended.Mapping{ - Filename: int64(getStringMapIndex(stringMap, fileName)), + profile.Mapping = append(profile.Mapping, &profiles.Mapping{ + Filename: int64(getStringMapIndex(stringMap, "")), BuildId: int64(getStringMapIndex(stringMap, fileID.StringNoQuotes())), - BuildIdKind: *pprofextended.BuildIdKind_BUILD_ID_BINARY_HASH.Enum(), + BuildIdKind: *profiles.BuildIdKind_BUILD_ID_BINARY_HASH.Enum(), }) } return locationMappingIndex } + +// setOnCPUValueType returns the default Profile.Sample_Type for on CPU profiling. +func setOnCPUValueType(stringMap map[string]uint32) *profiles.ValueType { + return &profiles.ValueType{ + Type: int64(getStringMapIndex(stringMap, "on_cpu")), + Unit: int64(getStringMapIndex(stringMap, "count")), + } +} + +// dedupSlice returns a sorted slice of unique values along with their count. +// If a value appears only a single time in values, its count will be 1, +// otherwise, count will be increased with every appearance of the same value. +// NOTE: This function may modify the input slice or return it as-is. +func dedupSlice(values []uint64) (out []uint64, count []int64) { + if len(values) == 1 { + return values, []int64{1} + } + + out = make([]uint64, 0, len(values)) + count = make([]int64, 0, len(values)) + slices.Sort(values) + + for i := 0; i < len(values); i++ { + if i > 0 && values[i-1] == values[i] { + count[len(count)-1]++ + continue + } + out = append(out, values[i]) + count = append(count, 1) + } + return out, count +} + +// waitGrpcEndpoint waits until the gRPC connection is established. +func waitGrpcEndpoint(ctx context.Context, cfg *Config, + statsHandler *StatsHandlerImpl) (*grpc.ClientConn, error) { + // Sleep with a fixed backoff time added of +/- 20% jitter + tick := time.NewTicker(libpf.AddJitter(cfg.Times.GRPCStartupBackoffTime(), 0.2)) + defer tick.Stop() + + var retries uint32 + for { + if collAgentConn, err := setupGrpcConnection(ctx, cfg, statsHandler); err != nil { + if retries >= cfg.MaxGRPCRetries { + return nil, err + } + retries++ + + log.Warnf( + "Failed to setup gRPC connection (try %d of %d): %v", + retries, + cfg.MaxGRPCRetries, + err, + ) + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-tick.C: + continue + } + } else { + return collAgentConn, nil + } + } +} + +// setupGrpcConnection sets up a gRPC connection instrumented with our auth interceptor +func setupGrpcConnection(parent context.Context, cfg *Config, + statsHandler *StatsHandlerImpl) (*grpc.ClientConn, error) { + //nolint:staticcheck + opts := []grpc.DialOption{grpc.WithBlock(), + grpc.WithStatsHandler(statsHandler), + grpc.WithUnaryInterceptor(cfg.GRPCClientInterceptor), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(cfg.MaxRPCMsgSize), + grpc.MaxCallSendMsgSize(cfg.MaxRPCMsgSize)), + grpc.WithReturnConnectionError(), + } + + if cfg.DisableTLS { + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } else { + opts = append(opts, + grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + // Support only TLS1.3+ with valid CA certificates + MinVersion: tls.VersionTLS13, + InsecureSkipVerify: false, + }))) + } + + ctx, cancel := context.WithTimeout(parent, cfg.Times.GRPCConnectionTimeout()) + defer cancel() + //nolint:staticcheck + return grpc.DialContext(ctx, cfg.CollAgentAddr, opts...) +} diff --git a/reporter/otlp_reporter_test.go b/reporter/otlp_reporter_test.go new file mode 100644 index 00000000..d13ef620 --- /dev/null +++ b/reporter/otlp_reporter_test.go @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package reporter + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDedupSlice(t *testing.T) { + tests := map[string]struct { + values []uint64 + out []uint64 + outCount []int64 + }{ + "single timestamp": { + values: []uint64{42}, + out: []uint64{42}, + outCount: []int64{1}, + }, + "duplicate timestamps": { + values: []uint64{42, 42, 42}, + out: []uint64{42}, + outCount: []int64{3}, + }, + "mixed timestamps": { + values: []uint64{42, 73, 42, 37, 42, 11}, + out: []uint64{11, 37, 42, 73}, + outCount: []int64{1, 1, 3, 1}, + }, + } + + for name, test := range tests { + name := name + test := test + t.Run(name, func(t *testing.T) { + newVal, newCount := dedupSlice(test.values) + assert.Equal(t, test.out, newVal) + assert.Equal(t, test.outCount, newCount) + }) + } +} diff --git a/reporter/reporter.go b/reporter/reporter.go deleted file mode 100644 index b297d7dd..00000000 --- a/reporter/reporter.go +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -// Package reporter implements a central reporting mechanism for various data types. The provided -// information is cached before it is sent in a configured interval to the destination. -// It may happen that information get lost if reporter can not send the provided information -// to the destination. -// -// As we must convert our internal types, e.g. libpf.TraceHash, into primitive types, before sending -// them over the wire, the question arises as to where to do this? In this package we favor doing -// so as close to the actual 'send' over the network as possible. So, the ReportX functions that -// clients of this package make use of try to accept our types, push them onto a reporting queue, -// and then do the conversion in whichever function flushes that queue and sends the data over -// the wire. -package reporter - -import ( - "context" - "fmt" - "time" - - "github.com/elastic/otel-profiling-agent/libpf" -) - -// HostMetadata holds metadata about the host. -type HostMetadata struct { - Metadata map[string]string - Timestamp uint64 -} - -type Config struct { - // CollAgentAddr defines the destination of the backend connection - CollAgentAddr string - - // MaxRPCMsgSize defines the maximum size of a gRPC message. - MaxRPCMsgSize int - - // ExecMetadataMaxQueue defines the maximum size for the queue which holds - // data of type collectionagent.ExecutableMetadata. - ExecMetadataMaxQueue uint32 - // CountsForTracesMaxQueue defines the maximum size for the queue which holds - // data of type libpf.TraceAndCounts. - CountsForTracesMaxQueue uint32 - // MetricsMaxQueue defines the maximum size for the queue which holds - // data of type collectionagent.Metric. - MetricsMaxQueue uint32 - // FramesForTracesMaxQueue defines the maximum size for the queue which holds - // data of type libpf.Trace. - FramesForTracesMaxQueue uint32 - // FrameMetadataMaxQueue defines the maximum size for the queue which holds - // data of type collectionagent.FrameMetadata. - FrameMetadataMaxQueue uint32 - // HostMetadataMaxQueue defines the maximum size for the queue which holds - // data of type collectionagent.HostMetadata. - HostMetadataMaxQueue uint32 - // FallbackSymbolsMaxQueue defines the maximum size for the queue which holds - // data of type collectionagent.FallbackSymbol. - FallbackSymbolsMaxQueue uint32 - // Disable secure communication with Collection Agent - DisableTLS bool - // Number of connection attempts to the collector after which we give up retrying - MaxGRPCRetries uint32 - - Times Times -} - -// GRPCReporter will be the reporter state and implements various reporting interfaces -type GRPCReporter struct { - // stopSignal is the stop signal for shutting down all background tasks. - stopSignal chan libpf.Void - - // rpcStats stores gRPC related statistics. - rpcStats *statsHandlerImpl - - // executableMetadataQueue is a ring buffer based FIFO for *executableMetadata - execMetadataQueue fifoRingBuffer[*executableMetadata] - // countsForTracesQueue is a ring buffer based FIFO for *libpf.TraceAndCounts - countsForTracesQueue fifoRingBuffer[*libpf.TraceAndCounts] - // metricsQueue is a ring buffer based FIFO for *tsMetric - metricsQueue fifoRingBuffer[*tsMetric] - // framesForTracesQueue is a ring buffer based FIFO for *libpf.Trace - framesForTracesQueue fifoRingBuffer[*libpf.Trace] - // frameMetadataQueue is a ring buffer based FIFO for *frameMetadata - frameMetadataQueue fifoRingBuffer[*libpf.FrameMetadata] - // hostMetadataQueue is a ring buffer based FIFO for collectionagent.HostMetadata. - hostMetadataQueue fifoRingBuffer[*HostMetadata] - // fallbackSymbolsQueue is a ring buffer based FIFO for *fallbackSymbol - fallbackSymbolsQueue fifoRingBuffer[*fallbackSymbol] -} - -// Assert that we implement the full Reporter interface. -var _ Reporter = (*GRPCReporter)(nil) - -// ReportFramesForTrace implements the TraceReporter interface. -func (r *GRPCReporter) ReportFramesForTrace(trace *libpf.Trace) { - r.framesForTracesQueue.append(trace) -} - -type executableMetadata struct { - fileID libpf.FileID - filename string - buildID string -} - -// ExecutableMetadata implements the SymbolReporter interface. -func (r *GRPCReporter) ExecutableMetadata(ctx context.Context, fileID libpf.FileID, - fileName, buildID string) { - select { - case <-ctx.Done(): - return - default: - r.execMetadataQueue.append(&executableMetadata{ - fileID: fileID, - filename: fileName, - buildID: buildID, - }) - } -} - -// FrameMetadata implements the SymbolReporter interface. -func (r *GRPCReporter) FrameMetadata(fileID libpf.FileID, - addressOrLine libpf.AddressOrLineno, lineNumber libpf.SourceLineno, functionOffset uint32, - functionName, filePath string) { - r.frameMetadataQueue.append(&libpf.FrameMetadata{ - FileID: fileID, - AddressOrLine: addressOrLine, - LineNumber: lineNumber, - FunctionOffset: functionOffset, - FunctionName: functionName, - Filename: filePath, - }) -} - -// ReportCountForTrace implements the TraceReporter interface. -func (r *GRPCReporter) ReportCountForTrace(traceHash libpf.TraceHash, timestamp libpf.UnixTime32, - count uint16, comm, podName, containerName string) { - r.countsForTracesQueue.append(&libpf.TraceAndCounts{ - Hash: traceHash, - Timestamp: timestamp, - Count: count, - Comm: comm, - PodName: podName, - ContainerName: containerName, - }) -} - -type fallbackSymbol struct { - frameID libpf.FrameID - symbol string -} - -// ReportFallbackSymbol implements the SymbolReporter interface. -func (r *GRPCReporter) ReportFallbackSymbol(frameID libpf.FrameID, symbol string) { - r.fallbackSymbolsQueue.append(&fallbackSymbol{ - frameID: frameID, - symbol: symbol, - }) -} - -type tsMetric struct { - timestamp uint32 - ids []uint32 - values []int64 -} - -// ReportMetrics implements the MetricsReporter interface. -func (r *GRPCReporter) ReportMetrics(timestamp uint32, ids []uint32, values []int64) { - r.metricsQueue.append(&tsMetric{ - timestamp: timestamp, - ids: ids, - values: values, - }) -} - -// ReportHostMetadata implements the HostMetadataReporter interface. -func (r *GRPCReporter) ReportHostMetadata(_ map[string]string) { -} - -// ReportHostMetadataBlocking implements the HostMetadataReporter interface. -func (r *GRPCReporter) ReportHostMetadataBlocking(_ context.Context, - _ map[string]string, _ int, _ time.Duration) error { - return nil -} - -// Start sets up and manages the reporting connection to our backend as well as a per data -// type caching mechanism to send the provided information in bulks to the backend. -// Callers of Start should be calling the corresponding Stop() API to conclude gracefully -// the operations managed here. -func Start(_ context.Context, c *Config) (*GRPCReporter, error) { - r := &GRPCReporter{ - stopSignal: make(chan libpf.Void), - rpcStats: newStatsHandler(), - } - - if err := r.execMetadataQueue.initFifo(c.ExecMetadataMaxQueue, - "executable metadata"); err != nil { - return nil, fmt.Errorf("failed to setup queue for executable metadata: %v", err) - } - - if err := r.countsForTracesQueue.initFifo(c.CountsForTracesMaxQueue, - "counts for traces"); err != nil { - return nil, fmt.Errorf("failed to setup queue for tracehash count: %v", err) - } - - if err := r.metricsQueue.initFifo(c.MetricsMaxQueue, - "metrics"); err != nil { - return nil, fmt.Errorf("failed to setup queue for metrics: %v", err) - } - - if err := r.framesForTracesQueue.initFifo(c.FramesForTracesMaxQueue, - "frames for traces"); err != nil { - return nil, fmt.Errorf("failed to setup queue for frames for traces: %v", err) - } - - if err := r.frameMetadataQueue.initFifo(c.FrameMetadataMaxQueue, - "frame metadata"); err != nil { - return nil, fmt.Errorf("failed to setup queue for frame metadata: %v", err) - } - - if err := r.hostMetadataQueue.initFifo(c.HostMetadataMaxQueue, - "host metadata"); err != nil { - return nil, fmt.Errorf("failed to setup queue for host metadata: %v", err) - } - - if err := r.fallbackSymbolsQueue.initFifo(c.FallbackSymbolsMaxQueue, - "fallback symbols"); err != nil { - return nil, fmt.Errorf("failed to setup queue for fallback symbols: %v", err) - } - - return r, nil -} - -// Stop asks all background tasks to exit. -func (r *GRPCReporter) Stop() { - close(r.stopSignal) -} diff --git a/reporter/times.go b/reporter/times.go new file mode 100644 index 00000000..a287d70d --- /dev/null +++ b/reporter/times.go @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package reporter + +import ( + "time" + + "github.com/elastic/otel-profiling-agent/config" +) + +// Compile time check to make sure config.Times satisfies the interfaces. +var _ Times = (*config.Times)(nil) + +// Times is a subset of config.IntervalsAndTimers. +type Times interface { + ReportInterval() time.Duration + ReportMetricsInterval() time.Duration + GRPCConnectionTimeout() time.Duration + GRPCOperationTimeout() time.Duration + GRPCStartupBackoffTime() time.Duration + GRPCAuthErrorDelay() time.Duration +} diff --git a/libpf/rlimit/rlimit.go b/rlimit/rlimit.go similarity index 100% rename from libpf/rlimit/rlimit.go rename to rlimit/rlimit.go diff --git a/libpf/stringutil/stringutil.go b/stringutil/stringutil.go similarity index 93% rename from libpf/stringutil/stringutil.go rename to stringutil/stringutil.go index 3c9b806b..873c49dd 100644 --- a/libpf/stringutil/stringutil.go +++ b/stringutil/stringutil.go @@ -66,7 +66,7 @@ func FieldsN(s string, f []string) int { func SplitN(s, sep string, f []string) int { n := len(f) i := 0 - for ; i < n-1 && len(s) > 0; i++ { + for ; i < n-1 && s != ""; i++ { fieldEnd := strings.Index(s, sep) if fieldEnd < 0 { f[i] = s @@ -87,3 +87,8 @@ func SplitN(s, sep string, f []string) int { func ByteSlice2String(b []byte) string { return *(*string)(unsafe.Pointer(&b)) } + +// StrDataPtr returns the string's underlying Data pointer through reflection. +func StrDataPtr(s string) *byte { + return unsafe.StringData(s) +} diff --git a/libpf/stringutil/stringutil_test.go b/stringutil/stringutil_test.go similarity index 74% rename from libpf/stringutil/stringutil_test.go rename to stringutil/stringutil_test.go index 730485d0..f9902f78 100644 --- a/libpf/stringutil/stringutil_test.go +++ b/stringutil/stringutil_test.go @@ -8,6 +8,9 @@ package stringutil import ( "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFieldsN(t *testing.T) { @@ -34,14 +37,7 @@ func TestFieldsN(t *testing.T) { t.Run(name, func(t *testing.T) { var fields [4]string n := FieldsN(testcase.input, fields[:testcase.maxFields]) - if len(testcase.expected) != n { - t.Fatalf("unexpected result1: %v\nexpected: %v", fields, testcase.expected) - } - for i := range testcase.expected { - if testcase.expected[i] != fields[i] { - t.Fatalf("unexpected result2: %v\nexpected: %v", fields, testcase.expected) - } - } + require.Equal(t, testcase.expected, fields[:n]) }) } } @@ -70,14 +66,7 @@ func TestSplitN(t *testing.T) { t.Run(name, func(t *testing.T) { var fields [4]string n := SplitN(testcase.input, "-", fields[:testcase.maxFields]) - if len(testcase.expected) != n { - t.Fatalf("unexpected result (%d): %v\nexpected: %v", n, fields, testcase.expected) - } - for i := range testcase.expected { - if testcase.expected[i] != fields[i] { - t.Fatalf("unexpected result2: %v\nexpected: %v", fields, testcase.expected) - } - } + require.Equal(t, testcase.expected, fields[:n]) }) } } @@ -85,13 +74,8 @@ func TestSplitN(t *testing.T) { func TestByteSlice2String(t *testing.T) { var b [4]byte s := ByteSlice2String(b[:1]) // create s with length 1 and a 0 byte inside - - if s != "\x00" { - t.Fatalf("Unexpected string '%s', expected '\x00'", s) - } + assert.Equal(t, "\x00", s) b[0] = 'a' - if s != "a" { - t.Fatalf("Unexpected string '%s', expected 'a'", s) - } + assert.Equal(t, "a", s) } diff --git a/libpf/successfailurecounter/successfailurecounter.go b/successfailurecounter/successfailurecounter.go similarity index 100% rename from libpf/successfailurecounter/successfailurecounter.go rename to successfailurecounter/successfailurecounter.go diff --git a/libpf/successfailurecounter/successfailurecounter_test.go b/successfailurecounter/successfailurecounter_test.go similarity index 86% rename from libpf/successfailurecounter/successfailurecounter_test.go rename to successfailurecounter/successfailurecounter_test.go index 0764ac33..e31f709c 100644 --- a/libpf/successfailurecounter/successfailurecounter_test.go +++ b/successfailurecounter/successfailurecounter_test.go @@ -9,6 +9,8 @@ package successfailurecounter import ( "sync/atomic" "testing" + + "github.com/stretchr/testify/assert" ) func defaultToSuccess(t *testing.T, sfc SuccessFailureCounter, n int) { @@ -79,14 +81,8 @@ func TestSuccessFailureCounter(t *testing.T) { var success, failure atomic.Uint64 sfc := New(&success, &failure) test.call(t, sfc, test.input) - if test.expectedSucess != success.Load() { - t.Fatalf("Expected success %d but got %d", - test.expectedSucess, success.Load()) - } - if test.expectedFailure != failure.Load() { - t.Fatalf("Expected failure %d but got %d", - test.expectedFailure, failure.Load()) - } + assert.Equal(t, test.expectedSucess, success.Load()) + assert.Equal(t, test.expectedFailure, failure.Load()) }) } } diff --git a/support/ebpf/COPYING b/support/ebpf/COPYING deleted file mode 100644 index 3dfbbdbe..00000000 --- a/support/ebpf/COPYING +++ /dev/null @@ -1,15 +0,0 @@ -Copyright (C) 2019-2024 Elasticsearch B.V. - -For code under this directory, unless otherwise specified, the following -license applies (GPLv2 only). Note that this licensing only applies to -the files under this directory, and not this project as a whole. - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License version 2 only, -as published by the Free Software Foundation; - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the file -LICENSE in this directory for more details about the terms of the -license. diff --git a/support/ebpf/Makefile b/support/ebpf/Makefile index 315d9526..d954694c 100644 --- a/support/ebpf/Makefile +++ b/support/ebpf/Makefile @@ -1,8 +1,8 @@ -SHELL:=/usr/bin/env bash +SHELL ?= bash -CLANG=clang -LINK=llvm-link -LLC=llc +CLANG ?= clang-16 +LINK ?= llvm-link-16 +LLC ?= llc-16 DEBUG_FLAGS = -DOPTI_DEBUG -g @@ -11,7 +11,7 @@ NATIVE_ARCH:=$(shell uname -m) ifeq ($(NATIVE_ARCH),x86_64) NATIVE_ARCH:=x86 -else ifneq (,$(filter $(NATIVE_ARCH),aarch64 arm64)) +else ifeq ($(NATIVE_ARCH),aarch64) NATIVE_ARCH:=arm64 else $(error Unsupported architecture: $(NATIVE_ARCH)) @@ -31,8 +31,9 @@ TARGET_FLAGS = -target x86_64-linux-gnu endif FLAGS=$(TARGET_FLAGS) \ - -nostdinc \ + -fno-jump-tables \ -nostdlib \ + -nostdinc \ -ffreestanding \ -O2 -emit-llvm -c $< \ -Wall -Wextra -Werror \ @@ -40,8 +41,7 @@ FLAGS=$(TARGET_FLAGS) \ -Wno-unused-label \ -Wno-unused-parameter \ -Wno-sign-compare \ - -fno-stack-protector \ - -fno-jump-tables + -fno-stack-protector SRCS := $(wildcard *.ebpf.c) OBJS := $(SRCS:.c=.o) @@ -65,6 +65,9 @@ debug-x86: debug-arm64: $(MAKE) target_arch=arm64 debug +errors.h: ../../tools/errors-codegen/errors.json + go run ../../tools/errors-codegen/main.go bpf $@ + %.ebpf.c: errors.h ; %.ebpf.o: %.ebpf.c @@ -81,4 +84,4 @@ bloatcheck: $(TRACER_NAME) python3 bloat-o-meter $(TRACER_NAME).baseline $(TRACER_NAME) clean: - rm -f *.o $(TRACER_NAME) $(TRACER_NAME).* + rm -f *.o diff --git a/support/ebpf/bpfdefs.h b/support/ebpf/bpfdefs.h index 20c202ba..7171b3c2 100644 --- a/support/ebpf/bpfdefs.h +++ b/support/ebpf/bpfdefs.h @@ -7,29 +7,24 @@ #if defined(TESTING_COREDUMP) // utils/coredump uses CGO to build the eBPF code. Provide here the glue to // dispatch the BPF API to helpers implemented in ebpfhelpers.go. - #define SEC(NAME) #define printt(fmt, ...) bpf_log(fmt, ##__VA_ARGS__) #define DEBUG_PRINT(fmt, ...) bpf_log(fmt, ##__VA_ARGS__) #define OPTI_DEBUG - // The members of the userspace 'struct pt_regs' are named - // slightly different than the members of the kernel space structure. - // So we don't include - // #include - // #include "linux/bpf.h" - // Instead we copy the kernel space 'struct pt_regs' here and - // define 'struct bpf_perf_event_data' manually. - // BPF helpers. Mostly stubs to dispatch the call to Go code with the context ID. int bpf_tail_call(void *ctx, bpf_map_def *map, int index); unsigned long long bpf_ktime_get_ns(void); int bpf_get_current_comm(void *, int); - static inline int bpf_probe_read(void *buf, u32 sz, const void *ptr) { - int __bpf_probe_read(u64, void *, u32, const void *); - return __bpf_probe_read(__cgo_ctx->id, buf, sz, ptr); + static inline long bpf_probe_read_user(void *buf, u32 sz, const void *ptr) { + long __bpf_probe_read_user(u64, void *, u32, const void *); + return __bpf_probe_read_user(__cgo_ctx->id, buf, sz, ptr); + } + + static inline long bpf_probe_read_kernel(void *buf, u32 sz, const void *ptr) { + return -1; } static inline u64 bpf_get_current_pid_tgid(void) { @@ -93,6 +88,11 @@ __attribute__ ((format (printf, 1, 3))) static int (*bpf_trace_printk)(const char *fmt, int fmt_size, ...) = (void *)BPF_FUNC_trace_printk; +static long (*bpf_probe_read_user)(void *dst, int size, const void *unsafe_ptr) = + (void *)BPF_FUNC_probe_read_user; +static long (*bpf_probe_read_kernel)(void *dst, int size, const void *unsafe_ptr) = + (void *)BPF_FUNC_probe_read_kernel; + // The sizeof in bpf_trace_printk() must include \0, else no output // is generated. The \n is not needed on 5.8+ kernels, but definitely on // 5.4 kernels. @@ -145,6 +145,4 @@ static int (*bpf_trace_printk)(const char *fmt, int fmt_size, ...) = #endif // !TESTING_COREDUMP -#define ATOMIC_ADD(ptr, n) __sync_fetch_and_add(ptr, n) - #endif // OPTI_BPFDEFS_H diff --git a/support/ebpf/dotnet_tracer.ebpf.c b/support/ebpf/dotnet_tracer.ebpf.c new file mode 100644 index 00000000..84b3ebc5 --- /dev/null +++ b/support/ebpf/dotnet_tracer.ebpf.c @@ -0,0 +1,290 @@ +// This file contains the code and map definitions for the Dotnet tracer +// +// Core unwinding of frames is simple, as all the generated code uses frame pointers, +// and all the interesting data is directly accessible via FP. +// +// See the host agent interpreter/dotnet/ for more references. + +#include "bpfdefs.h" +#include "tracemgmt.h" +#include "types.h" + +// The number of dotnet frames to unwind per frame-unwinding eBPF program. +#define DOTNET_FRAMES_PER_PROGRAM 5 + +// The maximum dotnet frame length used in heuristic to validate FP +#define DOTNET_MAX_FRAME_LENGTH 8192 + +// Keep in sync with dotnet interpreter code +#define DOTNET_CODE_JIT 0x1f +#define DOTNET_CODE_FLAG_LEAF 0x80 + +// Map from dotnet process IDs to a structure containing addresses of variables +// we require in order to build the stack trace +bpf_map_def SEC("maps") dotnet_procs = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(pid_t), + .value_size = sizeof(DotnetProcInfo), + .max_entries = 1024, +}; + +// Nibble map tunables +// https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/inc/nibblemapmacros.h +#define DOTNET_CODE_ALIGN 4 +#define DOTNET_CODE_NIBBLES_PER_ENTRY 8 // 8nibbles * 4 bits/nibble = 32bit word +#define DOTNET_CODE_BYTES_PER_NIBBLE 32 // one nibble maps to 32 bytes of code +#define DOTNET_CODE_BYTES_PER_ENTRY (DOTNET_CODE_BYTES_PER_NIBBLE*DOTNET_CODE_NIBBLES_PER_ENTRY) + +// Find method code header using a dotnet coreclr "NibbleMap" +// Currently this technically could require an unbounded for loop to scan through the nibble map. +// The make things work in the eBPF the number of elements we parse are limited by the scratch +// buffer size. This needs to be in eBPF for the methods which may be Garbage Collected (typically +// short runtime generated IL code). If we start seeing "code too large" errors, we can also do +// this same lookup from the Host Agent because most generated code (especially large pieces) are +// currently not Garbage Collected by the runtime. Though, we have submitted also an enhancement +// request to fix the nibble map format to something sane, and this might get implemented. +// see: https://github.com/dotnet/runtime/issues/93550 +static inline __attribute__((__always_inline__)) +ErrorCode dotnet_find_code_start(PerCPURecord *record, DotnetProcInfo *vi, u64 pc, u64 *code_start) { + // This is an ebpf optimized implementation of EEJitManager::FindMethodCode() + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/codeman.cpp#L4115 + // The support code setups the page mapping so that: + // text_section_base = pHp->mapBase (base address of the JIT area) + // text_section_id = pHp->pHdrMap (pointer to the nibble map) + const UnwindState *state = &record->state; + DotnetUnwindScratchSpace *scratch = &record->dotnetUnwindScratch; + const int map_elements = sizeof(scratch->map)/sizeof(scratch->map[0]); + u64 pc_base = state->text_section_bias; + u64 pc_delta = pc - pc_base; + u64 map_start = state->text_section_id; + + DEBUG_PRINT("dotnet: --> find code start for %lx: pc_base %lx, map_start %lx", + (unsigned long) pc_delta, (unsigned long) pc_base, (unsigned long) map_start); + pc_delta &= ~(DOTNET_CODE_ALIGN-1); + + // Read the nibble map data + int offs = 0; + if (pc_delta < (map_elements-2)*DOTNET_CODE_BYTES_PER_ENTRY) { + // Read from map_start so that end of scratch->map corresponds to pc_delta + offs = map_elements - pc_delta/DOTNET_CODE_BYTES_PER_ENTRY - 1; + } else { + // We can read full scratch buffer, adjust map_start so that last entry read corresponds pc_delta + map_start += pc_delta/DOTNET_CODE_BYTES_PER_ENTRY*sizeof(u32) - sizeof(scratch->map) + sizeof(u32); + } + if (bpf_probe_read_user(&scratch->map[offs], sizeof(scratch->map), (void*) map_start)) { + goto bad_code_header; + } + + // Determine if the first map entry contains the start region + int pos = map_elements; + u32 val = scratch->map[--pos]; + DEBUG_PRINT("dotnet: --> find code start for %lx: first entry %x", + (unsigned long) pc_delta, val); + val >>= 28 - ((pc_delta / DOTNET_CODE_BYTES_PER_NIBBLE) % DOTNET_CODE_NIBBLES_PER_ENTRY) * 4; + if (val != 0) { + // Adjust pc_delta to beginning of the positioned nibble of 'val' + pc_delta &= ~(DOTNET_CODE_BYTES_PER_NIBBLE - 1); + } else { + // Adjust delta to end of previous map entry + pc_delta &= ~(DOTNET_CODE_BYTES_PER_ENTRY - 1); + pc_delta -= DOTNET_CODE_BYTES_PER_NIBBLE; + val = scratch->map[--pos]; + DEBUG_PRINT("dotnet: --> find code start for %lx: second entry %x", + (unsigned long) pc_delta, val); + + // Find backwards the first non-zero entry as it marks function start + // This is unrolled several times, so it needs to be minimal in size. + // And currently this is the major limit for DOTNET_FRAMES_PER_PROGRAM. + int orig_pos = pos; +#pragma unroll 256 + for (int i = 0; i < map_elements - 2; i++) { + if (val != 0) { + break; + } + val = scratch->map[--pos]; + } + + // Adjust pc_delta based on how many iterations were done + u64 pc_skipped = DOTNET_CODE_BYTES_PER_ENTRY * (orig_pos - pos); + if (pc_delta < pc_skipped) { + DEBUG_PRINT("dotnet: nibble map search went below pc_base"); + goto bad_code_header; + } + pc_delta -= pc_skipped; + DEBUG_PRINT("dotnet: --> find code start for %lx: skipped %d, entry %x", + (unsigned long) pc_delta, orig_pos - pos, val); + if (val == 0) { + increment_metric(metricID_UnwindDotnetErrCodeTooLarge); + return ERR_DOTNET_CODE_TOO_LARGE; + } + } + + // Decode the code start info from the entry +#pragma unroll + for (int i = 0; i < DOTNET_CODE_NIBBLES_PER_ENTRY; i++) { + u8 nybble = val & 0xf; + if (nybble != 0) { + *code_start = pc_base + pc_delta + (nybble - 1) * DOTNET_CODE_ALIGN; + DEBUG_PRINT("dotnet: --> pc_delta = %lx, val=%x, ret=%lx", + (unsigned long) pc_delta, nybble, (unsigned long) *code_start); + return ERR_OK; + } + val >>= 4; + pc_delta -= DOTNET_CODE_BYTES_PER_NIBBLE; + } + +bad_code_header: + DEBUG_PRINT("dotnet: not found"); + increment_metric(metricID_UnwindDotnetErrCodeHeader); + return ERR_DOTNET_CODE_HEADER; +} + +// Record a Dotnet frame +static inline __attribute__((__always_inline__)) +ErrorCode push_dotnet(Trace *trace, u64 code_header_ptr, u64 pc_offset, bool return_address) { + return _push_with_return_address(trace, code_header_ptr, pc_offset, FRAME_MARKER_DOTNET, return_address); +} + +// Unwind one dotnet frame +static inline __attribute__((__always_inline__)) +ErrorCode unwind_one_dotnet_frame(PerCPURecord *record, DotnetProcInfo *vi, bool top) { + UnwindState *state = &record->state; + Trace *trace = &record->trace; + u64 regs[2], sp = state->sp, fp = state->fp, pc = state->pc; + bool return_address = state->return_address; + + // All dotnet frames have frame pointer. Check that the FP looks valid. + DEBUG_PRINT("dotnet: pc: %lx, sp: %lx, fp: %lx", + (unsigned long) pc, (unsigned long) sp, (unsigned long) fp); + + if (fp < sp || fp >= sp + DOTNET_MAX_FRAME_LENGTH) { + DEBUG_PRINT("dotnet: frame pointer too far off %lx / %lx", + (unsigned long) fp, (unsigned long) sp); + increment_metric(metricID_UnwindDotnetErrBadFP); + return ERR_DOTNET_BAD_FP; + } + + // Default to R2R/stub code_start. + u64 type = state->text_section_id; + u64 code_start = state->text_section_bias; + u64 code_header_ptr = pc; + + state->return_address = true; + + if (type < 0x100 && (type & DOTNET_CODE_FLAG_LEAF)) { + // Stub frame that does not do calls. + // For arm this is unwind with LR, and for x86-64 unwind with RA only. + if (bpf_probe_read_user(&state->pc, sizeof(state->pc), (void*)state->sp)) { + DEBUG_PRINT("dotnet: --> bad stack pointer"); + increment_metric(metricID_UnwindDotnetErrBadFP); + return ERR_DOTNET_BAD_FP; + } + state->sp += 8; + type &= 0x7f; + goto push_frame; + } + + // Unwind with frame pointer. On Linux the frame pointers are always on. + // https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/botr/clr-abi.md#system-v-x86_64-support + // FIXME: Early prologue and epilogue may skip a frame. Seems prologue is fixed, consider + // using heuristic to handle prologue when the new frame is not yet pushed to stack. + if (bpf_probe_read_user(regs, sizeof(regs), (void*)fp)) { + DEBUG_PRINT("dotnet: --> bad frame pointer"); + increment_metric(metricID_UnwindDotnetErrBadFP); + return ERR_DOTNET_BAD_FP; + } + state->sp = fp + sizeof(regs); + state->fp = regs[0]; + state->pc = regs[1]; + DEBUG_PRINT("dotnet: pc: %lx, sp: %lx, fp: %lx", + (unsigned long) state->pc, (unsigned long) state->sp, + (unsigned long) state->fp); + + if (type < 0x100) { + // Not a JIT frame. A R2R frame at this point. + type &= 0x7f; + goto push_frame; + } + + // JIT generated code, locate code start + ErrorCode error = dotnet_find_code_start(record, vi, pc, &code_start); + if (error != ERR_OK) { + DEBUG_PRINT("dotnet: --> code_start failed with %d", error); + // dotnet_find_code_start incremented the metric already + if (error != ERR_DOTNET_CODE_TOO_LARGE) { + return error; + } + return _push(trace, 0, ERR_DOTNET_CODE_TOO_LARGE, FRAME_MARKER_DOTNET|FRAME_MARKER_ERROR_BIT); + } + + // code_start points to beginning of the JIT generated code. This is preceded by a CodeHeader + // structure. The platforms we care define USE_INDIRECT_CODEHEADER, so the data is defined at: + // https://github.com/dotnet/runtime/blob/v7.0.15/src/coreclr/vm/codeman.h#L246-L248 + // This just reads the single pointer to the RealCodeHeader. + if (bpf_probe_read_user(&code_header_ptr, sizeof(code_header_ptr), (void*)code_start-sizeof(u64))) { + DEBUG_PRINT("dotnet: --> bad code header"); + increment_metric(metricID_UnwindDotnetErrCodeHeader); + return ERR_DOTNET_CODE_HEADER; + } + type = DOTNET_CODE_JIT; + +push_frame: + DEBUG_PRINT("dotnet: --> code_start = %lx, code_header = %lx, pc_offset = %lx", + (unsigned long) code_start, (unsigned long) code_header_ptr, (unsigned long)(pc - code_start)); + error = push_dotnet(trace, (code_header_ptr << 5) + type, pc - code_start, return_address); + if (error) { + return error; + } + + increment_metric(metricID_UnwindDotnetFrames); + return ERR_OK; +} + +// unwind_dotnet is the entry point for tracing when invoked from the native tracer +// or interpreter dispatcher. It does not reset the trace object and will append the +// dotnet stack frames to the trace object for the current CPU. +SEC("perf_event/unwind_dotnet") +int unwind_dotnet(struct pt_regs *ctx) { + PerCPURecord *record = get_per_cpu_record(); + if (!record) { + return -1; + } + + Trace *trace = &record->trace; + u32 pid = trace->pid; + DEBUG_PRINT("==== unwind_dotnet %d ====", trace->stack_len); + + int unwinder = PROG_UNWIND_STOP; + ErrorCode error = ERR_OK; + DotnetProcInfo *vi = bpf_map_lookup_elem(&dotnet_procs, &pid); + if (!vi) { + DEBUG_PRINT("dotnet: no DotnetProcInfo for this pid"); + error = ERR_DOTNET_NO_PROC_INFO; + increment_metric(metricID_UnwindDotnetErrNoProcInfo); + goto exit; + } + + record->ratelimitAction = RATELIMIT_ACTION_FAST; + increment_metric(metricID_UnwindDotnetAttempts); + +#pragma unroll + for (int i = 0; i < DOTNET_FRAMES_PER_PROGRAM; i++) { + unwinder = PROG_UNWIND_STOP; + + error = unwind_one_dotnet_frame(record, vi, i == 0); + if (error) { + break; + } + + error = get_next_unwinder_after_native_frame(record, &unwinder); + if (error || unwinder != PROG_UNWIND_DOTNET) { + break; + } + } + +exit: + record->state.unwind_error = error; + tail_call(ctx, unwinder); + DEBUG_PRINT("dotnet: tail call for next frame unwinder (%d) failed", unwinder); + return -1; +} diff --git a/support/ebpf/errors.h b/support/ebpf/errors.h index d4ad52d5..4dd60ba8 100644 --- a/support/ebpf/errors.h +++ b/support/ebpf/errors.h @@ -16,7 +16,7 @@ typedef enum ErrorCode { // The trace stack was empty after unwinding completed ERR_EMPTY_STACK = 3, - // Failed to lookup entry in the per-CPU frame list + // Deprecated: Failed to lookup entry in the per-CPU frame list ERR_LOOKUP_PER_CPU_FRAME_LIST = 4, // Maximum number of tail calls was reached @@ -139,6 +139,9 @@ typedef enum ErrorCode { // Native: Code is running in ARM 32-bit compat mode. ERR_NATIVE_AARCH64_32BIT_COMPAT_MODE = 4016, + // Native: Code is running in x86_64 32-bit compat mode. + ERR_NATIVE_X64_32BIT_COMPAT_MODE = 4017, + // V8: Encountered a bad frame pointer during V8 unwinding ERR_V8_BAD_FP = 5000, @@ -146,7 +149,19 @@ typedef enum ErrorCode { ERR_V8_BAD_JS_FUNC = 5001, // V8: No entry for this process exists in the V8 process info array - ERR_V8_NO_PROC_INFO = 5002 + ERR_V8_NO_PROC_INFO = 5002, + + // Dotnet: No entry for this process exists in the dotnet process info array + ERR_DOTNET_NO_PROC_INFO = 6000, + + // Dotnet: Encountered a bad frame pointer during dotnet unwinding + ERR_DOTNET_BAD_FP = 6001, + + // Dotnet: Failed to find or read CodeHeader + ERR_DOTNET_CODE_HEADER = 6002, + + // Dotnet: Code object was too large to unwind in eBPF + ERR_DOTNET_CODE_TOO_LARGE = 6003 } ErrorCode; #endif // OPTI_ERRORS_H diff --git a/support/ebpf/extmaps.h b/support/ebpf/extmaps.h index 28f2b8ac..76a83f57 100644 --- a/support/ebpf/extmaps.h +++ b/support/ebpf/extmaps.h @@ -16,12 +16,14 @@ extern bpf_map_def pid_events; extern bpf_map_def inhibit_events; extern bpf_map_def interpreter_offsets; extern bpf_map_def system_config; +extern bpf_map_def trace_events; #if defined(TESTING_COREDUMP) // References to maps in alphabetical order that // are needed only for testing. +extern bpf_map_def apm_int_procs; extern bpf_map_def exe_id_to_8_stack_deltas; extern bpf_map_def exe_id_to_9_stack_deltas; extern bpf_map_def exe_id_to_10_stack_deltas; @@ -38,10 +40,9 @@ extern bpf_map_def exe_id_to_20_stack_deltas; extern bpf_map_def exe_id_to_21_stack_deltas; extern bpf_map_def hotspot_procs; extern bpf_map_def kernel_stackmap; +extern bpf_map_def dotnet_procs; extern bpf_map_def perl_procs; extern bpf_map_def php_procs; -extern bpf_map_def php_jit_procs; -extern bpf_map_def ptregs_size; extern bpf_map_def py_procs; extern bpf_map_def ruby_procs; extern bpf_map_def stack_delta_page_to_info; diff --git a/support/ebpf/frametypes.h b/support/ebpf/frametypes.h index 0435d3bd..8b947942 100644 --- a/support/ebpf/frametypes.h +++ b/support/ebpf/frametypes.h @@ -31,6 +31,8 @@ #define FRAME_MARKER_V8 0x8 // Indicates a PHP JIT frame #define FRAME_MARKER_PHP_JIT 0x9 +// Indicates a Dotnet frame +#define FRAME_MARKER_DOTNET 0xA // Indicates a frame containing information about a critical unwinding error // that caused further unwinding to be aborted. diff --git a/support/ebpf/hotspot_tracer.ebpf.c b/support/ebpf/hotspot_tracer.ebpf.c index 192a5b3b..11847e4a 100644 --- a/support/ebpf/hotspot_tracer.ebpf.c +++ b/support/ebpf/hotspot_tracer.ebpf.c @@ -102,8 +102,8 @@ bpf_map_def SEC("maps") hotspot_procs = { // Record a HotSpot frame static inline __attribute__((__always_inline__)) -ErrorCode push_hotspot(Trace *trace, u64 file, u64 line) { - return _push(trace, file, line, FRAME_MARKER_HOTSPOT); +ErrorCode push_hotspot(Trace *trace, u64 file, u64 line, bool return_address) { + return _push_with_return_address(trace, file, line, FRAME_MARKER_HOTSPOT, return_address); } // calc_line merges the three values to be encoded in a frame 'line' @@ -161,7 +161,7 @@ u64 hotspot_find_codeblob(const UnwindState *state, const HotspotProcInfo *ji) #pragma unroll for (int i = 0; i < HOTSPOT_SEGMAP_ITERATIONS; i++) { - if (bpf_probe_read(&tag, sizeof(tag), (void*)(segmap_start + segment))) { + if (bpf_probe_read_user(&tag, sizeof(tag), (void*)(segmap_start + segment))) { return 0; } DEBUG_PRINT("jvm: segment %lu, tag %u", segment, (unsigned) tag); @@ -235,7 +235,7 @@ ErrorCode hotspot_handle_interpreter(UnwindState *state,Trace *trace, #define BCP_REGISTER r22 #endif u64 regs[FP_OFFS+2]; - if (bpf_probe_read(regs, sizeof(regs), (void *) (ui->fp - sizeof(u64[FP_OFFS])))) { + if (bpf_probe_read_user(regs, sizeof(regs), (void *) (ui->fp - sizeof(u64[FP_OFFS])))) { DEBUG_PRINT("jvm: failed to read interpreter frame"); goto error; } @@ -266,7 +266,7 @@ ErrorCode hotspot_handle_interpreter(UnwindState *state,Trace *trace, // be offset of the byte code. Mainly to reduce the amount needed for this data from 64-bits // to 16-bits as the bytecode size is limited by JVM to 0xFFFE. u64 cmethod; - if (bpf_probe_read(&cmethod, sizeof(cmethod), (void *) (method + ji->method_constmethod))) { + if (bpf_probe_read_user(&cmethod, sizeof(cmethod), (void *) (method + ji->method_constmethod))) { DEBUG_PRINT("jvm: failed to read interpreter cmethod"); goto error; } @@ -323,7 +323,7 @@ void breadcrumb_fixup(HotspotUnwindInfo *ui) { // https://github.com/openjdk/jdk/blob/jdk-17%2B35/src/hotspot/cpu/aarch64/aarch64.ad#L3731 u64 lookback; - bpf_probe_read(&lookback, sizeof(lookback), (void*)(ui->pc - sizeof(lookback))); + bpf_probe_read_user(&lookback, sizeof(lookback), (void*)(ui->pc - sizeof(lookback))); if (lookback == 0xd63f0100a9bf27ffULL /* stp; blr */) { ui->sp += 0x10; } @@ -460,7 +460,7 @@ bool hotspot_handle_epilogue(const CodeBlobInfo *cbi, HotspotUnwindInfo *ui, u8 find_offset = 0; u32 window[EPI_LOOKBACK]; u64 needle = ldp | (add << 32); - bpf_probe_read(window, sizeof(window), (void*)(ui->pc - sizeof(window) + INSN_LEN)); + bpf_probe_read_user(window, sizeof(window), (void*)(ui->pc - sizeof(window) + INSN_LEN)); #pragma unroll for (; find_offset < EPI_LOOKBACK - 1; ++find_offset) { @@ -530,7 +530,7 @@ ErrorCode hotspot_handle_nmethod(const CodeBlobInfo *cbi, Trace *trace, // Similar fixup is strategy for external unwinding is in: // https://hg.openjdk.java.net/jdk-updates/jdk14u/file/default/src/java.base/solaris/native/libjvm_db/libjvm_db.c#l1059 u64 orig; - if (bpf_probe_read(&orig, sizeof(orig), (void *) (ui->sp + cbi->orig_pc_offset)) || + if (bpf_probe_read_user(&orig, sizeof(orig), (void *) (ui->sp + cbi->orig_pc_offset)) || orig < cbi->code_start || orig >= cbi->code_end) { // Just keep using the deoptimization point PC. It usually unwinds ok, and symbolizes // to the correct function. Potentially inlined scopes, and source line number is lost. @@ -591,7 +591,7 @@ ErrorCode hotspot_handle_nmethod(const CodeBlobInfo *cbi, Trace *trace, // mapping area. Additional checking could be done to search for CodeBlob and to verify that // the value is actually inside the code area and that the CodeBlob is in valid state. u64 stack[HOTSPOT_RA_SEARCH_SLOTS]; - bpf_probe_read(stack, sizeof(stack), (void*)(ui->sp - sizeof(u64))); + bpf_probe_read_user(stack, sizeof(stack), (void*)(ui->sp - sizeof(u64))); for (int i = 0; i < HOTSPOT_RA_SEARCH_SLOTS; i++, ui->sp += sizeof(u64)) { DEBUG_PRINT("jvm: -> %u pc candidate 0x%lx", i, (unsigned long)stack[i]); if (hotspot_addr_in_codecache(trace->pid, stack[i])) { @@ -660,7 +660,7 @@ ErrorCode hotspot_execute_unwind_action(CodeBlobInfo *cbi, HotspotUnwindAction a return ERR_UNREACHABLE; #if defined(__aarch64__) case UA_UNWIND_AARCH64_LR: - if (!state->lr_valid) { + if (state->return_address) { increment_metric(metricID_UnwindHotspotErrLrUnwindingMidTrace); return ERR_HOTSPOT_LR_UNWINDING_MID_TRACE; } @@ -682,7 +682,7 @@ ErrorCode hotspot_execute_unwind_action(CodeBlobInfo *cbi, HotspotUnwindAction a // fallthrough case UA_UNWIND_REGS: { u64 frame[2]; - bpf_probe_read(frame, sizeof(frame), (void *) (ui->sp - sizeof(frame))); + bpf_probe_read_user(frame, sizeof(frame), (void *) (ui->sp - sizeof(frame))); ui->pc = frame[1]; if (cbi->frame_size >= sizeof(frame)) { DEBUG_PRINT("jvm: -> recover fp"); @@ -692,7 +692,7 @@ ErrorCode hotspot_execute_unwind_action(CodeBlobInfo *cbi, HotspotUnwindAction a case UA_UNWIND_COMPLETE: { unwind_complete:; u64 line = calc_line(ui->line.subtype, ui->line.pc_delta_or_bci, ui->line.ptr_check); - ErrorCode error = push_hotspot(trace, ui->file, line); + ErrorCode error = push_hotspot(trace, ui->file, line, state->return_address); if (error) { return error; } @@ -702,9 +702,7 @@ ErrorCode hotspot_execute_unwind_action(CodeBlobInfo *cbi, HotspotUnwindAction a state->pc = ui->pc; state->sp = ui->sp; state->fp = ui->fp; -#if defined(__aarch64__) - state->lr_valid = false; -#endif + state->return_address = true; increment_metric(metricID_UnwindHotspotFrames); } } @@ -730,7 +728,7 @@ ErrorCode hotspot_read_codeblob(const UnwindState *state, const HotspotProcInfo // the exact CodeBlob/CompiledMethod/nmethod size. The CodeBlob is allocated in the JIT area, // preceding the actual JIT code and data for the function. It is thus exceedingly unlikely for // us to accidentally read into a guard / unallocated page despite the over-read. - if (bpf_probe_read(scratch->codeblob, sizeof(scratch->codeblob), (void*)cbi->address)) { + if (bpf_probe_read_user(scratch->codeblob, sizeof(scratch->codeblob), (void*)cbi->address)) { goto read_error_exit; } @@ -753,7 +751,7 @@ ErrorCode hotspot_read_codeblob(const UnwindState *state, const HotspotProcInfo // `frame_type` is actually the first 4 characters of the CodeBlob type name. u64 code_name_addr = *(u64*)(scratch->codeblob + ji->codeblob_name); - if (bpf_probe_read(&cbi->frame_type, sizeof(cbi->frame_type), (void*)code_name_addr)) { + if (bpf_probe_read_user(&cbi->frame_type, sizeof(cbi->frame_type), (void*)code_name_addr)) { goto read_error_exit; } diff --git a/support/ebpf/integration_test.ebpf.c b/support/ebpf/integration_test.ebpf.c index 3e85e33d..eae731da 100644 --- a/support/ebpf/integration_test.ebpf.c +++ b/support/ebpf/integration_test.ebpf.c @@ -1,35 +1,95 @@ // This file contains the code and map definitions that are used in integration tests only. #include "bpfdefs.h" +#include "frametypes.h" +#include "types.h" +#include "extmaps.h" +#include "tracemgmt.h" extern bpf_map_def kernel_stackmap; -// kernel_stack_array is used to communicate the kernel stack id to the userspace part of the -// integration test. -bpf_map_def SEC("maps") kernel_stack_array = { - .type = BPF_MAP_TYPE_ARRAY, - .key_size = sizeof(u32), - .value_size = sizeof(s32), - .max_entries = 1, -}; +static inline __attribute__((__always_inline__)) +void send_sample_traces(void *ctx, u64 pid, s32 kstack) { + // Use the per CPU record for trace storage: it's too big for stack. + PerCPURecord *record = get_pristine_per_cpu_record(); + if (!record) { + return; // unreachable + } + + // Single native frame, no kernel trace. + Trace *trace = &record->trace; + + // Use COMM as a marker for our test traces. COMM[3] serves as test case ID. + trace->comm[0] = 0xAA; + trace->comm[1] = 0xBB; + trace->comm[2] = 0xCC; + trace->comm[3] = 1; + trace->pid = pid; + trace->kernel_stack_id = -1, + trace->stack_len = 1; + trace->frames[0] = (Frame) { + .kind = FRAME_MARKER_NATIVE, + .file_id = 1337, + .addr_or_line = 21, + }; + send_trace(ctx, trace); + + // Single native frame, with kernel trace. + trace->comm[3] = 2; + trace->kernel_stack_id = kstack; + send_trace(ctx, trace); + + // Single Python frame. + trace->comm[3] = 3; + trace->kernel_stack_id = -1; + trace->stack_len = 3; + trace->frames[0] = (Frame) { + .kind = FRAME_MARKER_NATIVE, + .file_id = 1337, + .addr_or_line = 42, + }; + trace->frames[1] = (Frame) { + .kind = FRAME_MARKER_NATIVE, + .file_id = 1338, + .addr_or_line = 21, + }; + trace->frames[2] = (Frame) { + .kind = FRAME_MARKER_PYTHON, + .file_id = 1339, + .addr_or_line = 22, + }; + send_trace(ctx, trace); + + // Maximum length native trace. + trace->comm[3] = 4; + trace->stack_len = MAX_FRAME_UNWINDS; + trace->kernel_stack_id = kstack; +#pragma unroll + for (u64 i = 0; i < MAX_FRAME_UNWINDS; ++i) { + // NOTE: this init schema eats up a lot of instructions. If we need more + // space later, we can instead just init `.kind` and a few fields in the + // start, middle, and end of the trace. + trace->frames[i] = (Frame) { + .kind = FRAME_MARKER_NATIVE, + .file_id = ~i, + .addr_or_line = i, + }; + } + send_trace(ctx, trace); +} // tracepoint__sched_switch fetches the current kernel stack ID from kernel_stackmap and // communicates it to userspace via kernel_stack_id map. SEC("tracepoint/sched/sched_switch") int tracepoint__sched_switch(void *ctx) { - u32 key0 = 0; u64 id = bpf_get_current_pid_tgid(); u64 pid = id >> 32; - s32 kernel_stack_id = bpf_get_stackid(ctx, &kernel_stackmap, BPF_F_REUSE_STACKID); - printt("pid %lld with kernel_stack_id %d", pid, kernel_stack_id); - if (bpf_map_update_elem(&kernel_stack_array, &key0, &kernel_stack_id, BPF_ANY)) { - return -1; - } + send_sample_traces(ctx, pid, kernel_stack_id); return 0; } diff --git a/support/ebpf/interpreter_dispatcher.ebpf.c b/support/ebpf/interpreter_dispatcher.ebpf.c index ec3c5ba2..e2c062d7 100644 --- a/support/ebpf/interpreter_dispatcher.ebpf.c +++ b/support/ebpf/interpreter_dispatcher.ebpf.c @@ -5,6 +5,7 @@ #include "bpfdefs.h" #include "types.h" #include "tracemgmt.h" +#include "tsd.h" // Begin shared maps @@ -49,12 +50,11 @@ bpf_map_def SEC("maps") report_events = { // reported_pids is a map that holds PIDs recently reported to user space. // // We use this map to avoid sending multiple notifications for the same PID to user space. -// As key, we use the PID and as value the timestamp of the moment we write into -// this map. When sizing this map, we are thinking about the maximum number of unique PIDs -// that could be stored, without immediately being removed, that we would like to support. -// PIDs are either left to expire from the LRU or manually overwritten through a timeout -// check via REPORTED_PIDS_TIMEOUT. Note that timeout checks are done lazily on map access, -// in report_pid, so this map may at any time contain multiple expired PIDs. +// As key, we use the PID and value is a rate limit token (see pid_event_ratelimit()). +// When sizing this map, we are thinking about the maximum number of unique PIDs that could +// be stored, without immediately being removed, that we would like to support. PIDs are +// either left to expire from the LRU or updated based on the rate limit token. Note that +// timeout checks are done lazily on access, so this map may contain multiple expired PIDs. bpf_map_def SEC("maps") reported_pids = { .type = BPF_MAP_TYPE_LRU_HASH, .key_size = sizeof(u32), @@ -118,6 +118,60 @@ bpf_map_def SEC("maps") trace_events = { // End shared maps +bpf_map_def SEC("maps") apm_int_procs = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(pid_t), + .value_size = sizeof(ApmIntProcInfo), + .max_entries = 128, +}; + +static inline __attribute__((__always_inline__)) +void maybe_add_apm_info(Trace *trace) { + u32 pid = trace->pid; // verifier needs this to be on stack on 4.15 kernel + ApmIntProcInfo *proc = bpf_map_lookup_elem(&apm_int_procs, &pid); + if (!proc) { + return; + } + + DEBUG_PRINT("Trace is within a process with APM integration enabled"); + + u64 tsd_base; + if (tsd_get_base((void **)&tsd_base) != 0) { + increment_metric(metricID_UnwindApmIntErrReadTsdBase); + DEBUG_PRINT("Failed to get TSD base for APM integration"); + return; + } + + DEBUG_PRINT("APM corr ptr should be at 0x%llx", tsd_base + proc->tls_offset); + + void *apm_corr_buf_ptr; + if (bpf_probe_read_user(&apm_corr_buf_ptr, sizeof(apm_corr_buf_ptr), + (void *)(tsd_base + proc->tls_offset))) { + increment_metric(metricID_UnwindApmIntErrReadCorrBufPtr); + DEBUG_PRINT("Failed to read APM correlation buffer pointer"); + return; + } + + ApmCorrelationBuf corr_buf; + if (bpf_probe_read_user(&corr_buf, sizeof(corr_buf), apm_corr_buf_ptr)) { + increment_metric(metricID_UnwindApmIntErrReadCorrBuf); + DEBUG_PRINT("Failed to read APM correlation buffer"); + return; + } + + if (corr_buf.trace_present && corr_buf.valid) { + trace->apm_trace_id.as_int.hi = corr_buf.trace_id.as_int.hi; + trace->apm_trace_id.as_int.lo = corr_buf.trace_id.as_int.lo; + trace->apm_transaction_id.as_int = corr_buf.transaction_id.as_int; + } + + increment_metric(metricID_UnwindApmIntReadSuccesses); + + // WARN: we print this as little endian + DEBUG_PRINT("APM transaction ID: %016llX, flags: 0x%02X", + trace->apm_transaction_id.as_int, corr_buf.trace_flags); +} + SEC("perf_event/unwind_stop") int unwind_stop(struct pt_regs *ctx) { PerCPURecord *record = get_per_cpu_record(); @@ -126,6 +180,8 @@ int unwind_stop(struct pt_regs *ctx) { Trace *trace = &record->trace; UnwindState *state = &record->state; + maybe_add_apm_info(trace); + // If the stack is otherwise empty, push an error for that: we should // never encounter empty stacks for successful unwinding. if (trace->stack_len == 0 && trace->kernel_stack_id < 0) { @@ -146,7 +202,7 @@ int unwind_stop(struct pt_regs *ctx) { // No Error break; case metricID_UnwindNativeErrWrongTextSection:; - if (report_pid(ctx, trace->pid, true)) { + if (report_pid(ctx, trace->pid, record->ratelimitAction)) { increment_metric(metricID_NumUnknownPC); } // Fallthrough to report the error diff --git a/support/ebpf/kernel.h b/support/ebpf/kernel.h index 7978efb6..a3f4c6b8 100644 --- a/support/ebpf/kernel.h +++ b/support/ebpf/kernel.h @@ -422,7 +422,4 @@ enum { // defined in include/uapi/linux/perf_event.h #define PERF_MAX_STACK_DEPTH 127 -// taken from the kernel sources -#define THREAD_SIZE 16384 - #endif // OPTI_KERNEL_H diff --git a/support/ebpf/native_stack_trace.ebpf.c b/support/ebpf/native_stack_trace.ebpf.c index 6082d09b..e282965e 100644 --- a/support/ebpf/native_stack_trace.ebpf.c +++ b/support/ebpf/native_stack_trace.ebpf.c @@ -4,6 +4,14 @@ #include "tracemgmt.h" #include "stackdeltatypes.h" +#ifndef __USER32_CS + // defined in arch/x86/include/asm/segment.h + #define GDT_ENTRY_DEFAULT_USER32_CS 4 + #define GDT_ENTRY_DEFAULT_USER_DS 5 + #define __USER32_CS (GDT_ENTRY_DEFAULT_USER32_CS*8 + 3) + #define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS*8 + 3) +#endif + // Macro to create a map named exe_id_to_X_stack_deltas that is a nested maps with a fileID for the // outer map and an array as inner map that holds up to 2^X stack delta entries for the given fileID. #define STACK_DELTA_BUCKET(X) \ @@ -78,21 +86,10 @@ bpf_map_def SEC("maps") kernel_stackmap = { .max_entries = 16*1024, }; -#if defined(__aarch64__) -// This contains the cached value of the pt_regs size structure as established by the -// get_arm64_ptregs_size function -struct bpf_map_def SEC("maps") ptregs_size = { - .type = BPF_MAP_TYPE_ARRAY, - .key_size = sizeof(u32), - .value_size = sizeof(u64), - .max_entries = 1, -}; -#endif - // Record a native frame static inline __attribute__((__always_inline__)) -ErrorCode push_native(Trace *trace, u64 file, u64 line) { - return _push(trace, file, line, FRAME_MARKER_NATIVE); +ErrorCode push_native(Trace *trace, u64 file, u64 line, bool return_address) { + return _push_with_return_address(trace, file, line, FRAME_MARKER_NATIVE, return_address); } #ifdef __aarch64__ @@ -308,6 +305,28 @@ u64 unwind_register_address(UnwindState *state, u64 cfa, u8 opcode, s32 param) { } return state->lr; +#endif +#if defined(__x86_64__) + case UNWIND_OPCODE_BASE_REG: + val = (param & ~UNWIND_REG_MASK) >> 1; + DEBUG_PRINT("unwind: r%d+%lu", param & UNWIND_REG_MASK, val); + switch (param & UNWIND_REG_MASK) { + case 0: // rax + addr = state->rax; + break; + case 9: // r9 + addr = state->r9; + break; + case 11: // r11 + addr = state->r11; + break; + case 15: // r15 + addr = state->r15; + break; + default: + return 0; + } + return addr + val; #endif default: return 0; @@ -344,7 +363,7 @@ u64 unwind_register_address(UnwindState *state, u64 cfa, u8 opcode, s32 param) { } // Dereference, and add the postDereference adder. - if (bpf_probe_read(&val, sizeof(val), (void*) addr)) { + if (bpf_probe_read_user(&val, sizeof(val), (void*) addr)) { DEBUG_PRINT("unwind failed to dereference address 0x%lx", addr); return 0; } @@ -399,13 +418,19 @@ static ErrorCode unwind_one_frame(u64 pid, u32 frame_idx, UnwindState *state, bo // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/include/asm/sigframe.h?h=v6.4#n59 // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/include/uapi/asm/sigcontext.h?h=v6.4#n238 // offsetof(struct rt_sigframe, uc.uc_mcontext) = 40 - if (bpf_probe_read(&rt_regs, sizeof(rt_regs), (void*)(state->sp + 40))) { + if (bpf_probe_read_user(&rt_regs, sizeof(rt_regs), (void*)(state->sp + 40))) { goto err_native_pc_read; } + state->rax = rt_regs[13]; + state->r9 = rt_regs[1]; + state->r11 = rt_regs[3]; state->r13 = rt_regs[5]; + state->r15 = rt_regs[7]; state->fp = rt_regs[10]; state->sp = rt_regs[15]; state->pc = rt_regs[16]; + state->return_address = false; + DEBUG_PRINT("signal frame"); goto frame_ok; case UNWIND_COMMAND_STOP: *stop = true; @@ -435,19 +460,20 @@ static ErrorCode unwind_one_frame(u64 pid, u32 frame_idx, UnwindState *state, bo u64 fpa = unwind_register_address(state, cfa, info->fpOpcode, info->fpParam); if (fpa) { - bpf_probe_read(&state->fp, sizeof(state->fp), (void*)fpa); + bpf_probe_read_user(&state->fp, sizeof(state->fp), (void*)fpa); } else if (info->opcode == UNWIND_OPCODE_BASE_FP) { // FP used for recovery, but no new FP value received, clear FP state->fp = 0; } } - if (!cfa || bpf_probe_read(&state->pc, sizeof(state->pc), (void*)(cfa - 8))) { + if (!cfa || bpf_probe_read_user(&state->pc, sizeof(state->pc), (void*)(cfa - 8))) { err_native_pc_read: increment_metric(metricID_UnwindNativeErrPCRead); return ERR_NATIVE_PC_READ; } state->sp = cfa; + state->return_address = true; frame_ok: increment_metric(metricID_UnwindNativeFrames); return ERR_OK; @@ -479,7 +505,7 @@ static ErrorCode unwind_one_frame(u64 pid, u32 frame_idx, struct UnwindState *st // offsetof(struct rt_sigframe, uc) 128 + // offsetof(struct ucontext, uc_mcontext) 176 + // offsetof(struct sigcontext, regs[0]) 8 - if (bpf_probe_read(&rt_regs, sizeof(rt_regs), (void*)(state->sp + 312))) { + if (bpf_probe_read_user(&rt_regs, sizeof(rt_regs), (void*)(state->sp + 312))) { goto err_native_pc_read; } state->pc = normalize_pac_ptr(rt_regs[32]); @@ -487,7 +513,8 @@ static ErrorCode unwind_one_frame(u64 pid, u32 frame_idx, struct UnwindState *st state->fp = rt_regs[29]; state->lr = normalize_pac_ptr(rt_regs[30]); state->r22 = rt_regs[22]; - state->lr_valid = true; + state->return_address = false; + DEBUG_PRINT("signal frame"); goto frame_ok; case UNWIND_COMMAND_STOP: *stop = true; @@ -523,7 +550,7 @@ static ErrorCode unwind_one_frame(u64 pid, u32 frame_idx, struct UnwindState *st if (info->fpOpcode == UNWIND_OPCODE_BASE_LR) { // Allow LR unwinding only if it's known to be valid: either because // it's the topmost user-mode frame, or recovered by signal trampoline. - if (!state->lr_valid) { + if (state->return_address) { increment_metric(metricID_UnwindNativeErrLrUnwindingMidTrace); return ERR_NATIVE_LR_UNWINDING_MID_TRACE; } @@ -534,7 +561,7 @@ static ErrorCode unwind_one_frame(u64 pid, u32 frame_idx, struct UnwindState *st DEBUG_PRINT("RA: %016llX", (u64)ra); // read the value of RA from stack - if (bpf_probe_read(&state->pc, sizeof(state->pc), (void*)ra)) { + if (bpf_probe_read_user(&state->pc, sizeof(state->pc), (void*)ra)) { // error reading memory, mark RA as invalid ra = 0; } @@ -563,12 +590,12 @@ static ErrorCode unwind_one_frame(u64 pid, u32 frame_idx, struct UnwindState *st // we can assume the presence of frame pointers if (info->fpOpcode != UNWIND_OPCODE_BASE_LR) { // FP precedes the RA on the stack (Aarch64 ABI requirement) - bpf_probe_read(&state->fp, sizeof(state->fp), (void*)(ra - 8)); + bpf_probe_read_user(&state->fp, sizeof(state->fp), (void*)(ra - 8)); } } state->sp = cfa; - state->lr_valid = false; + state->return_address = true; frame_ok: increment_metric(metricID_UnwindNativeFrames); return ERR_OK; @@ -578,173 +605,150 @@ static ErrorCode unwind_one_frame(u64 pid, u32 frame_idx, struct UnwindState *st #endif // Initialize state from pt_regs -static inline void copy_state_regs(UnwindState *state, struct pt_regs *regs) +static inline ErrorCode copy_state_regs(UnwindState *state, + struct pt_regs *regs, + bool interrupted_kernelmode) { #if defined(__x86_64__) + // Check if the process is running in 32-bit mode on the x86_64 system. + // This check follows the Linux kernel implementation of user_64bit_mode() in + // arch/x86/include/asm/ptrace.h. + if (regs->cs == __USER32_CS) { + return ERR_NATIVE_X64_32BIT_COMPAT_MODE; + } state->pc = regs->ip; state->sp = regs->sp; state->fp = regs->bp; + state->rax = regs->ax; + state->r9 = regs->r9; + state->r11 = regs->r11; state->r13 = regs->r13; + state->r15 = regs->r15; + + // Treat syscalls as return addresses, but not IRQ handling, page faults, etc.. + // https://github.com/torvalds/linux/blob/2ef5971ff3/arch/x86/include/asm/syscall.h#L31-L39 + // https://github.com/torvalds/linux/blob/2ef5971ff3/arch/x86/entry/entry_64.S#L847 + state->return_address = interrupted_kernelmode && regs->orig_ax != -1; #elif defined(__aarch64__) + // For backwards compatability aarch64 can run 32-bit code. + // Check if the process is running in this 32-bit compat mod. + if (regs->pstate & PSR_MODE32_BIT) { + return ERR_NATIVE_AARCH64_32BIT_COMPAT_MODE; + } state->pc = normalize_pac_ptr(regs->pc); state->sp = regs->sp; state->fp = regs->regs[29]; state->lr = normalize_pac_ptr(regs->regs[30]); state->r22 = regs->regs[22]; - state->lr_valid = true; -#endif -} - -#if defined(__aarch64__) -// on ARM64, the size of pt_regs structure is not constant across kernel -// versions as in x86-64 platform (!) -// get_arm64_ptregs_size tries to find out the size of this structure with the -// help of a simple heuristic, this size is needed for locating user process -// registers on kernel stack -static inline u64 get_arm64_ptregs_size(u64 stack_top) { - // this var should be static, but verifier complains, in the meantime - // just leave it here - u32 key0 = 0; - u64 pc, sp; - - u64 *paddr = bpf_map_lookup_elem(&ptregs_size, &key0); - if (!paddr) { - DEBUG_PRINT("Failed to look up ptregs_size map"); - return -1; - } - - // read current (possibly cached) value of pt_regs structure size - u64 arm64_ptregs_size = *paddr; - - // if the size of pt_regs has been already established, just return it - if (arm64_ptregs_size) return arm64_ptregs_size; - - // assume default pt_regs structure size as for kernel 4.19.120 - arm64_ptregs_size = sizeof(struct pt_regs); - // the candidate addr where pt_regs structure may start - u64 ptregs_candidate_addr = stack_top - arm64_ptregs_size; - - struct pt_regs *regs = (struct pt_regs*)ptregs_candidate_addr; - - // read the value of pc and sp registers - if (bpf_probe_read(&pc, sizeof(pc), ®s->pc) || - bpf_probe_read(&sp, sizeof(sp), ®s->sp)) { - goto exit; - } + // Treat syscalls as return addresses, but not IRQ handling, page faults, etc.. + // https://github.com/torvalds/linux/blob/2ef5971ff3/arch/arm64/include/asm/ptrace.h#L118 + // https://github.com/torvalds/linux/blob/2ef5971ff3/arch/arm64/include/asm/ptrace.h#L206-L209 + state->return_address = interrupted_kernelmode && regs->syscallno != -1; +#endif - // if pc and sp are kernel pointers, we may assume correct candidate - // addr of pt_regs structure (as seen 4.19.120 and 5.4.38 kernels) - if (is_kernel_address(pc) && is_kernel_address(sp)) { - DEBUG_PRINT("default pt_regs struct size"); - goto exit; - } + return ERR_OK; +} - // this is likely pt_regs structure as present in kernel 5.10.0-7 - // with two extra fields in pt_regs: - // u64 lockdep_hardirqs; - // u64 exit_rcu; - // Adjust arm64_ptregs_size for that - DEBUG_PRINT("adjusted pt_regs struct size"); - arm64_ptregs_size += 2*sizeof(u64); +#ifndef TESTING_COREDUMP -exit: - // update the map (cache the value) - if (!bpf_map_update_elem(&ptregs_size, &key0, &arm64_ptregs_size, BPF_ANY)) { - DEBUG_PRINT("Failed to update ptregs_size map"); +// Read the task's entry stack pt_regs. This has identical functionality +// to bpf_task_pt_regs which is emulated to support older kernels. +// Once kernel requirement is increased to 5.15 this can be replaced with +// the bpf_task_pt_regs() helper. +static inline +long get_task_pt_regs(struct task_struct *task, SystemConfig* syscfg) { + u64 stack_ptr = (u64)task + syscfg->task_stack_offset; + long stack_base; + if (bpf_probe_read_kernel(&stack_base, sizeof(stack_base), (void*) stack_ptr)) { + return 0; } - - return arm64_ptregs_size; + return stack_base + syscfg->stack_ptregs_offset; } -#endif -// Convert kernel stack pointer to pt_regs pointers at the top-of-stack. -static inline void *get_kernel_stack_ptregs(u64 addr) -{ +// Determine whether the given pt_regs are from user-mode register context. +// This needs to detect also invalid pt_regs in case we its kernel thread stack +// without valid user mode pt_regs so is_kernel_address(pc) is not enough. +static inline +bool ptregs_is_usermode(struct pt_regs *regs) { #if defined(__x86_64__) - // Fortunately on x86_64 IRQ_STACK_SIZE = THREAD_SIZE. Both depend on - // CONFIG_KASAN, but that can be assumed off on all production systems. - // The thread kernel stack should be aligned at least to THREAD_SIZE so the - // below calculation should yield correct end of stack. - u64 stack_end = (addr | (THREAD_SIZE - 1)) + 1; - return (void*)(stack_end - sizeof(struct pt_regs)); + // On x86_64 the user mode SS should always be __USER_DS. + if (regs->ss != __USER_DS) { + return false; + } + return true; #elif defined(__aarch64__) - u64 stack_end = (addr | (THREAD_SIZE - 1)) + 1; - - u64 ptregs_size = get_arm64_ptregs_size(stack_end); - - return (void*)(stack_end - ptregs_size); + // Check if the processor state is in the EL0t what linux uses for usermode. + if ((regs->pstate & PSR_MODE_MASK) != PSR_MODE_EL0t) { + return false; + } + return true; +#else +#error add support for new architecture #endif } -// Extract the usermode pt_regs for given context. +// Extract the usermode pt_regs for current task. Use context given pt_regs +// if it is usermode regs, or resolve it via struct task_struct. // // State registers are not touched (get_pristine_per_cpu_record already reset it) -// if something fails. has_usermode_regs receives a boolean indicating whether a -// user-mode register context was found: not every thread that we interrupt will -// actually have a user-mode context (e.g. kernel worker threads won't). +// if something fails. has_usermode_regs is set to true if a user-mode register +// context was found: not every thread that we interrupt will actually have +// a user-mode context (e.g. kernel worker threads won't). static inline ErrorCode get_usermode_regs(struct pt_regs *ctx, UnwindState *state, bool *has_usermode_regs) { - if (is_kernel_address(ctx->sp)) { - // We are in kernel mode stack. There are several different kind kernel - // stacks. See get_stack_info() in arch/x86/kernel/dumpstack_64.c - // 1) Exception stack. We don't expect to see these. - // 2) IRQ stack. Used for hard and softirqs. We can see them in softirq - // context. Top-of-stack contains pointer to previous stack (always kernel). - // Size on x86_64 is IRQ_STACK_SIZE. - // 3) Entry stack. This used for kernel code when application does syscall. - // Top-of-stack contains user mode pt_regs. Size is THREAD_SIZE. - // - // We expect to be on Entry stack, or inside one level IRQ stack which was - // triggered by executing softirq work on hardirq stack. - // - // To optimize code, we always read full pt_regs from the top of kernel stack. - // The last word of pt_regs is 'ss' which can be used to distinguish if we - // are on IRQ stack (it's actually the link pointer to previous stack) or - // entry stack (real SS) depending on if looks like real descriptor. + ErrorCode error; + + if (!ptregs_is_usermode(ctx)) { + u32 key = 0; + SystemConfig* syscfg = bpf_map_lookup_elem(&system_config, &key); + if (!syscfg) { + // Unreachable: array maps are always fully initialized. + return ERR_UNREACHABLE; + } + + // Use the current task's entry pt_regs + struct task_struct *task = (struct task_struct *) bpf_get_current_task(); + long ptregs_addr = get_task_pt_regs(task, syscfg); + struct pt_regs regs; - if (bpf_probe_read(®s, sizeof(regs), get_kernel_stack_ptregs(ctx->sp))) { + if (!ptregs_addr || bpf_probe_read_kernel(®s, sizeof(regs), (void*) ptregs_addr)) { increment_metric(metricID_UnwindNativeErrReadKernelModeRegs); return ERR_NATIVE_READ_KERNELMODE_REGS; } -#if defined(__x86_64__) - if (is_kernel_address(regs.ss)) { - // ss looks like kernel address. It must be the IRQ stack's link to previous - // kernel stack. In our case it should be the kernel Entry stack. - DEBUG_PRINT("Chasing IRQ stack link, ss=0x%lx", regs.ss); - if (bpf_probe_read(®s, sizeof(regs), get_kernel_stack_ptregs(regs.ss))) { - increment_metric(metricID_UnwindNativeErrChaseIrqStackLink); - return ERR_NATIVE_CHASE_IRQ_STACK_LINK; - } - } - // On x86_64 the user mode SS is practically always 0x2B. But this allows - // for some flexibility. Expect RPL (requested privilege level) to be - // user mode (3), and to be under max GDT value of 16. - if ((regs.ss & 3) != 3 || regs.ss >= 16*8) { - DEBUG_PRINT("No user mode stack, ss=0x%lx", regs.ss); - *has_usermode_regs = false; + + if (!ptregs_is_usermode(®s)) { + // No usermode registers context found. return ERR_OK; } - DEBUG_PRINT("Kernel mode regs (ss=0x%lx)", regs.ss); -#elif defined(__aarch64__) - // For backwards compatability aarch64 can run 32-bit code. Check if the process - // is running in this 32-bit compat mod. - if ((regs.pstate & (PSR_MODE32_BIT | PSR_MODE_MASK)) == (PSR_MODE32_BIT | PSR_MODE_EL0t)) { - return ERR_NATIVE_AARCH64_32BIT_COMPAT_MODE; - } -#endif - copy_state_regs(state, ®s); + error = copy_state_regs(state, ®s, true); } else { // User mode code interrupted, registers are available via the ebpf context. - copy_state_regs(state, ctx); + error = copy_state_regs(state, ctx, false); + } + if (error == ERR_OK) { + DEBUG_PRINT("Read regs: pc: %llx sp: %llx fp: %llx", state->pc, state->sp, state->fp); + *has_usermode_regs = true; } + return error; +} - DEBUG_PRINT("Read regs: pc: %llx sp: %llx fp: %llx", state->pc, state->sp, state->fp); - *has_usermode_regs = true; - return ERR_OK; +#else // TESTING_COREDUMP + +static inline ErrorCode get_usermode_regs(struct pt_regs *ctx, + UnwindState *state, + bool *has_usermode_regs) { + // Coredumps provide always usermode pt_regs directly. + ErrorCode error = copy_state_regs(state, ctx, false); + if (error == ERR_OK) { + *has_usermode_regs = true; + } + return error; } +#endif + SEC("perf_event/unwind_native") int unwind_native(struct pt_regs *ctx) { PerCPURecord *record = get_per_cpu_record(); @@ -767,7 +771,8 @@ int unwind_native(struct pt_regs *ctx) { DEBUG_PRINT("Pushing %llx %llx to position %u on stack", record->state.text_section_id, record->state.text_section_offset, trace->stack_len); - error = push_native(trace, record->state.text_section_id, record->state.text_section_offset); + error = push_native(trace, record->state.text_section_id, record->state.text_section_offset, + record->state.return_address); if (error) { DEBUG_PRINT("failed to push native frame"); break; @@ -806,6 +811,8 @@ int collect_trace(struct pt_regs *ctx) { return 0; } + u64 ktime = bpf_ktime_get_ns(); + DEBUG_PRINT("==== do_perf_event ===="); // The trace is reused on each call to this function so we have to reset the @@ -818,7 +825,7 @@ int collect_trace(struct pt_regs *ctx) { Trace *trace = &record->trace; trace->pid = pid; - trace->ktime = bpf_ktime_get_ns(); + trace->ktime = ktime; if (bpf_get_current_comm(&(trace->comm), sizeof(trace->comm)) < 0) { increment_metric(metricID_ErrBPFCurrentComm); } @@ -829,15 +836,14 @@ int collect_trace(struct pt_regs *ctx) { // Recursive unwind frames int unwinder = PROG_UNWIND_STOP; - bool has_usermode_regs; + bool has_usermode_regs = false; ErrorCode error = get_usermode_regs(ctx, &record->state, &has_usermode_regs); - if (error || !has_usermode_regs) { goto exit; } if (!pid_information_exists(ctx, pid)) { - if (report_pid(ctx, pid, true)) { + if (report_pid(ctx, pid, RATELIMIT_ACTION_DEFAULT)) { increment_metric(metricID_NumProcNew); } return 0; diff --git a/support/ebpf/perl_tracer.ebpf.c b/support/ebpf/perl_tracer.ebpf.c index 6c3b333c..c498341b 100644 --- a/support/ebpf/perl_tracer.ebpf.c +++ b/support/ebpf/perl_tracer.ebpf.c @@ -89,7 +89,7 @@ static inline __attribute__((__always_inline__)) void *resolve_cv_egv(const PerlProcInfo *perlinfo, const void *cv) { // First check the CV's type u32 cv_flags; - if (bpf_probe_read(&cv_flags, sizeof(cv_flags), cv + perlinfo->sv_flags)) { + if (bpf_probe_read_user(&cv_flags, sizeof(cv_flags), cv + perlinfo->sv_flags)) { goto err; } @@ -100,12 +100,12 @@ void *resolve_cv_egv(const PerlProcInfo *perlinfo, const void *cv) { // Follow the any pointer for the XPVCV body void *xpvcv; - if (bpf_probe_read(&xpvcv, sizeof(xpvcv), cv + perlinfo->sv_any)) { + if (bpf_probe_read_user(&xpvcv, sizeof(xpvcv), cv + perlinfo->sv_any)) { goto err; } u32 xcv_flags; - if (bpf_probe_read(&xcv_flags, sizeof(xcv_flags), xpvcv + perlinfo->xcv_flags)) { + if (bpf_probe_read_user(&xcv_flags, sizeof(xcv_flags), xpvcv + perlinfo->xcv_flags)) { goto err; } @@ -120,7 +120,7 @@ void *resolve_cv_egv(const PerlProcInfo *perlinfo, const void *cv) { // At this point we have CV with GV (symbol). This is expected of all seen CVs // inside the Context Stack. void *gv; - if (bpf_probe_read(&gv, sizeof(gv), xpvcv + perlinfo->xcv_gv) || + if (bpf_probe_read_user(&gv, sizeof(gv), xpvcv + perlinfo->xcv_gv) || !gv) { goto err; } @@ -129,7 +129,7 @@ void *resolve_cv_egv(const PerlProcInfo *perlinfo, const void *cv) { // Make sure we read GV with a GP u32 gv_flags; - if (bpf_probe_read(&gv_flags, sizeof(gv_flags), gv + perlinfo->sv_flags)) { + if (bpf_probe_read_user(&gv_flags, sizeof(gv_flags), gv + perlinfo->sv_flags)) { goto err; } @@ -142,13 +142,13 @@ void *resolve_cv_egv(const PerlProcInfo *perlinfo, const void *cv) { // Follow GP pointer void *gp; - if (bpf_probe_read(&gp, sizeof(gp), gv + perlinfo->svu_gp)) { + if (bpf_probe_read_user(&gp, sizeof(gp), gv + perlinfo->svu_gp)) { goto err; } // Read the Effective GV (EGV) from the GP to be reported for HA void *egv; - if (bpf_probe_read(&egv, sizeof(egv), gp + perlinfo->gp_egv)) { + if (bpf_probe_read_user(&egv, sizeof(egv), gp + perlinfo->gp_egv)) { goto err; } @@ -159,7 +159,7 @@ void *resolve_cv_egv(const PerlProcInfo *perlinfo, const void *cv) { return gv; err: - DEBUG_PRINT("Bad bpf_probe_read() in resolve_cv_egv"); + DEBUG_PRINT("Bad bpf_probe_read_user() in resolve_cv_egv"); increment_metric(metricID_UnwindPerlResolveEGV); return 0; } @@ -173,7 +173,7 @@ int process_perl_frame(PerCPURecord *record, const PerlProcInfo *perlinfo, const // context entries. Others are non-functions, or helper entries. // https://github.com/Perl/perl5/blob/v5.32.0/pp_ctl.c#L1432-L1462 u8 type; - if (bpf_probe_read(&type, sizeof(type), cx + perlinfo->context_type)) { + if (bpf_probe_read_user(&type, sizeof(type), cx + perlinfo->context_type)) { goto err; } @@ -205,7 +205,7 @@ int process_perl_frame(PerCPURecord *record, const PerlProcInfo *perlinfo, const // return to the next native frame: // https://github.com/Perl/perl5/blob/v5.32.0/run.c#L41 u64 retop; - if (bpf_probe_read(&retop, sizeof(retop), cx + perlinfo->context_blk_sub_retop)) { + if (bpf_probe_read_user(&retop, sizeof(retop), cx + perlinfo->context_blk_sub_retop)) { goto err; } if (retop == 0) { @@ -215,7 +215,7 @@ int process_perl_frame(PerCPURecord *record, const PerlProcInfo *perlinfo, const // Extract the functions Code Value for symbolization void *cv; - if (bpf_probe_read(&cv, sizeof(cv), cx + perlinfo->context_blk_sub_cv)) { + if (bpf_probe_read_user(&cv, sizeof(cv), cx + perlinfo->context_blk_sub_cv)) { goto err; } @@ -236,9 +236,9 @@ int process_perl_frame(PerCPURecord *record, const PerlProcInfo *perlinfo, const // Record the first valid COP from block contexts to determine current // line number inside the sub/format block. if (!record->perlUnwindState.cop) { - if (bpf_probe_read(&record->perlUnwindState.cop, - sizeof(record->perlUnwindState.cop), - cx + perlinfo->context_blk_oldcop)) { + if (bpf_probe_read_user(&record->perlUnwindState.cop, + sizeof(record->perlUnwindState.cop), + cx + perlinfo->context_blk_oldcop)) { goto err; } DEBUG_PRINT("COP from context stack 0x%lx", (unsigned long)record->perlUnwindState.cop); @@ -264,8 +264,8 @@ void prepare_perl_stack(PerCPURecord *record, const PerlProcInfo *perlinfo) { s32 cxix; void *cxstack; - if (bpf_probe_read(&cxstack, sizeof(cxstack), si + perlinfo->si_cxstack) || - bpf_probe_read(&cxix, sizeof(cxix), si + perlinfo->si_cxix)) { + if (bpf_probe_read_user(&cxstack, sizeof(cxstack), si + perlinfo->si_cxstack) || + bpf_probe_read_user(&cxix, sizeof(cxix), si + perlinfo->si_cxix)) { DEBUG_PRINT("Failed to read stackinfo at 0x%lx", (unsigned long)si); unwinder_mark_done(record, PROG_UNWIND_PERL); increment_metric(metricID_UnwindPerlReadStackInfo); @@ -331,9 +331,9 @@ int walk_perl_stack(PerCPURecord *record, const PerlProcInfo *perlinfo) { // the context stack. Potential stackinfos below are not part of the real // Perl call stack. s32 type = 0; - if (bpf_probe_read(&type, sizeof(type), si + perlinfo->si_type) || + if (bpf_probe_read_user(&type, sizeof(type), si + perlinfo->si_type) || type == PERLSI_MAIN || - bpf_probe_read(&si, sizeof(si), si + perlinfo->si_next) || + bpf_probe_read_user(&si, sizeof(si), si + perlinfo->si_next) || si == NULL) { // Stop walking stacks if main stack is finished, or something went wrong. DEBUG_PRINT("Perl stackinfos done"); @@ -383,13 +383,13 @@ int unwind_perl(struct pt_regs *ctx) { void *interpreter; if (perlinfo->stateInTSD) { void *tsd_base; - if (tsd_get_base(ctx, &tsd_base)) { + if (tsd_get_base(&tsd_base)) { DEBUG_PRINT("Failed to get TSD base address"); goto err_tsd; } int tsd_key; - if (bpf_probe_read(&tsd_key, sizeof(tsd_key), (void*)perlinfo->stateAddr)) { + if (bpf_probe_read_user(&tsd_key, sizeof(tsd_key), (void*)perlinfo->stateAddr)) { DEBUG_PRINT("Failed to read tsdKey from 0x%lx", (unsigned long)perlinfo->stateAddr); goto err_tsd; } @@ -406,10 +406,10 @@ int unwind_perl(struct pt_regs *ctx) { } DEBUG_PRINT("PerlInterpreter 0x%lx", (unsigned long)interpreter); - if (bpf_probe_read(&record->perlUnwindState.stackinfo, sizeof(record->perlUnwindState.stackinfo), - (void*)interpreter + perlinfo->interpreter_curstackinfo) || - bpf_probe_read(&record->perlUnwindState.cop, sizeof(record->perlUnwindState.cop), - (void*)interpreter + perlinfo->interpreter_curcop)) { + if (bpf_probe_read_user(&record->perlUnwindState.stackinfo, sizeof(record->perlUnwindState.stackinfo), + (void*)interpreter + perlinfo->interpreter_curstackinfo) || + bpf_probe_read_user(&record->perlUnwindState.cop, sizeof(record->perlUnwindState.cop), + (void*)interpreter + perlinfo->interpreter_curcop)) { DEBUG_PRINT("Failed to read interpreter state"); increment_metric(metricID_UnwindPerlReadStackInfo); goto exit; diff --git a/support/ebpf/php_tracer.ebpf.c b/support/ebpf/php_tracer.ebpf.c index 62baa7d3..3d5d5287 100644 --- a/support/ebpf/php_tracer.ebpf.c +++ b/support/ebpf/php_tracer.ebpf.c @@ -25,15 +25,6 @@ bpf_map_def SEC("maps") php_procs = { .max_entries = 1024, }; - -// Map from PHP JIT process IDs to the address range of the `dasmBuf` for that process -bpf_map_def SEC("maps") php_jit_procs = { - .type = BPF_MAP_TYPE_HASH, - .key_size = sizeof(pid_t), - .value_size = sizeof(PHPJITProcInfo), - .max_entries = 1024, -}; - // Record a PHP frame static inline __attribute__((__always_inline__)) ErrorCode push_php(Trace *trace, u64 file, u64 line, bool is_jitted) { @@ -47,28 +38,15 @@ ErrorCode push_unknown_php(Trace *trace) { return _push(trace, UNKNOWN_FILE, FUNC_TYPE_UNKNOWN, FRAME_MARKER_PHP); } -// Returns true if `func` is inside the JIT buffer and false otherwise. -static inline __attribute__((__always_inline__)) -bool is_jit_function(u64 func, PHPJITProcInfo* jitinfo) { - // Check if there is JIT introspection data available. - if (!jitinfo) { return false; } - - // To avoid verifier complains like "pointer arithmetic on PTR_TO_MAP_VALUE_OR_NULL prohibited" - // on older kernels, we use temporary variables here. - bool start = (func >= jitinfo->start); - bool end = (func < jitinfo->end); - - return start && end; -} - static inline __attribute__((__always_inline__)) -int process_php_frame(PerCPURecord *record, PHPProcInfo *phpinfo, PHPJITProcInfo* jitinfo, +int process_php_frame(PerCPURecord *record, PHPProcInfo *phpinfo, bool is_jitted, const void *execute_data, u32 *type_info) { Trace *trace = &record->trace; // Get current_execute_data->func void *zend_function; - if (bpf_probe_read(&zend_function, sizeof(void *), execute_data + phpinfo->zend_execute_data_function)) { + if (bpf_probe_read_user(&zend_function, sizeof(void *), + execute_data + phpinfo->zend_execute_data_function)) { DEBUG_PRINT("Failed to read current_execute_data->func (0x%lx)", (unsigned long) (execute_data + phpinfo->zend_execute_data_function)); return metricID_UnwindPHPErrBadZendExecuteData; @@ -85,7 +63,7 @@ int process_php_frame(PerCPURecord *record, PHPProcInfo *phpinfo, PHPJITProcInfo // Get zend_function->type u8 func_type; - if (bpf_probe_read(&func_type, sizeof(func_type), zend_function + phpinfo->zend_function_type)) { + if (bpf_probe_read_user(&func_type, sizeof(func_type), zend_function + phpinfo->zend_function_type)) { DEBUG_PRINT("Failed to read execute_data->func->type (0x%lx)", (unsigned long) zend_function); return metricID_UnwindPHPErrBadZendFunction; @@ -95,14 +73,14 @@ int process_php_frame(PerCPURecord *record, PHPProcInfo *phpinfo, PHPJITProcInfo if (func_type == ZEND_USER_FUNCTION || func_type == ZEND_EVAL_CODE) { // Get execute_data->opline void *zend_op; - if (bpf_probe_read(&zend_op, sizeof(void *), execute_data + phpinfo->zend_execute_data_opline)) { + if (bpf_probe_read_user(&zend_op, sizeof(void *), execute_data + phpinfo->zend_execute_data_opline)) { DEBUG_PRINT("Failed to read execute_data->opline (0x%lx)", (unsigned long) (execute_data + phpinfo->zend_execute_data_opline)); return metricID_UnwindPHPErrBadZendExecuteData; } // Get opline->lineno - if (bpf_probe_read(&lineno, sizeof(u32), zend_op + phpinfo->zend_op_lineno)) { + if (bpf_probe_read_user(&lineno, sizeof(u32), zend_op + phpinfo->zend_op_lineno)) { DEBUG_PRINT("Failed to read executor_globals->opline->lineno (0x%lx)", (unsigned long) (zend_op + phpinfo->zend_op_lineno)); return metricID_UnwindPHPErrBadZendOpline; @@ -110,7 +88,8 @@ int process_php_frame(PerCPURecord *record, PHPProcInfo *phpinfo, PHPJITProcInfo // Get execute_data->This.type_info. This reads into the `type_info` argument // so we can re-use it in walk_php_stack - if(bpf_probe_read(type_info, sizeof(u32), execute_data + phpinfo->zend_execute_data_this_type_info)) { + if (bpf_probe_read_user(type_info, sizeof(u32), + execute_data + phpinfo->zend_execute_data_this_type_info)) { DEBUG_PRINT("Failed to read execute_data->This.type_info (0x%lx)", (unsigned long) execute_data); return metricID_UnwindPHPErrBadZendExecuteData; @@ -122,8 +101,7 @@ int process_php_frame(PerCPURecord *record, PHPProcInfo *phpinfo, PHPJITProcInfo u64 lineno_and_type_info = ((u64)*type_info) << 32 | lineno; DEBUG_PRINT("Pushing PHP 0x%lx %u", (unsigned long) zend_function, lineno); - if (push_php(trace, (u64) zend_function, lineno_and_type_info, - is_jit_function(record->state.pc, jitinfo)) != ERR_OK) { + if (push_php(trace, (u64) zend_function, lineno_and_type_info, is_jitted) != ERR_OK) { DEBUG_PRINT("failed to push php frame"); return -1; } @@ -132,7 +110,7 @@ int process_php_frame(PerCPURecord *record, PHPProcInfo *phpinfo, PHPJITProcInfo } static inline __attribute__((__always_inline__)) -int walk_php_stack(PerCPURecord *record, PHPProcInfo *phpinfo, PHPJITProcInfo* jitinfo) { +int walk_php_stack(PerCPURecord *record, PHPProcInfo *phpinfo, bool is_jitted) { const void *execute_data = record->phpUnwindState.zend_execute_data; bool mixed_traces = get_next_unwinder_after_interpreter(record) != PROG_UNWIND_STOP; @@ -146,7 +124,7 @@ int walk_php_stack(PerCPURecord *record, PHPProcInfo *phpinfo, PHPJITProcInfo* j u32 type_info = 0; #pragma unroll for (u32 i = 0; i < FRAMES_PER_WALK_PHP_STACK; ++i) { - int metric = process_php_frame(record, phpinfo, jitinfo, execute_data, &type_info); + int metric = process_php_frame(record, phpinfo, is_jitted, execute_data, &type_info); if (metric >= 0) { increment_metric(metric); } @@ -155,8 +133,8 @@ int walk_php_stack(PerCPURecord *record, PHPProcInfo *phpinfo, PHPJITProcInfo* j } // Get current_execute_data->prev_execute_data - if (bpf_probe_read(&execute_data, sizeof(void *), - execute_data + phpinfo->zend_execute_data_prev_execute_data)) { + if (bpf_probe_read_user(&execute_data, sizeof(void *), + execute_data + phpinfo->zend_execute_data_prev_execute_data)) { DEBUG_PRINT("Failed to read current_execute_data->prev_execute_data (0x%lx)", (unsigned long) execute_data); increment_metric(metricID_UnwindPHPErrBadZendExecuteData); @@ -182,8 +160,9 @@ int walk_php_stack(PerCPURecord *record, PHPProcInfo *phpinfo, PHPJITProcInfo* j // get the next unwinder instead. // This is only necessary when it's the last function because walking the PHP // stack is enough for the other functions. - if (is_jit_function(record->state.pc, jitinfo)) { + if (is_jitted) { record->state.pc = phpinfo->jit_return_address; + record->state.return_address = false; if (resolve_unwind_mapping(record, &unwinder) != ERR_OK) { unwinder = PROG_UNWIND_STOP; } @@ -217,12 +196,18 @@ int unwind_php(struct pt_regs *ctx) { goto exit; } + // The section id and bias are zeroes if matched via JIT page mapping. + // Otherwise its the native code interpreter range match and these are + // set to the native code's values. + bool is_jitted = record->state.text_section_id == 0 && + record->state.text_section_bias == 0; + increment_metric(metricID_UnwindPHPAttempts); if (!record->phpUnwindState.zend_execute_data) { // Get executor_globals.current_execute_data - if (bpf_probe_read(&record->phpUnwindState.zend_execute_data, sizeof(void *), - (void*) phpinfo->current_execute_data)) { + if (bpf_probe_read_user(&record->phpUnwindState.zend_execute_data, sizeof(void *), + (void*) phpinfo->current_execute_data)) { DEBUG_PRINT("Failed to read executor_globals.current_execute data (0x%lx)", (unsigned long) phpinfo->current_execute_data); increment_metric(metricID_UnwindPHPErrBadCurrentExecuteData); @@ -230,12 +215,6 @@ int unwind_php(struct pt_regs *ctx) { } } - // Check whether the PHP process has an enabled JIT - PHPJITProcInfo *jitinfo = bpf_map_lookup_elem(&php_jit_procs, &pid); - if(!jitinfo) { - DEBUG_PRINT("No PHP JIT introspection data"); - } - #if defined(__aarch64__) // On ARM we need to adjust the stack pointer if we entered from JIT code // This is only a problem on ARM where the SP/FP are used for unwinding. @@ -246,7 +225,7 @@ int unwind_php(struct pt_regs *ctx) { // Given that there's no guarantess that anything pushed to the stack is useful we // simply ignore it. There may be a return address in some modes, but this is hard to detect // consistently. - if (is_jit_function(record->state.pc, jitinfo)) { + if (is_jitted) { record->state.sp = record->state.fp; } #endif @@ -254,7 +233,7 @@ int unwind_php(struct pt_regs *ctx) { DEBUG_PRINT("Building PHP stack (execute_data = 0x%lx)", (unsigned long) record->phpUnwindState.zend_execute_data); // Unwind one call stack or unrolled length, and continue - unwinder = walk_php_stack(record, phpinfo, jitinfo); + unwinder = walk_php_stack(record, phpinfo, is_jitted); exit: tail_call(ctx, unwinder); diff --git a/support/ebpf/print_instruction_count.sh b/support/ebpf/print_instruction_count.sh index a7c9e402..c1155690 100755 --- a/support/ebpf/print_instruction_count.sh +++ b/support/ebpf/print_instruction_count.sh @@ -9,7 +9,7 @@ file="$1" OBJDUMP_CMD=llvm-objdump if ! type -p "${OBJDUMP_CMD}"; then - OBJDUMP_CMD=llvm-objdump-13 + OBJDUMP_CMD=llvm-objdump-16 fi echo -e "\nInstruction counts for ${file}:\n" diff --git a/support/ebpf/python_tracer.ebpf.c b/support/ebpf/python_tracer.ebpf.c index 0f461852..d99147be 100644 --- a/support/ebpf/python_tracer.ebpf.c +++ b/support/ebpf/python_tracer.ebpf.c @@ -49,15 +49,15 @@ ErrorCode process_python_frame(PerCPURecord *record, const PyProcInfo *pyinfo, PythonUnwindScratchSpace *pss = &record->pythonUnwindScratch; // Make verifier happy for PyFrameObject offsets - if (pyinfo->PyFrameObject_f_code > sizeof(pss->frame) - sizeof(void*) || - pyinfo->PyFrameObject_f_back > sizeof(pss->frame) - sizeof(void*) || - pyinfo->PyFrameObject_f_lasti > sizeof(pss->frame) - sizeof(int) || - pyinfo->PyFrameObject_f_is_entry > sizeof(pss->frame) - sizeof(bool)) { + if (pyinfo->PyFrameObject_f_code > sizeof(pss->frame) - sizeof(void*) || + pyinfo->PyFrameObject_f_back > sizeof(pss->frame) - sizeof(void*) || + pyinfo->PyFrameObject_f_lasti > sizeof(pss->frame) - sizeof(u64) || + pyinfo->PyFrameObject_entry_member > sizeof(pss->frame) - sizeof(u8)) { return ERR_UNREACHABLE; } // Read PyFrameObject - if (bpf_probe_read(pss->frame, sizeof(pss->frame), py_frameobject)) { + if (bpf_probe_read_user(pss->frame, sizeof(pss->frame), py_frameobject)) { DEBUG_PRINT( "Failed to read PyFrameObject 0x%lx", (unsigned long) py_frameobject); @@ -82,14 +82,27 @@ ErrorCode process_python_frame(PerCPURecord *record, const PyProcInfo *pyinfo, // try to construct one below by hashing together a few fields. These fields are // selected in the *hope* that no collisions occur between code objects. - int py_f_lasti = *(int*)(&pss->frame[pyinfo->PyFrameObject_f_lasti]); + int py_f_lasti = 0; if (pyinfo->version >= 0x030b) { // With Python 3.11 the element f_lasti not only got renamed but also its - // type changed from int to uint16. - py_f_lasti &= 0xffff; - if (*(bool*)(&pss->frame[pyinfo->PyFrameObject_f_is_entry])) { + // type changed from int to a _Py_CODEUNIT* and needs to be translated to lastI. + // It is a direct pointer to the bytecode, so calculate the byte code index. + // sizeof(_Py_CODEUNIT) == 2. + // https://github.com/python/cpython/commit/ef6a482b0285870c45f39c9b17ed827362b334ae + u64 prev_instr = *(u64*)(&pss->frame[pyinfo->PyFrameObject_f_lasti]); + s64 instr_diff = (s64)prev_instr - (s64)py_codeobject - pyinfo->PyCodeObject_sizeof; + if (instr_diff < -2 || instr_diff > 0x10000000) + instr_diff = -2; + py_f_lasti = (int)instr_diff >> 1; + + // Python 3.11+ the frame object has some field that can be used to determine + // if this is the last frame in the interpreter loop. This generalized test + // works on 3.11 and 3.12 though the actual struct members are different. + if (*(u8*)(&pss->frame[pyinfo->PyFrameObject_entry_member]) == pyinfo->PyFrameObject_entry_val) { *continue_with_next = true; } + } else { + py_f_lasti = *(int*)(&pss->frame[pyinfo->PyFrameObject_f_lasti]); } if (!py_codeobject) { @@ -110,7 +123,7 @@ ErrorCode process_python_frame(PerCPURecord *record, const PyProcInfo *pyinfo, } // Read PyCodeObject - if (bpf_probe_read(pss->code, sizeof(pss->code), py_codeobject)) { + if (bpf_probe_read_user(pss->code, sizeof(pss->code), py_codeobject)) { DEBUG_PRINT( "Failed to read PyCodeObject at 0x%lx", (unsigned long) (py_codeobject)); @@ -181,7 +194,7 @@ static inline __attribute__((__always_inline__)) ErrorCode get_PyThreadState(const PyProcInfo *pyinfo, void *tsd_base, void *autoTLSkeyAddr, void **thread_state) { int key; - if (bpf_probe_read(&key, sizeof(key), autoTLSkeyAddr)) { + if (bpf_probe_read_user(&key, sizeof(key), autoTLSkeyAddr)) { DEBUG_PRINT("Failed to read autoTLSkey from 0x%lx", (unsigned long) autoTLSkeyAddr); increment_metric(metricID_UnwindPythonErrBadAutoTlsKeyAddr); return ERR_PYTHON_BAD_AUTO_TLS_KEY_ADDR; @@ -196,9 +209,9 @@ ErrorCode get_PyThreadState(const PyProcInfo *pyinfo, void *tsd_base, void *auto } static inline __attribute__((__always_inline__)) -ErrorCode get_PyFrame(struct pt_regs *ctx, const PyProcInfo *pyinfo, void **frame) { +ErrorCode get_PyFrame(const PyProcInfo *pyinfo, void **frame) { void *tsd_base; - if (tsd_get_base(ctx, &tsd_base)) { + if (tsd_get_base(&tsd_base)) { DEBUG_PRINT("Failed to get TSD base address"); increment_metric(metricID_UnwindPythonErrReadTsdBase); return ERR_PYTHON_READ_TSD_BASE; @@ -227,8 +240,8 @@ ErrorCode get_PyFrame(struct pt_regs *ctx, const PyProcInfo *pyinfo, void **fram // Get PyThreadState.cframe void *cframe_ptr; - if (bpf_probe_read(&cframe_ptr, sizeof(void *), - py_tsd_thread_state + pyinfo->PyThreadState_frame)) { + if (bpf_probe_read_user(&cframe_ptr, sizeof(void *), + py_tsd_thread_state + pyinfo->PyThreadState_frame)) { DEBUG_PRINT( "Failed to read PyThreadState.cframe at 0x%lx", (unsigned long) (py_tsd_thread_state + pyinfo->PyThreadState_frame)); @@ -237,8 +250,8 @@ ErrorCode get_PyFrame(struct pt_regs *ctx, const PyProcInfo *pyinfo, void **fram } // Get _PyCFrame.current_frame - if (bpf_probe_read(frame, sizeof(void *), - cframe_ptr + pyinfo->PyCFrame_current_frame)) { + if (bpf_probe_read_user(frame, sizeof(void *), + cframe_ptr + pyinfo->PyCFrame_current_frame)) { DEBUG_PRINT( "Failed to read _PyCFrame.current_frame at 0x%lx", (unsigned long) (cframe_ptr + pyinfo->PyCFrame_current_frame)); @@ -247,8 +260,8 @@ ErrorCode get_PyFrame(struct pt_regs *ctx, const PyProcInfo *pyinfo, void **fram } } else { // Get PyThreadState.frame - if (bpf_probe_read(frame, sizeof(void *), - py_tsd_thread_state + pyinfo->PyThreadState_frame)) { + if (bpf_probe_read_user(frame, sizeof(void *), + py_tsd_thread_state + pyinfo->PyThreadState_frame)) { DEBUG_PRINT( "Failed to read PyThreadState.frame at 0x%lx", (unsigned long) (py_tsd_thread_state + pyinfo->PyThreadState_frame)); @@ -287,7 +300,7 @@ int unwind_python(struct pt_regs *ctx) { DEBUG_PRINT("Building Python stack for 0x%x", pyinfo->version); if (!record->pythonUnwindState.py_frame) { increment_metric(metricID_UnwindPythonAttempts); - error = get_PyFrame(ctx, pyinfo, &record->pythonUnwindState.py_frame); + error = get_PyFrame(pyinfo, &record->pythonUnwindState.py_frame); if (error) { goto exit; } diff --git a/support/ebpf/ruby_tracer.ebpf.c b/support/ebpf/ruby_tracer.ebpf.c index bda809b7..41ecacaa 100644 --- a/support/ebpf/ruby_tracer.ebpf.c +++ b/support/ebpf/ruby_tracer.ebpf.c @@ -75,13 +75,15 @@ ErrorCode walk_ruby_stack(PerCPURecord *record, const RubyProcInfo *rubyinfo, // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_core.h#L846 size_t stack_size; - if (bpf_probe_read(&stack_ptr_current, sizeof(stack_ptr_current), (void *)(current_ctx_addr + rubyinfo->vm_stack))) { + if (bpf_probe_read_user(&stack_ptr_current, sizeof(stack_ptr_current), + (void *)(current_ctx_addr + rubyinfo->vm_stack))) { DEBUG_PRINT("ruby: failed to read current stack pointer"); increment_metric(metricID_UnwindRubyErrReadStackPtr); return ERR_RUBY_READ_STACK_PTR; } - if (bpf_probe_read(&stack_size, sizeof(stack_size), (void *)(current_ctx_addr + rubyinfo->vm_stack_size))) { + if (bpf_probe_read_user(&stack_size, sizeof(stack_size), + (void *)(current_ctx_addr + rubyinfo->vm_stack_size))) { DEBUG_PRINT("ruby: failed to get stack size"); increment_metric(metricID_UnwindRubyErrReadStackSize); return ERR_RUBY_READ_STACK_SIZE; @@ -93,7 +95,7 @@ ErrorCode walk_ruby_stack(PerCPURecord *record, const RubyProcInfo *rubyinfo, last_stack_frame = stack_ptr_current + (rubyinfo->size_of_value * stack_size) - (2 * rubyinfo->size_of_control_frame_struct); - if (bpf_probe_read(&stack_ptr, sizeof(stack_ptr), (void *)(current_ctx_addr + rubyinfo->cfp))) { + if (bpf_probe_read_user(&stack_ptr, sizeof(stack_ptr), (void *)(current_ctx_addr + rubyinfo->cfp))) { DEBUG_PRINT("ruby: failed to get cfp"); increment_metric(metricID_UnwindRubyErrReadCfp); return ERR_RUBY_READ_CFP; @@ -119,8 +121,8 @@ ErrorCode walk_ruby_stack(PerCPURecord *record, const RubyProcInfo *rubyinfo, pc = 0; iseq_addr = NULL; - bpf_probe_read(&iseq_addr, sizeof(iseq_addr), (void *)(stack_ptr + rubyinfo->iseq)); - bpf_probe_read(&pc, sizeof(pc), (void *)(stack_ptr + rubyinfo->pc)); + bpf_probe_read_user(&iseq_addr, sizeof(iseq_addr), (void *)(stack_ptr + rubyinfo->iseq)); + bpf_probe_read_user(&pc, sizeof(pc), (void *)(stack_ptr + rubyinfo->pc)); // If iseq or pc is 0, then this frame represents a registered hook. // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm.c#L1960 if (pc == 0 || iseq_addr == NULL) { @@ -138,7 +140,7 @@ ErrorCode walk_ruby_stack(PerCPURecord *record, const RubyProcInfo *rubyinfo, } u64 ep = 0; - if (bpf_probe_read(&ep, sizeof(ep), (void *)(stack_ptr + rubyinfo->ep))) { + if (bpf_probe_read_user(&ep, sizeof(ep), (void *)(stack_ptr + rubyinfo->ep))) { DEBUG_PRINT("ruby: failed to get ep"); increment_metric(metricID_UnwindRubyErrReadEp); return ERR_RUBY_READ_EP; @@ -155,19 +157,20 @@ ErrorCode walk_ruby_stack(PerCPURecord *record, const RubyProcInfo *rubyinfo, goto save_state; } - if (bpf_probe_read(&iseq_body, sizeof(iseq_body), (void *)(iseq_addr + rubyinfo->body))) { + if (bpf_probe_read_user(&iseq_body, sizeof(iseq_body), (void *)(iseq_addr + rubyinfo->body))) { DEBUG_PRINT("ruby: failed to get iseq body"); increment_metric(metricID_UnwindRubyErrReadIseqBody); return ERR_RUBY_READ_ISEQ_BODY; } - if (bpf_probe_read(&iseq_encoded, sizeof(iseq_encoded), (void *)(iseq_body + rubyinfo->iseq_encoded))) { + if (bpf_probe_read_user(&iseq_encoded, sizeof(iseq_encoded), + (void *)(iseq_body + rubyinfo->iseq_encoded))) { DEBUG_PRINT("ruby: failed to get iseq encoded"); increment_metric(metricID_UnwindRubyErrReadIseqEncoded); return ERR_RUBY_READ_ISEQ_ENCODED; } - if (bpf_probe_read(&iseq_size, sizeof(iseq_size), (void *)(iseq_body + rubyinfo->iseq_size))) { + if (bpf_probe_read_user(&iseq_size, sizeof(iseq_size), (void *)(iseq_body + rubyinfo->iseq_size))) { DEBUG_PRINT("ruby: failed to get iseq size"); increment_metric(metricID_UnwindRubyErrReadIseqSize); return ERR_RUBY_READ_ISEQ_SIZE; @@ -243,18 +246,18 @@ int unwind_ruby(struct pt_regs *ctx) { // the offset to running_ec. void *single_main_ractor = NULL; - if (bpf_probe_read(&single_main_ractor, sizeof(single_main_ractor), - (void *)rubyinfo->current_ctx_ptr)) { + if (bpf_probe_read_user(&single_main_ractor, sizeof(single_main_ractor), + (void *)rubyinfo->current_ctx_ptr)) { goto exit; } - if (bpf_probe_read(¤t_ctx_addr, sizeof(current_ctx_addr), - (void *)(single_main_ractor + rubyinfo->running_ec))) { + if (bpf_probe_read_user(¤t_ctx_addr, sizeof(current_ctx_addr), + (void *)(single_main_ractor + rubyinfo->running_ec))) { goto exit; } } else { - if (bpf_probe_read(¤t_ctx_addr, sizeof(current_ctx_addr), - (void *)rubyinfo->current_ctx_ptr)) { + if (bpf_probe_read_user(¤t_ctx_addr, sizeof(current_ctx_addr), + (void *)rubyinfo->current_ctx_ptr)) { goto exit; } } diff --git a/support/ebpf/sched_monitor.ebpf.c b/support/ebpf/sched_monitor.ebpf.c index f5c64556..540c87fa 100644 --- a/support/ebpf/sched_monitor.ebpf.c +++ b/support/ebpf/sched_monitor.ebpf.c @@ -27,10 +27,9 @@ int tracepoint__sched_process_exit(void *ctx) { goto exit; } - if (report_pid(ctx, pid, false)) { + if (report_pid(ctx, pid, RATELIMIT_ACTION_RESET)) { increment_metric(metricID_NumProcExit); } - exit: return 0; } diff --git a/support/ebpf/stackdeltatypes.h b/support/ebpf/stackdeltatypes.h index 633fad44..131450b8 100644 --- a/support/ebpf/stackdeltatypes.h +++ b/support/ebpf/stackdeltatypes.h @@ -11,6 +11,8 @@ #define UNWIND_OPCODE_BASE_FP 0x03 // Expression with base value being the Link Register (ARM64) #define UNWIND_OPCODE_BASE_LR 0x04 +// Expression with base value being a Generic Register +#define UNWIND_OPCODE_BASE_REG 0x05 // An opcode flag to indicate that the value should be dereferenced #define UNWIND_OPCODEF_DEREF 0x80 @@ -33,4 +35,8 @@ // This assumes register size offsets are used. #define UNWIND_DEREF_MULTIPLIER 8 +// For the UNWIND_OPCODE_BASE_REG, the bitmask reserved for the register +// number. Remaining bits are the offset. +#define UNWIND_REG_MASK 15 + #endif diff --git a/support/ebpf/system_config.ebpf.c b/support/ebpf/system_config.ebpf.c index b797dce4..8786ce51 100644 --- a/support/ebpf/system_config.ebpf.c +++ b/support/ebpf/system_config.ebpf.c @@ -1,10 +1,95 @@ +// This file contains the code and map definitions for system configuration +// and analysis related functions. + #include "bpfdefs.h" #include "types.h" +#include "extmaps.h" - -struct bpf_map_def SEC("maps") system_config = { +// system config is the bpf map containing HA provided system configuration +bpf_map_def SEC("maps") system_config = { .type = BPF_MAP_TYPE_ARRAY, .key_size = sizeof(u32), - .value_size = sizeof(SystemConfig), + .value_size = sizeof(struct SystemConfig), .max_entries = 1, }; + +#ifndef TESTING_COREDUMP + +// system_analysis is the bpf map the HA and this module uses to communicate +bpf_map_def SEC("maps") system_analysis = { + .type = BPF_MAP_TYPE_ARRAY, + .key_size = sizeof(u32), + .value_size = sizeof(struct SystemAnalysis), + .max_entries = 1, +}; + +// read_kernel_memory reads data from given kernel address. This is +// invoked once on entry to bpf() syscall on the given pid context. +SEC("tracepoint/syscalls/sys_enter_bpf") +int read_kernel_memory(void *ctx) { + u32 key0 = 0; + + struct SystemAnalysis *sys = bpf_map_lookup_elem(&system_analysis, &key0); + if (!sys) { + // Not reachable. The one array element always exists. + return 0; + } + + if (sys->pid != (bpf_get_current_pid_tgid() >> 32)) { + // Execute the hook only in the context of requesting task. + return 0; + } + + // Mark request handled + sys->pid = 0; + + // Handle the read request + if (bpf_probe_read_kernel(sys->code, sizeof(sys->code), (void*) sys->address)) { + DEBUG_PRINT("Failed to read code from 0x%lx", (unsigned long) sys->address); + return -1; + } + + return 0; +} + +// read_task_struct reads data the current struct task_struct along with +// the struct pt_regs pointer to the entry stack's usermode cpu state. +// Requires kernel 4.19 or newer due to attaching to a raw tracepoint. +SEC("raw_tracepoint/sys_enter") +int read_task_struct(struct bpf_raw_tracepoint_args *ctx) { + struct pt_regs *regs = (struct pt_regs *) ctx->args[0]; + u32 key0 = 0; + + struct SystemAnalysis *sys = bpf_map_lookup_elem(&system_analysis, &key0); + if (!sys) { + // Not reachable. The one array element always exists. + return 0; + } + + if (sys->pid != (bpf_get_current_pid_tgid() >> 32)) { + // Execute the hook only in the context of requesting task. + return 0; + } + + // Mark request handled + sys->pid = 0; + + // Request to read current task. Adjust read address, and return + // also the address of struct pt_regs in the entry stack. + u64 addr = bpf_get_current_task() + sys->address; + + // As this is a raw tracepoint for syscall entry, the struct pt_regs * + // is guaranteed to be the user mode cpu state on the entry stack. + // Return this to the caller. + sys->address = (u64) regs; + + // Execute the read request. + if (bpf_probe_read_kernel(sys->code, sizeof(sys->code), (void*) addr)) { + DEBUG_PRINT("Failed to read task_struct from 0x%lx", (unsigned long) addr); + return -1; + } + + return 0; +} + +#endif diff --git a/support/ebpf/tracemgmt.h b/support/ebpf/tracemgmt.h index 37a44255..e1f0bedd 100644 --- a/support/ebpf/tracemgmt.h +++ b/support/ebpf/tracemgmt.h @@ -21,6 +21,156 @@ void increment_metric(u32 metricID) { } } +// Send immediate notifications for event triggers to Go. +// Notifications for GENERIC_PID and TRACES_FOR_SYMBOLIZATION will be +// automatically inhibited until HA resets the type. +static inline void event_send_trigger(struct pt_regs *ctx, u32 event_type) { + int inhibit_key = event_type; + bool inhibit_value = true; + + // GENERIC_PID is a global notification that triggers eBPF map iteration+processing in Go. + // To avoid redundant notifications while userspace processing for them is already taking + // place, we allow latch-like inhibition, where eBPF sets it and Go has to manually reset + // it, before new notifications are triggered. + if (event_type != EVENT_TYPE_GENERIC_PID) { + return; + } + + if (bpf_map_update_elem(&inhibit_events, &inhibit_key, &inhibit_value, BPF_NOEXIST) < 0) { + DEBUG_PRINT("Event type %d inhibited", event_type); + return; + } + + switch (event_type) { + case EVENT_TYPE_GENERIC_PID: + increment_metric(metricID_NumGenericPID); + break; + default: + // no action + break; + } + + Event event = {.event_type = event_type}; + int ret = bpf_perf_event_output(ctx, &report_events, BPF_F_CURRENT_CPU, &event, sizeof(event)); + if (ret < 0) { + DEBUG_PRINT("event_send_trigger failed to send event %d: error %d", event_type, ret); + } +} + +// Forward declaration +struct bpf_perf_event_data; + +// pid_information_exists checks if the given pid exists in pid_page_to_mapping_info or not. +static inline __attribute__((__always_inline__)) +bool pid_information_exists(void *ctx, int pid) { + PIDPage key = {}; + key.prefixLen = BIT_WIDTH_PID + BIT_WIDTH_PAGE; + key.pid = __constant_cpu_to_be32((u32) pid); + key.page = 0; + + return bpf_map_lookup_elem(&pid_page_to_mapping_info, &key) != NULL; +} + +// Reset the ratelimit cache +#define RATELIMIT_ACTION_RESET 0 +// Use default timer +#define RATELIMIT_ACTION_DEFAULT 1 +// Set PID to fast timer mode +#define RATELIMIT_ACTION_FAST 2 + +// pid_event_ratelimit determines if the PID event should be inhibited or not +// based on rate limiting rules. +static inline __attribute__((__always_inline__)) +bool pid_event_ratelimit(u32 pid, int ratelimit_action) { + const u8 default_max_attempts = 8; // 25 seconds + const u8 fast_max_attempts = 4; // 1.6 seconds + const u8 fast_timer_flag = 0x10; + u64 *token_ptr = bpf_map_lookup_elem(&reported_pids, &pid); + u64 ts = bpf_ktime_get_ns(); + u8 attempt = 0; + u8 fast_timer = (ratelimit_action == RATELIMIT_ACTION_FAST) ? fast_timer_flag : 0; + + if (ratelimit_action == RATELIMIT_ACTION_RESET) { + return false; + } + + if (token_ptr) { + u64 token = *token_ptr; + u64 diff_ts = ts - (token & ~0x1fULL); + attempt = token & 0xf; + fast_timer |= token & fast_timer_flag; + // Calculate the limit window size. 100ms << attempt. + u64 limit_window_ts = (100*1000000ULL) << attempt; + + if (diff_ts < limit_window_ts) { + // Minimum event interval. + DEBUG_PRINT("PID %d event limited: too fast", pid); + return true; + } + if (diff_ts < limit_window_ts + (5000*1000000ULL)) { + // PID event within 5 seconds, increase limit window size if possible + if (attempt < (fast_timer ? fast_max_attempts : default_max_attempts)) { + attempt++; + } + } else { + // Silence for at least 5 seconds. Reset back to zero. + attempt = 0; + } + } + + // Create new token: + // 59 bits - the high bits of timestamp of last event + // 1 bit - set if the PID should be in fast timer mode + // 4 bits - number of bursts left at event time + DEBUG_PRINT("PID %d event send, attempt=%d", pid, attempt); + u64 token = (ts & ~0x1fULL) | fast_timer | attempt; + + // Update the map entry. Technically this is not SMP safe, but doing + // an atomic update would require EBPF atomics. At worst we send an + // extra sync event and the likelyhood for this race is very low, so + // we can live with this. + int err = bpf_map_update_elem(&reported_pids, &pid, &token, BPF_ANY); + if (err != 0) { + // Should never happen + DEBUG_PRINT("Failed to report PID %d: %d", pid, err); + increment_metric(metricID_ReportedPIDsErr); + return true; + } + + return false; +} + +// report_pid informs userspace about a PID that needs to be processed. +// If inhibit is true, PID will first be checked against maps/reported_pids +// and reporting aborted if PID has been recently reported. +// Returns true if the PID was successfully reported to user space. +static inline __attribute__((__always_inline__)) +bool report_pid(void *ctx, int pid, int ratelimit_action) { + u32 key = (u32) pid; + + if (pid_event_ratelimit(pid, ratelimit_action)) { + return false; + } + + bool value = true; + int errNo = bpf_map_update_elem(&pid_events, &key, &value, BPF_ANY); + if (errNo != 0) { + DEBUG_PRINT("Failed to update pid_events with PID %d: %d", pid, errNo); + increment_metric(metricID_PIDEventsErr); + return false; + } + if (ratelimit_action == RATELIMIT_ACTION_RESET || errNo != 0) { + bpf_map_delete_elem(&reported_pids, &key); + } + + // Notify userspace that there is a PID waiting to be processed. + // At this point, the PID was successfully written to maps/pid_events, + // therefore there is no need to track success/failure of event_send_trigger + // and we can simply return success. + event_send_trigger(ctx, EVENT_TYPE_GENERIC_PID); + return true; +} + // Return the per-cpu record. // As each per-cpu array only has 1 entry, we hard-code 0 as the key. // The return value of get_per_cpu_record() can never be NULL and return value checks only exist @@ -50,8 +200,8 @@ static inline PerCPURecord *get_pristine_per_cpu_record() #elif defined(__aarch64__) record->state.lr = 0; record->state.r22 = 0; - record->state.lr_valid = false; #endif + record->state.return_address = false; record->state.error_metric = -1; record->state.unwind_error = ERR_OK; record->perlUnwindState.stackinfo = 0; @@ -62,13 +212,15 @@ static inline PerCPURecord *get_pristine_per_cpu_record() record->rubyUnwindState.last_stack_frame = 0; record->unwindersDone = 0; record->tailCalls = 0; + record->ratelimitAction = RATELIMIT_ACTION_DEFAULT; Trace *trace = &record->trace; trace->kernel_stack_id = -1; trace->stack_len = 0; trace->pid = 0; - - // TODO: memset trace to all-zero here? + trace->apm_trace_id.as_int.hi = 0; + trace->apm_trace_id.as_int.lo = 0; + trace->apm_transaction_id.as_int = 0; return record; } @@ -97,7 +249,7 @@ void unwinder_mark_done(PerCPURecord *record, int unwinder) { // calc_line). This should probably be renamed to something like "frame type // specific data". static inline __attribute__((__always_inline__)) -ErrorCode _push_with_max_frames(Trace *trace, u64 file, u64 line, u8 frame_type, u32 max_frames) { +ErrorCode _push_with_max_frames(Trace *trace, u64 file, u64 line, u8 frame_type, u8 return_address, u32 max_frames) { if (trace->stack_len >= max_frames) { DEBUG_PRINT("unable to push frame: stack is full"); increment_metric(metricID_UnwindErrStackLengthExceeded); @@ -107,30 +259,37 @@ ErrorCode _push_with_max_frames(Trace *trace, u64 file, u64 line, u8 frame_type, #ifdef TESTING_COREDUMP // utils/coredump uses CGO to build the eBPF code. This dispatches // the frame information directly to helper implemented in ebpfhelpers.go. - int __push_frame(u64, u64, u64, u8); + int __push_frame(u64, u64, u64, u8, u8); trace->stack_len++; - return __push_frame(__cgo_ctx->id, file, line, frame_type); + return __push_frame(__cgo_ctx->id, file, line, frame_type, return_address); #else trace->frames[trace->stack_len++] = (Frame) { .file_id = file, .addr_or_line = line, .kind = frame_type, + .return_address = return_address, }; return ERR_OK; #endif } +// Push the file ID, line number and frame type into FrameList +static inline __attribute__((__always_inline__)) +ErrorCode _push_with_return_address(Trace *trace, u64 file, u64 line, u8 frame_type, bool return_address) { + return _push_with_max_frames(trace, file, line, frame_type, return_address, MAX_NON_ERROR_FRAME_UNWINDS); +} + // Push the file ID, line number and frame type into FrameList static inline __attribute__((__always_inline__)) ErrorCode _push(Trace *trace, u64 file, u64 line, u8 frame_type) { - return _push_with_max_frames(trace, file, line, frame_type, MAX_NON_ERROR_FRAME_UNWINDS); + return _push_with_max_frames(trace, file, line, frame_type, 0, MAX_NON_ERROR_FRAME_UNWINDS); } // Push a critical error frame. static inline __attribute__((__always_inline__)) ErrorCode push_error(Trace *trace, ErrorCode error) { - return _push_with_max_frames(trace, 0, error, FRAME_MARKER_ABORT, MAX_FRAME_UNWINDS); + return _push_with_max_frames(trace, 0, error, FRAME_MARKER_ABORT, 0, MAX_FRAME_UNWINDS); } // Send a trace to user-land via the `trace_events` perf event buffer. @@ -143,105 +302,9 @@ void send_trace(void *ctx, Trace *trace) { return; // unreachable } - extern bpf_map_def trace_events; bpf_perf_event_output(ctx, &trace_events, BPF_F_CURRENT_CPU, trace, send_size); } -// Send immediate notifications for event triggers to Go. -// Notifications for GENERIC_PID and TRACES_FOR_SYMBOLIZATION will be -// automatically inhibited until HA resets the type. -static inline void event_send_trigger(struct pt_regs *ctx, u32 event_type) { - int inhibit_key = event_type; - bool inhibit_value = true; - - // GENERIC_PID is global notifications that trigger eBPF map iteration+processing in Go. - // To avoid redundant notifications while userspace processing for them is already taking - // place, we allow latch-like inhibition, where eBPF sets it and Go has to manually reset - // it, before new notifications are triggered. - if (event_type != EVENT_TYPE_GENERIC_PID) { - return; - } - - if (bpf_map_update_elem(&inhibit_events, &inhibit_key, &inhibit_value, BPF_NOEXIST) < 0) { - DEBUG_PRINT("Event type %d inhibited", event_type); - return; - } - - switch (event_type) { - case EVENT_TYPE_GENERIC_PID: - increment_metric(metricID_NumGenericPID); - break; - default: - // no action - break; - } - - Event event = {.event_type = event_type}; - int ret = bpf_perf_event_output(ctx, &report_events, BPF_F_CURRENT_CPU, &event, sizeof(event)); - if (ret < 0) { - DEBUG_PRINT("event_send_trigger failed to send event %d: error %d", event_type, ret); - } -} - -// Forward declaration -struct bpf_perf_event_data; - -// pid_information_exists checks if the given pid exists in pid_page_to_mapping_info or not. -static inline __attribute__((__always_inline__)) -bool pid_information_exists(void *ctx, int pid) { - PIDPage key = {}; - key.prefixLen = BIT_WIDTH_PID + BIT_WIDTH_PAGE; - key.pid = __constant_cpu_to_be32((u32) pid); - key.page = 0; - - return bpf_map_lookup_elem(&pid_page_to_mapping_info, &key) != NULL; -} - -// report_pid informs userspace about a PID that needs to be processed. -// If inhibit is true, PID will first be checked against maps/reported_pids -// and reporting aborted if PID has been recently reported. -// Returns true if the PID was successfully reported to user space. -static inline __attribute__((__always_inline__)) -bool report_pid(void *ctx, int pid, bool inhibit) { - u32 key = (u32) pid; - int errNo; - - if (inhibit) { - u64 *ts_old = bpf_map_lookup_elem(&reported_pids, &key); - u64 ts = bpf_ktime_get_ns(); - if (ts_old && (ts - *ts_old) < REPORTED_PIDS_TIMEOUT) { - DEBUG_PRINT("PID %d was recently reported. User space will not be notified", pid); - return false; - } - - errNo = bpf_map_update_elem(&reported_pids, &key, &ts, BPF_ANY); - if (errNo != 0) { - // Should never happen - DEBUG_PRINT("Failed to report PID %d: %d", pid, errNo); - increment_metric(metricID_ReportedPIDsErr); - return false; - } - } - - bool value = true; - errNo = bpf_map_update_elem(&pid_events, &key, &value, BPF_ANY); - if (errNo != 0) { - DEBUG_PRINT("Failed to update pid_events with PID %d: %d", pid, errNo); - increment_metric(metricID_PIDEventsErr); - if (inhibit) { - bpf_map_delete_elem(&reported_pids, &key); - } - return false; - } - - // Notify userspace that there is a PID waiting to be processed. - // At this point, the PID was successfully written to maps/pid_events, - // therefore there is no need to track success/failure of event_send_trigger - // and we can simply return success. - event_send_trigger(ctx, EVENT_TYPE_GENERIC_PID); - return true; -} - // is_kernel_address checks if the given address looks like virtual address to kernel memory. static bool is_kernel_address(u64 addr) { return addr & 0xFF00000000000000UL; diff --git a/support/ebpf/tracer.ebpf.arm64 b/support/ebpf/tracer.ebpf.arm64 new file mode 100644 index 00000000..197c4284 Binary files /dev/null and b/support/ebpf/tracer.ebpf.arm64 differ diff --git a/support/ebpf/tracer.ebpf.x86 b/support/ebpf/tracer.ebpf.x86 new file mode 100644 index 00000000..197c4284 Binary files /dev/null and b/support/ebpf/tracer.ebpf.x86 differ diff --git a/support/ebpf/tsd.ebpf.c b/support/ebpf/tsd.ebpf.c deleted file mode 100644 index d007f136..00000000 --- a/support/ebpf/tsd.ebpf.c +++ /dev/null @@ -1,57 +0,0 @@ -// This file contains the code and map definitions for Thread Local Storage (TLS) access - -#undef asm_volatile_goto -#define asm_volatile_goto(x...) asm volatile("invalid use of asm_volatile_goto") - -#include "bpfdefs.h" -#include "types.h" - -// codedump_addr is used to communicate the address of kernel function to eBPF code. -// It is used by extract_tpbase_offset. -bpf_map_def SEC("maps") codedump_addr = { - .type = BPF_MAP_TYPE_ARRAY, - .key_size = sizeof(u32), - .value_size = sizeof(u64), - .max_entries = 1, -}; - -// codedump_code is populated by `codedump` it is meant to contain the first -// CODEDUMP_BYTES bytes of the function code requested via codedump_addr. -bpf_map_def SEC("maps") codedump_code = { - .type = BPF_MAP_TYPE_ARRAY, - .key_size = sizeof(u32), - .value_size = CODEDUMP_BYTES, - .max_entries = 1, -}; - -// codedump extracts the first CODEDUMP_BYTES bytes of code from the function at -// address codedump_addr[0], and stores them in codedump_code[0]. -SEC("tracepoint/syscalls/sys_enter_bpf") -int tracepoint__sys_enter_bpf(struct pt_regs *ctx) { - u32 key0 = 0; - int ret; - u8 code[CODEDUMP_BYTES]; - - // Read address of aout_dump_debugregs, provided by userspace - void **paddr = bpf_map_lookup_elem(&codedump_addr, &key0); - if (!paddr) { - DEBUG_PRINT("Failed to look up codedump_addr for function address"); - return -1; - } - - // Read first few bytes of aout_dump_debugregs code - ret = bpf_probe_read(code, sizeof(code), *paddr); - if (ret) { - DEBUG_PRINT("Failed to read code from 0x%lx: error code %d", (unsigned long) *paddr, ret); - return -1; - } - - // Copy the bytes to a map, for userspace processing - ret = bpf_map_update_elem(&codedump_code, &key0, code, BPF_ANY); - if (ret) { - DEBUG_PRINT("Failed to store code: error code %d", ret); - return -1; - } - - return 0; -} diff --git a/support/ebpf/tsd.h b/support/ebpf/tsd.h index 8e1fa4b9..7864964c 100644 --- a/support/ebpf/tsd.h +++ b/support/ebpf/tsd.h @@ -9,7 +9,7 @@ int tsd_read(const TSDInfo *tsi, const void *tsd_base, int key, void **out) { const void *tsd_addr = tsd_base + tsi->offset; if (tsi->indirect) { // Read the memory pointer that contains the per-TSD key data - if (bpf_probe_read(&tsd_addr, sizeof(tsd_addr), tsd_addr)) { + if (bpf_probe_read_user(&tsd_addr, sizeof(tsd_addr), tsd_addr)) { goto err; } } @@ -17,7 +17,7 @@ int tsd_read(const TSDInfo *tsi, const void *tsd_base, int key, void **out) { tsd_addr += key * tsi->multiplier; DEBUG_PRINT("readTSD key %d from address 0x%lx", key, (unsigned long) tsd_addr); - if (bpf_probe_read(out, sizeof(*out), tsd_addr)) { + if (bpf_probe_read_user(out, sizeof(*out), tsd_addr)) { goto err; } return 0; @@ -30,7 +30,7 @@ int tsd_read(const TSDInfo *tsi, const void *tsd_base, int key, void **out) { // tsd_get_base looks up the base address for TSD variables (TPBASE). static inline __attribute__((__always_inline__)) -int tsd_get_base(struct pt_regs *ctx, void **tsd_base) { +int tsd_get_base(void **tsd_base) { #ifdef TESTING_COREDUMP *tsd_base = (void *) __cgo_ctx->tp_base; return 0; @@ -50,7 +50,7 @@ int tsd_get_base(struct pt_regs *ctx, void **tsd_base) { // syscfg->tpbase_offset is populated with the offset of `fsbase` or equivalent field // relative to a `task_struct`, so we use that instead. void *tpbase_ptr = ((char *)task) + syscfg->tpbase_offset; - if (bpf_probe_read(tsd_base, sizeof(void *), tpbase_ptr)) { + if (bpf_probe_read_kernel(tsd_base, sizeof(void *), tpbase_ptr)) { DEBUG_PRINT("Failed to read tpbase value"); increment_metric(metricID_UnwindErrBadTPBaseAddr); return -1; diff --git a/support/ebpf/types.h b/support/ebpf/types.h index f403a82b..74f5b310 100644 --- a/support/ebpf/types.h +++ b/support/ebpf/types.h @@ -271,6 +271,36 @@ enum { // number of times an unwind_info_array index was invalid metricID_UnwindNativeErrBadUnwindInfoIndex, + // number of failures to get TSD base for APM correlation + metricID_UnwindApmIntErrReadTsdBase, + + // number of failures read the APM correlation pointer + metricID_UnwindApmIntErrReadCorrBufPtr, + + // number of failures read the APM correlation buffer + metricID_UnwindApmIntErrReadCorrBuf, + + // number of successful reads of APM correlation info + metricID_UnwindApmIntReadSuccesses, + + // number of attempted Dotnet unwinds + metricID_UnwindDotnetAttempts, + + // number of unwound Dotnet frames + metricID_UnwindDotnetFrames, + + // number of times no entry for a process exists in the Dotnet process info array + metricID_UnwindDotnetErrNoProcInfo, + + // number of failures to read Dotnet frame pointer data + metricID_UnwindDotnetErrBadFP, + + // number of failures to read Dotnet CodeHeader object + metricID_UnwindDotnetErrCodeHeader, + + // number of failures to unwind code object due to its large size + metricID_UnwindDotnetErrCodeTooLarge, + // // Metric IDs above are for counters (cumulative values) // @@ -297,6 +327,7 @@ typedef enum TracePrograms { PROG_UNWIND_PHP, PROG_UNWIND_RUBY, PROG_UNWIND_V8, + PROG_UNWIND_DOTNET, NUM_TRACER_PROGS, } TracePrograms; @@ -327,10 +358,12 @@ typedef struct Frame { u64 addr_or_line; // Indicates the type of the frame (Python, PHP, native etc.). u8 kind; + // Indicates that the address is a return address. + u8 return_address; // Explicit padding bytes that the compiler would have inserted anyway. // Here to make it clear to readers that there are spare bytes that could // be put to work without extra cost in case an interpreter needs it. - u8 pad[7]; + u8 pad[6]; } Frame; _Static_assert(sizeof(Frame) == 3 * 8, "frame padding not working as expected"); @@ -342,6 +375,11 @@ typedef struct TSDInfo { u8 indirect; } TSDInfo; +// DotnetProcInfo is a container for the data needed to build stack trace for a dotnet process. +typedef struct DotnetProcInfo { + u32 version; +} DotnetProcInfo; + // PerlProcInfo is a container for the data needed to build a stack trace for a Perl process. typedef struct PerlProcInfo { u64 stateAddr; @@ -363,9 +401,11 @@ typedef struct PyProcInfo { // The Python object member offsets u8 PyThreadState_frame; u8 PyCFrame_current_frame; - u8 PyFrameObject_f_back, PyFrameObject_f_code, PyFrameObject_f_lasti, PyFrameObject_f_is_entry; + u8 PyFrameObject_f_back, PyFrameObject_f_code, PyFrameObject_f_lasti; + u8 PyFrameObject_entry_member, PyFrameObject_entry_val; u8 PyCodeObject_co_argcount, PyCodeObject_co_kwonlyargcount; u8 PyCodeObject_co_flags, PyCodeObject_co_firstlineno; + u8 PyCodeObject_sizeof; } PyProcInfo; // PHPProcInfo is a container for the data needed to build a stack trace for a PHP process. @@ -378,12 +418,6 @@ typedef struct PHPProcInfo { u8 zend_execute_data_this_type_info, zend_function_type, zend_op_lineno; } PHPProcInfo; -// PHPJITProcInfo is a container for the data needed to detect if a PC corresponds to a PHP -// JIT program. This is used to adjust the return address. -typedef struct PHPJITProcInfo { - u64 start, end; -} PHPJITProcInfo; - // HotspotProcInfo is a container for the data needed to build a stack trace // for a Java Hotspot VM process. typedef struct HotspotProcInfo { @@ -446,6 +480,38 @@ typedef struct V8ProcInfo { // COMM_LEN defines the maximum length we will receive for the comm of a task. #define COMM_LEN 16 +// 128-bit APM trace ID. +typedef union ApmTraceID { + u8 raw[16]; + struct { + u64 lo; + u64 hi; + } as_int; +} ApmTraceID; + +_Static_assert(sizeof(ApmTraceID) == 16, "unexpected trace ID size"); + +// 64-bit APM transaction / span ID. +typedef union ApmSpanID { + u8 raw[8]; + u64 as_int; +} ApmSpanID; + +_Static_assert(sizeof(ApmSpanID) == 8, "unexpected trace ID size"); + +// Defines the format of the APM correlation TLS buffer. +// +// Specification: https://github.com/elastic/apm/blob/bd5fa9c1/specs/agents/universal-profiling-integration.md#thread-local-storage-layout +typedef struct __attribute__((packed)) ApmCorrelationBuf { + u16 layout_minor_ver; + u8 valid; + u8 trace_present; + u8 trace_flags; + ApmTraceID trace_id; + ApmSpanID span_id; + ApmSpanID transaction_id; +} ApmCorrelationBuf; + // Container for a stack trace typedef struct Trace { // The process ID @@ -454,6 +520,10 @@ typedef struct Trace { u64 ktime; // The current COMM of the thread of this Trace. char comm[COMM_LEN]; + // APM transaction ID or all-zero if not present. + ApmSpanID apm_transaction_id; + // APM trace ID or all-zero if not present. + ApmTraceID apm_trace_id; // The kernel stack ID. s32 kernel_stack_id; // The number of frames in the stack. @@ -476,13 +546,11 @@ typedef struct UnwindState { u64 fp; #if defined(__x86_64__) - // Current register value for r13 - u64 r13; + // Current register values for named registers + u64 rax, r9, r11, r13, r15; #elif defined(__aarch64__) - // Current register value for lr - u64 lr; - // Current register value for r22 - u64 r22; + // Current register values for named registers + u64 lr, r22; #endif // The executable ID/hash associated with PC @@ -497,10 +565,10 @@ typedef struct UnwindState { // If unwinding was aborted due to an error, this contains the reason why. ErrorCode unwind_error; -#if defined(__aarch64__) - // If unwinding on LR register can be used (top frame or after signal handler) - bool lr_valid; -#endif + // Set if the PC is a return address. That is, it points to the next instruction + // after a CALL instruction, and requires to be adjusted during symbolization. + // On aarch64, this additionally means that LR register can not be used. + bool return_address; } UnwindState; // Container for unwinding state needed by the Perl unwinder. Keeping track of @@ -538,6 +606,16 @@ typedef struct RubyUnwindState { void *last_stack_frame; } RubyUnwindState; +// Container for additional scratch space needed by the HotSpot unwinder. +typedef struct DotnetUnwindScratchSpace { + // Buffer to read nibble map to locate code start. One map entry allows seeking backwards + // 32*8 = 256 bytes of code. This defines the maximum size for a JITted function we + // can reconize: 256 bytes/element * 128 elements = 32kB function size. + u32 map[128]; + // Extra space to read to map fixed amount of bytes, but to dynamic offset. + u32 extra[128]; +} DotnetUnwindScratchSpace; + // Container for additional scratch space needed by the HotSpot unwinder. typedef struct HotspotUnwindScratchSpace { // Read buffer for storing the codeblob. It's not needed across calls, but the buffer is too @@ -585,6 +663,8 @@ typedef struct PerCPURecord { // The current Ruby unwinder state. RubyUnwindState rubyUnwindState; union { + // Scratch space for the Dotnet unwinder. + DotnetUnwindScratchSpace dotnetUnwindScratch; // Scratch space for the HotSpot unwinder. HotspotUnwindScratchSpace hotspotUnwindScratch; // Scratch space for the V8 unwinder @@ -597,6 +677,9 @@ typedef struct PerCPURecord { // tailCalls tracks the number of calls to bpf_tail_call(). u8 tailCalls; + + // ratelimitAction determines the PID event rate limiting mode + u8 ratelimitAction; } PerCPURecord; // UnwindInfo contains the unwind information needed to unwind one frame @@ -662,9 +745,12 @@ typedef struct OffsetRange { u16 program_index; // The interpreter-specific program index to call. } OffsetRange; -// Number of bytes of code to extract to userspace via codedump helper. -// Needed for tpbase offset calculations. -#define CODEDUMP_BYTES 128 +// SystemAnalysis is the structure in system_analysis map +typedef struct SystemAnalysis { + u64 address; + u32 pid; + u8 code[128]; +} SystemAnalysis; // Event is the header for all events sent through the report_events // perf event output channel (event_send_trigger). @@ -675,11 +761,6 @@ typedef struct Event { // Event types that notifications are sent for through event_send_trigger. #define EVENT_TYPE_GENERIC_PID 1 -// Maximum time in nanoseconds that a PID is allowed to stay in -// maps/reported_pids before being replaced/overwritten. -// Default is 30 seconds. -#define REPORTED_PIDS_TIMEOUT 30000000000ULL - // PIDPage represents the key of the eBPF map pid_page_to_mapping_info. typedef struct PIDPage { u32 prefixLen; // Number of bits for pid and page that defines the @@ -749,6 +830,12 @@ typedef struct SystemConfig { // populated by the host agent based on kernel code analysis. u64 tpbase_offset; + // The offset of stack base within `task_struct`. + u32 task_stack_offset; + + // The offset of struct pt_regs within the kernel entry stack. + u32 stack_ptregs_offset; + // Enables the temporary hack that drops pure errors frames in unwind_stop. bool drop_error_only_traces; } SystemConfig; @@ -759,4 +846,8 @@ typedef struct SystemConfig { #define PSR_MODE_MASK 0x0000000f #define PSR_MODE_EL0t 0x00000000 +typedef struct ApmIntProcInfo { + u64 tls_offset; +} ApmIntProcInfo; + #endif diff --git a/support/ebpf/v8_tracer.ebpf.c b/support/ebpf/v8_tracer.ebpf.c index 610571e9..9928b51c 100644 --- a/support/ebpf/v8_tracer.ebpf.c +++ b/support/ebpf/v8_tracer.ebpf.c @@ -32,10 +32,11 @@ bpf_map_def SEC("maps") v8_procs = { // Record a V8 frame static inline __attribute__((__always_inline__)) -ErrorCode push_v8(Trace *trace, unsigned long pointer_and_type, unsigned long delta_or_marker) { +ErrorCode push_v8(Trace *trace, unsigned long pointer_and_type, unsigned long delta_or_marker, + bool return_address) { DEBUG_PRINT("Pushing v8 frame delta_or_marker=%lx, pointer_and_type=%lx", delta_or_marker, pointer_and_type); - return _push(trace, pointer_and_type, delta_or_marker, FRAME_MARKER_V8); + return _push_with_return_address(trace, pointer_and_type, delta_or_marker, FRAME_MARKER_V8, return_address); } // Verify a V8 tagged pointer @@ -51,7 +52,7 @@ uintptr_t v8_verify_pointer(uintptr_t maybe_pointer) { static inline __attribute__((__always_inline__)) uintptr_t v8_read_object_ptr(uintptr_t addr) { uintptr_t maybe_pointer; - if (bpf_probe_read(&maybe_pointer, sizeof(maybe_pointer), (void*)addr)) { + if (bpf_probe_read_user(&maybe_pointer, sizeof(maybe_pointer), (void*)addr)) { return 0; } return v8_verify_pointer(maybe_pointer); @@ -77,7 +78,7 @@ u16 v8_read_object_type(V8ProcInfo *vi, uintptr_t addr) { } uintptr_t map = v8_read_object_ptr(addr + vi->off_HeapObject_map); u16 type; - if (!map || bpf_probe_read(&type, sizeof(type), (void*)(map + vi->off_Map_instancetype))) { + if (!map || bpf_probe_read_user(&type, sizeof(type), (void*)(map + vi->off_Map_instancetype))) { return 0; } return type; @@ -100,7 +101,7 @@ ErrorCode unwind_one_v8_frame(PerCPURecord *record, V8ProcInfo *vi, bool top) { } // Read FP pointer data - if (bpf_probe_read(scratch->fp_ctx, V8_FP_CONTEXT_SIZE, (void*)(fp - V8_FP_CONTEXT_SIZE))) { + if (bpf_probe_read_user(scratch->fp_ctx, V8_FP_CONTEXT_SIZE, (void*)(fp - V8_FP_CONTEXT_SIZE))) { DEBUG_PRINT("v8: -> failed to read frame pointer context"); increment_metric(metricID_UnwindV8ErrBadFP); return ERR_V8_BAD_FP; @@ -177,7 +178,7 @@ ErrorCode unwind_one_v8_frame(PerCPURecord *record, V8ProcInfo *vi, bool top) { } // Read the Code blob type and size - if (bpf_probe_read(scratch->code, sizeof(scratch->code), (void*) code)) { + if (bpf_probe_read_user(scratch->code, sizeof(scratch->code), (void*) code)) { increment_metric(metricID_UnwindV8ErrBadCode); goto frame_done; } @@ -212,7 +213,7 @@ ErrorCode unwind_one_v8_frame(PerCPURecord *record, V8ProcInfo *vi, bool top) { if (top && trace->stack_len == 0) { unsigned long stk[3]; - if (bpf_probe_read(stk, sizeof(stk), (void*)(sp - sizeof(stk)))) { + if (bpf_probe_read_user(stk, sizeof(stk), (void*)(sp - sizeof(stk)))) { DEBUG_PRINT("v8: --> bad stack pointer"); increment_metric(metricID_UnwindV8ErrBadFP); return ERR_V8_BAD_FP; @@ -256,20 +257,22 @@ ErrorCode unwind_one_v8_frame(PerCPURecord *record, V8ProcInfo *vi, bool top) { frame_done: // Unwind with frame pointer - if (bpf_probe_read(regs, sizeof(regs), (void*)fp)) { + if (bpf_probe_read_user(regs, sizeof(regs), (void*)fp)) { DEBUG_PRINT("v8: --> bad frame pointer"); increment_metric(metricID_UnwindV8ErrBadFP); return ERR_V8_BAD_FP; } - state->sp = fp + sizeof(regs); - state->fp = regs[0]; - state->pc = regs[1]; - ErrorCode error = push_v8(trace, pointer_and_type, delta_or_marker); + ErrorCode error = push_v8(trace, pointer_and_type, delta_or_marker, state->return_address); if (error) { return error; } + state->sp = fp + sizeof(regs); + state->fp = regs[0]; + state->pc = regs[1]; + state->return_address = true; + DEBUG_PRINT("v8: pc: %lx, sp: %lx, fp: %lx", (unsigned long) state->pc, (unsigned long) state->sp, (unsigned long) state->fp); diff --git a/support/ebpf_integration_test.go b/support/ebpf_integration_test.go index a46d0379..9e40f10a 100644 --- a/support/ebpf_integration_test.go +++ b/support/ebpf_integration_test.go @@ -13,55 +13,34 @@ import ( cebpf "github.com/cilium/ebpf" "github.com/cilium/ebpf/link" + "github.com/elastic/otel-profiling-agent/rlimit" - "github.com/elastic/otel-profiling-agent/libpf/rlimit" + "github.com/stretchr/testify/require" ) -// TestEbpf is a simplified version of otel-profiling-agent. -// It takes the same eBPF ELF file (from support/ebpf/tracer.ebpf.x86) +// TestEbpf is a simplified version of the profiling agent. +// It takes the same eBPF ELF file (from ebpf/tracer.ebpf.x86) // and loads it into the kernel. With this test, we can make sure, // our eBPF code is loaded correctly and not rejected by the kernel. // As this tests uses the BPF syscall, it is protected by the build tag integration. func TestEbpf(t *testing.T) { restoreRlimit, err := rlimit.MaximizeMemlock() - if err != nil { - t.Fatalf("failed to adjust rlimit: %v", err) - } + require.NoError(t, err) defer restoreRlimit() var coll *cebpf.CollectionSpec - t.Run("Load Tracer specification", func(t *testing.T) { - coll, err = LoadCollectionSpec() - if err != nil { - t.Fatalf("Failed to load specification for tracer: %v", err) - } - }) + coll, err = LoadCollectionSpec() + require.NoError(t, err) - var tracepointProbe *cebpf.Program - t.Run("Load tracepoint probe", func(t *testing.T) { - tracepointProbe, err = cebpf.NewProgram(coll.Programs["tracepoint__sys_enter_read"]) - if err != nil { - t.Fatalf("Failed to load tracepoint probe: %v", err) - } - }) + tracepointProbe, err := cebpf.NewProgram(coll.Programs["tracepoint__sys_enter_read"]) + require.NoError(t, err) + defer func() { + require.NoError(t, tracepointProbe.Close()) + }() - var hook link.Link - t.Run("Attach probe to tracepoint", func(t *testing.T) { - hook, err = link.Tracepoint("syscalls", "sys_enter_read", tracepointProbe, nil) - if err != nil { - t.Fatalf("Failed to hook tracepoint probe: %v", err) - } - }) + hook, err := link.Tracepoint("syscalls", "sys_enter_read", tracepointProbe, nil) + require.NoError(t, err) - t.Run("Remove tracepoint hook", func(t *testing.T) { - if err := hook.Close(); err != nil { - t.Fatalf("Failed to remove tracepoint hook: %v", err) - } - }) - - t.Run("Unload tracepoint probe", func(t *testing.T) { - if err := tracepointProbe.Close(); err != nil { - t.Fatalf("Failed to unload tracepoint probe: %v", err) - } - }) + err = hook.Close() + require.NoError(t, err) } diff --git a/support/types.go b/support/types.go index 62f7e17f..e99a1981 100644 --- a/support/types.go +++ b/support/types.go @@ -26,6 +26,7 @@ const ( FrameMarkerRuby = C.FRAME_MARKER_RUBY FrameMarkerPerl = C.FRAME_MARKER_PERL FrameMarkerV8 = C.FRAME_MARKER_V8 + FrameMarkerDotnet = C.FRAME_MARKER_DOTNET FrameMarkerAbort = C.FRAME_MARKER_ABORT ) @@ -38,6 +39,7 @@ const ( ProgUnwindRuby = C.PROG_UNWIND_RUBY ProgUnwindPerl = C.PROG_UNWIND_PERL ProgUnwindV8 = C.PROG_UNWIND_V8 + ProgUnwindDotnet = C.PROG_UNWIND_DOTNET ) const ( @@ -81,12 +83,6 @@ func DecodeBiasAndUnwindProgram(biasAndUnwindProgram uint64) (bias uint64, unwin return bias, unwindProgram } -const ( - // CodedumpBytes holds the number of bytes of code to extract to userspace via codedump helper. - // Needed for fsbase offset calculations. - CodedumpBytes = C.CODEDUMP_BYTES -) - const ( // StackDeltaBucket[Smallest|Largest] define the boundaries of the bucket sizes of the various // nested stack delta maps. diff --git a/testsupport/testfiles.go b/testsupport/testfiles.go index 57f787df..88e25d76 100644 --- a/testsupport/testfiles.go +++ b/testsupport/testfiles.go @@ -8,6 +8,7 @@ package testsupport import ( "encoding/base64" + "errors" "fmt" "os" ) @@ -15,11 +16,11 @@ import ( func writeExecutable(exeContents string) (string, error) { buffer, err := base64.StdEncoding.DecodeString(exeContents) if err != nil { - return "", fmt.Errorf("failed to base64-decode the embedded executable?") + return "", errors.New("failed to base64-decode the embedded executable?") } exeFile, err := os.CreateTemp("", "proc_test_tmp_exe_*") if err != nil { - return "", fmt.Errorf("failed to open tempfile") + return "", errors.New("failed to open tempfile") } b, err := exeFile.Write(buffer) diff --git a/utils/coredump/.gitignore b/tools/coredump/.gitignore similarity index 100% rename from utils/coredump/.gitignore rename to tools/coredump/.gitignore diff --git a/utils/coredump/README.md b/tools/coredump/README.md similarity index 98% rename from utils/coredump/README.md rename to tools/coredump/README.md index a2fce040..cb9697fa 100644 --- a/utils/coredump/README.md +++ b/tools/coredump/README.md @@ -210,7 +210,7 @@ the process that triggered it. You'll have to prepare your environment in the same manner as described in the ["Option 1"][opt1] section. [opt1]: #option-1-using-coredump-new -[macro]: https://github.com/elastic/prodfiler/blob/c099efec5564584d32deeaa8d6f5ad00eb80573c/pf-host-agent/support/ebpf/bpfdefs.h#L135 +[macro]: https://github.com/elastic/otel-profiling-agent/blob/319d980b2406f40e68e850f429c38e28aed69e36/support/ebpf/bpfdefs.h#L116 ## Extracting coredumps or modules diff --git a/utils/coredump/analyze.go b/tools/coredump/analyze.go similarity index 87% rename from utils/coredump/analyze.go rename to tools/coredump/analyze.go index 6a0c9bb4..591e8b0a 100644 --- a/utils/coredump/analyze.go +++ b/tools/coredump/analyze.go @@ -9,6 +9,7 @@ package main import ( "context" "encoding/json" + "errors" "flag" "fmt" "os" @@ -19,8 +20,9 @@ import ( log "github.com/sirupsen/logrus" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/process" - "github.com/elastic/otel-profiling-agent/utils/coredump/modulestore" + "github.com/elastic/otel-profiling-agent/process" + "github.com/elastic/otel-profiling-agent/tools/coredump/modulestore" + "github.com/elastic/otel-profiling-agent/util" ) type analyzeCmd struct { @@ -67,10 +69,10 @@ func (cmd *analyzeCmd) exec(context.Context, []string) (err error) { sourceArgCount++ } if sourceArgCount != 1 { - return fmt.Errorf("please specify either `-core`, `-case` or `-pid`") + return errors.New("please specify either `-core`, `-case` or `-pid`") } - lwpFilter := libpf.Set[libpf.PID]{} + lwpFilter := libpf.Set[util.PID]{} if cmd.lwpFilter != "" { for _, lwp := range strings.Split(cmd.lwpFilter, ",") { var parsed int64 @@ -78,7 +80,7 @@ func (cmd *analyzeCmd) exec(context.Context, []string) (err error) { if err != nil { return fmt.Errorf("failed to parse LWP: %v", err) } - lwpFilter[libpf.PID(parsed)] = libpf.Void{} + lwpFilter[util.PID(parsed)] = libpf.Void{} } } @@ -87,12 +89,13 @@ func (cmd *analyzeCmd) exec(context.Context, []string) (err error) { } var proc process.Process - if cmd.pid != 0 { - proc, err = process.NewPtrace(libpf.PID(cmd.pid)) + switch { + case cmd.pid != 0: + proc, err = process.NewPtrace(util.PID(cmd.pid)) if err != nil { return fmt.Errorf("failed to open pid `%d`: %w", cmd.pid, err) } - } else if cmd.casePath != "" { + case cmd.casePath != "": var testCase *CoredumpTestCase testCase, err = readTestCase(cmd.casePath) if err != nil { @@ -103,7 +106,7 @@ func (cmd *analyzeCmd) exec(context.Context, []string) (err error) { if err != nil { return fmt.Errorf("failed to open coredump: %w", err) } - } else { + default: proc, err = process.OpenCoredump(cmd.coredumpPath) if err != nil { return fmt.Errorf("failed to open coredump `%s`: %w", cmd.coredumpPath, err) diff --git a/utils/coredump/clean.go b/tools/coredump/clean.go similarity index 96% rename from utils/coredump/clean.go rename to tools/coredump/clean.go index 9226c1be..9914b968 100644 --- a/utils/coredump/clean.go +++ b/tools/coredump/clean.go @@ -8,14 +8,16 @@ package main import ( "context" + "errors" "flag" "fmt" "time" - "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/utils/coredump/modulestore" "github.com/peterbourgon/ff/v3/ffcli" log "github.com/sirupsen/logrus" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/tools/coredump/modulestore" ) type cleanCmd struct { @@ -47,7 +49,7 @@ func newCleanCmd(store *modulestore.Store) *ffcli.Command { func (cmd *cleanCmd) exec(context.Context, []string) error { referenced, err := collectReferencedIDs() if err != nil { - return fmt.Errorf("failed to collect referenced IDs") + return errors.New("failed to collect referenced IDs") } for _, task := range []struct { diff --git a/utils/coredump/coredump.go b/tools/coredump/coredump.go similarity index 77% rename from utils/coredump/coredump.go rename to tools/coredump/coredump.go index 829d2ab8..f2cf26ec 100644 --- a/utils/coredump/coredump.go +++ b/tools/coredump/coredump.go @@ -19,16 +19,13 @@ import ( cebpf "github.com/cilium/ebpf" "github.com/elastic/otel-profiling-agent/config" - "github.com/elastic/otel-profiling-agent/host" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/nativeunwind" - "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/elfunwindinfo" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" - "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/elastic/otel-profiling-agent/libpf/process" "github.com/elastic/otel-profiling-agent/libpf/xsync" + "github.com/elastic/otel-profiling-agent/nativeunwind/elfunwindinfo" + "github.com/elastic/otel-profiling-agent/process" pm "github.com/elastic/otel-profiling-agent/processmanager" "github.com/elastic/otel-profiling-agent/support" + "github.com/elastic/otel-profiling-agent/util" ) // #include @@ -41,35 +38,13 @@ func sliceBuffer(buf unsafe.Pointer, sz C.int) []byte { return unsafe.Slice((*byte)(buf), int(sz)) } -// coredumpResourceOpener implements resourceOpener to provide access inside the coredump -// -// WARNING: this implementation is not actually compliant with the interpreter.ResourceOpener -// interface's requirement of being implemented thread-safe: we currently simply assume that -// with the way that we're calling the process manager in the coredump tests, we can't run into -// these race conditions. This is not a good idea because it relies on implementation details -// of the process manager and should probably be fixed at one point or another. -type coredumpResourceOpener struct { - process.Process -} - -var _ nativeunwind.StackDeltaProvider = &coredumpResourceOpener{} - -func (cro *coredumpResourceOpener) GetAndResetStatistics() nativeunwind.Statistics { - return nativeunwind.Statistics{} -} - -func (cro *coredumpResourceOpener) GetIntervalStructuresForFile(_ host.FileID, - elfRef *pfelf.Reference, interval *sdtypes.IntervalData) error { - return elfunwindinfo.ExtractELF(elfRef, interval) -} - type symbolKey struct { fileID libpf.FileID addressOrLine libpf.AddressOrLineno } type symbolData struct { - lineNumber libpf.SourceLineno + lineNumber util.SourceLineno functionOffset uint32 functionName string fileName string @@ -95,7 +70,7 @@ func (c *symbolizationCache) ExecutableMetadata(_ context.Context, fileID libpf. } func (c *symbolizationCache) FrameMetadata(fileID libpf.FileID, - addressOrLine libpf.AddressOrLineno, lineNumber libpf.SourceLineno, + addressOrLine libpf.AddressOrLineno, lineNumber util.SourceLineno, functionOffset uint32, functionName, filePath string) { key := symbolKey{fileID, addressOrLine} data := symbolData{lineNumber, @@ -163,7 +138,7 @@ func (c *symbolizationCache) symbolize(ty libpf.FrameType, fileID libpf.FileID, } func ExtractTraces(ctx context.Context, pr process.Process, debug bool, - lwpFilter libpf.Set[libpf.PID]) ([]ThreadInfo, error) { + lwpFilter libpf.Set[util.PID]) ([]ThreadInfo, error) { todo, cancel := context.WithCancel(ctx) defer cancel() @@ -175,8 +150,8 @@ func ExtractTraces(ctx context.Context, pr process.Process, debug bool, dummyMaps := make(map[string]*cebpf.Map) for _, mapName := range []string{"interpreter_offsets", "pid_page_to_mapping_info", "stack_delta_page_to_info", "pid_page_to_mapping_info", - "perl_procs", "py_procs", "hotspot_procs", "ruby_procs", "php_procs", - "v8_procs"} { + "dotnet_procs", "perl_procs", "py_procs", "hotspot_procs", "ruby_procs", + "php_procs", "v8_procs"} { dummyMaps[mapName] = &cebpf.Map{} } for i := support.StackDeltaBucketSmallest; i <= support.StackDeltaBucketLargest; i++ { @@ -218,18 +193,14 @@ func ExtractTraces(ctx context.Context, pr process.Process, debug bool, ebpfCtx := newEBPFContext(pr) defer ebpfCtx.release() - coredumpOpener := &coredumpResourceOpener{Process: pr} coredumpEbpfMaps := ebpfMapsCoredump{ctx: ebpfCtx} symCache := newSymbolizationCache() // Instantiate managers and enable all tracers by default - includeTracers := make([]bool, config.MaxTracers) - for i := range includeTracers { - includeTracers[i] = true - } + includeTracers, _ := config.ParseTracers("all") manager, err := pm.New(todo, includeTracers, monitorInterval, &coredumpEbpfMaps, - pm.NewMapFileIDMapper(), symCache, coredumpOpener, false) + pm.NewMapFileIDMapper(), symCache, elfunwindinfo.NewStackDeltaProvider(), false) if err != nil { return nil, fmt.Errorf("failed to get Interpreter manager: %v", err) } @@ -239,7 +210,7 @@ func ExtractTraces(ctx context.Context, pr process.Process, debug bool, info := make([]ThreadInfo, 0, len(threadInfo)) for _, thread := range threadInfo { if len(lwpFilter) > 0 { - if _, exists := lwpFilter[libpf.PID(thread.LWP)]; !exists { + if _, exists := lwpFilter[util.PID(thread.LWP)]; !exists { continue } } diff --git a/tools/coredump/coredump_test.go b/tools/coredump/coredump_test.go new file mode 100644 index 00000000..bfaabea6 --- /dev/null +++ b/tools/coredump/coredump_test.go @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package main + +// NOTE: temporarily disabled until we figured out how to best do this without S3 in the OTel env + +//nolint:gocritic +//func TestCoreDumps(t *testing.T) { +// cases, err := findTestCases(true) +// require.NoError(t, err) +// require.NotEmpty(t, cases) +// +// store := initModuleStore() +// +// for _, filename := range cases { +// filename := filename +// t.Run(filename, func(t *testing.T) { +// testCase, err := readTestCase(filename) +// require.NoError(t, err) +// +// ctx := context.Background() +// +// core, err := OpenStoreCoredump(store, testCase.CoredumpRef, testCase.Modules) +// require.NoError(t, err) +// defer core.Close() +// +// data, err := ExtractTraces(ctx, core, false, nil) +// +// require.NoError(t, err) +// require.Equal(t, testCase.Threads, data) +// }) +// } +//} diff --git a/utils/coredump/ebpfcode.go b/tools/coredump/ebpfcode.go similarity index 96% rename from utils/coredump/ebpfcode.go rename to tools/coredump/ebpfcode.go index 809c1a4e..54d272e6 100644 --- a/utils/coredump/ebpfcode.go +++ b/tools/coredump/ebpfcode.go @@ -49,6 +49,7 @@ int bpf_log(const char *fmt, ...) #include "../../support/ebpf/interpreter_dispatcher.ebpf.c" #include "../../support/ebpf/native_stack_trace.ebpf.c" +#include "../../support/ebpf/dotnet_tracer.ebpf.c" #include "../../support/ebpf/perl_tracer.ebpf.c" #include "../../support/ebpf/php_tracer.ebpf.c" #include "../../support/ebpf/python_tracer.ebpf.c" @@ -116,6 +117,9 @@ int bpf_tail_call(void *ctx, bpf_map_def *map, int index) case PROG_UNWIND_V8: rc = unwind_v8(ctx); break; + case PROG_UNWIND_DOTNET: + rc = unwind_dotnet(ctx); + break; default: return -1; } diff --git a/utils/coredump/ebpfcontext.go b/tools/coredump/ebpfcontext.go similarity index 97% rename from utils/coredump/ebpfcontext.go rename to tools/coredump/ebpfcontext.go index 01f7cb5d..0b7c7682 100644 --- a/utils/coredump/ebpfcontext.go +++ b/tools/coredump/ebpfcontext.go @@ -10,8 +10,8 @@ import ( "unsafe" "github.com/elastic/otel-profiling-agent/host" - "github.com/elastic/otel-profiling-agent/libpf/process" - "github.com/elastic/otel-profiling-agent/libpf/remotememory" + "github.com/elastic/otel-profiling-agent/process" + "github.com/elastic/otel-profiling-agent/remotememory" ) /* diff --git a/utils/coredump/ebpfhelpers.go b/tools/coredump/ebpfhelpers.go similarity index 87% rename from utils/coredump/ebpfhelpers.go rename to tools/coredump/ebpfhelpers.go index 9e8c540e..735d56f3 100644 --- a/utils/coredump/ebpfhelpers.go +++ b/tools/coredump/ebpfhelpers.go @@ -22,6 +22,7 @@ import ( "github.com/elastic/otel-profiling-agent/host" "github.com/elastic/otel-profiling-agent/libpf" "github.com/elastic/otel-profiling-agent/support" + "github.com/elastic/otel-profiling-agent/util" log "github.com/sirupsen/logrus" ) @@ -41,13 +42,14 @@ func __bpf_log(buf unsafe.Pointer, sz C.int) { //nolint:gocritic //export __push_frame -func __push_frame(id, file, line C.u64, frameType C.uchar) C.int { +func __push_frame(id, file, line C.u64, frameType C.uchar, returnAddress C.uchar) C.int { ctx := ebpfContextMap[id] ctx.trace.Frames = append(ctx.trace.Frames, host.Frame{ - File: host.FileID(file), - Lineno: libpf.AddressOrLineno(line), - Type: libpf.FrameType(frameType), + File: host.FileID(file), + Lineno: libpf.AddressOrLineno(line), + Type: libpf.FrameType(frameType), + ReturnAddress: returnAddress != 0, }) return C.ERR_OK @@ -56,7 +58,7 @@ func __push_frame(id, file, line C.u64, frameType C.uchar) C.int { //nolint:gocritic //export bpf_ktime_get_ns func bpf_ktime_get_ns() C.ulonglong { - return C.ulonglong(libpf.GetKTime()) + return C.ulonglong(util.GetKTime()) } //nolint:gocritic @@ -67,8 +69,8 @@ func bpf_get_current_comm(buf unsafe.Pointer, sz C.int) C.int { } //nolint:gocritic -//export __bpf_probe_read -func __bpf_probe_read(id C.u64, buf unsafe.Pointer, sz C.int, ptr unsafe.Pointer) C.long { +//export __bpf_probe_read_user +func __bpf_probe_read_user(id C.u64, buf unsafe.Pointer, sz C.int, ptr unsafe.Pointer) C.long { ctx := ebpfContextMap[id] dst := sliceBuffer(buf, sz) if _, err := ctx.remoteMemory.ReadAt(dst, int64(uintptr(ptr))); err != nil { @@ -101,8 +103,8 @@ func __bpf_map_lookup_elem(id C.u64, mapdef *C.bpf_map_def, keyptr unsafe.Pointe } case &C.per_cpu_records: return ctx.perCPURecord - case &C.interpreter_offsets, &C.perl_procs, &C.php_procs, &C.py_procs, &C.hotspot_procs, - &C.ruby_procs, &C.v8_procs: + case &C.interpreter_offsets, &C.dotnet_procs, &C.perl_procs, &C.php_procs, &C.py_procs, + &C.hotspot_procs, &C.ruby_procs, &C.v8_procs: var key any switch mapdef.key_size { case 8: diff --git a/utils/coredump/ebpfmaps.go b/tools/coredump/ebpfmaps.go similarity index 88% rename from utils/coredump/ebpfmaps.go rename to tools/coredump/ebpfmaps.go index 56c279e4..9d376b7f 100644 --- a/utils/coredump/ebpfmaps.go +++ b/tools/coredump/ebpfmaps.go @@ -14,11 +14,12 @@ import ( "github.com/elastic/otel-profiling-agent/host" "github.com/elastic/otel-profiling-agent/interpreter" "github.com/elastic/otel-profiling-agent/libpf" - sdtypes "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/stackdeltatypes" "github.com/elastic/otel-profiling-agent/lpm" "github.com/elastic/otel-profiling-agent/metrics" + sdtypes "github.com/elastic/otel-profiling-agent/nativeunwind/stackdeltatypes" pmebpf "github.com/elastic/otel-profiling-agent/processmanager/ebpf" "github.com/elastic/otel-profiling-agent/support" + "github.com/elastic/otel-profiling-agent/util" ) /* @@ -37,7 +38,7 @@ type ebpfMapsCoredump struct { var _ interpreter.EbpfHandler = &ebpfMapsCoredump{} -func (emc *ebpfMapsCoredump) RemoveReportedPID(libpf.PID) { +func (emc *ebpfMapsCoredump) RemoveReportedPID(util.PID) { } func (emc *ebpfMapsCoredump) CollectMetrics() []metrics.Metric { @@ -45,7 +46,7 @@ func (emc *ebpfMapsCoredump) CollectMetrics() []metrics.Metric { } func (emc *ebpfMapsCoredump) UpdateInterpreterOffsets(ebpfProgIndex uint16, - fileID host.FileID, offsetRanges []libpf.Range) error { + fileID host.FileID, offsetRanges []util.Range) error { offsetRange := offsetRanges[0] value := C.OffsetRange{ lower_offset: C.u64(offsetRange.Start), @@ -56,9 +57,11 @@ func (emc *ebpfMapsCoredump) UpdateInterpreterOffsets(ebpfProgIndex uint16, return nil } -func (emc *ebpfMapsCoredump) UpdateProcData(t libpf.InterpType, pid libpf.PID, +func (emc *ebpfMapsCoredump) UpdateProcData(t libpf.InterpreterType, pid util.PID, ptr unsafe.Pointer) error { switch t { + case libpf.Dotnet: + emc.ctx.addMap(&C.dotnet_procs, C.u32(pid), sliceBuffer(ptr, C.sizeof_DotnetProcInfo)) case libpf.Perl: emc.ctx.addMap(&C.perl_procs, C.u32(pid), sliceBuffer(ptr, C.sizeof_PerlProcInfo)) case libpf.PHP: @@ -75,8 +78,10 @@ func (emc *ebpfMapsCoredump) UpdateProcData(t libpf.InterpType, pid libpf.PID, return nil } -func (emc *ebpfMapsCoredump) DeleteProcData(t libpf.InterpType, pid libpf.PID) error { +func (emc *ebpfMapsCoredump) DeleteProcData(t libpf.InterpreterType, pid util.PID) error { switch t { + case libpf.Dotnet: + emc.ctx.delMap(&C.dotnet_procs, C.u32(pid)) case libpf.Perl: emc.ctx.delMap(&C.perl_procs, C.u32(pid)) case libpf.PHP: @@ -93,7 +98,7 @@ func (emc *ebpfMapsCoredump) DeleteProcData(t libpf.InterpType, pid libpf.PID) e return nil } -func (emc *ebpfMapsCoredump) UpdatePidInterpreterMapping(pid libpf.PID, +func (emc *ebpfMapsCoredump) UpdatePidInterpreterMapping(pid util.PID, prefix lpm.Prefix, interpreterProgram uint8, fileID host.FileID, bias uint64) error { ctx := emc.ctx // pid_page_to_mapping_info is a LPM trie and expects the pid and page @@ -122,7 +127,7 @@ func (emc *ebpfMapsCoredump) UpdatePidInterpreterMapping(pid libpf.PID, return nil } -func (emc *ebpfMapsCoredump) DeletePidInterpreterMapping(pid libpf.PID, +func (emc *ebpfMapsCoredump) DeletePidInterpreterMapping(pid util.PID, prefix lpm.Prefix) error { ctx := emc.ctx // pid_page_to_mapping_info is a LPM trie and expects the pid and page @@ -230,13 +235,13 @@ func (emc *ebpfMapsCoredump) DeleteStackDeltaPage(fileID host.FileID, page uint6 return nil } -func (emc *ebpfMapsCoredump) UpdatePidPageMappingInfo(pid libpf.PID, prefix lpm.Prefix, +func (emc *ebpfMapsCoredump) UpdatePidPageMappingInfo(pid util.PID, prefix lpm.Prefix, fileID, bias uint64) error { return emc.UpdatePidInterpreterMapping(pid, prefix, support.ProgUnwindNative, host.FileID(fileID), bias) } -func (emc *ebpfMapsCoredump) DeletePidPageMappingInfo(pid libpf.PID, prefixes []lpm.Prefix) (int, +func (emc *ebpfMapsCoredump) DeletePidPageMappingInfo(pid util.PID, prefixes []lpm.Prefix) (int, error) { var deleted int for _, prefix := range prefixes { diff --git a/utils/coredump/exportmodule.go b/tools/coredump/exportmodule.go similarity index 87% rename from utils/coredump/exportmodule.go rename to tools/coredump/exportmodule.go index 4f47b302..358cc3be 100644 --- a/utils/coredump/exportmodule.go +++ b/tools/coredump/exportmodule.go @@ -8,12 +8,13 @@ package main import ( "context" + "errors" "flag" "fmt" "github.com/peterbourgon/ff/v3/ffcli" - "github.com/elastic/otel-profiling-agent/utils/coredump/modulestore" + "github.com/elastic/otel-profiling-agent/tools/coredump/modulestore" ) type exportModuleCmd struct { @@ -41,10 +42,10 @@ func newExportModuleCmd(store *modulestore.Store) *ffcli.Command { func (cmd *exportModuleCmd) exec(context.Context, []string) error { if cmd.id == "" { - return fmt.Errorf("missing required argument `-id`") + return errors.New("missing required argument `-id`") } if cmd.out == "" { - return fmt.Errorf("missing required argument `-out`") + return errors.New("missing required argument `-out`") } id, err := modulestore.IDFromString(cmd.id) diff --git a/utils/coredump/gdb.go b/tools/coredump/gdb.go similarity index 95% rename from utils/coredump/gdb.go rename to tools/coredump/gdb.go index 288252c8..1adeb82d 100644 --- a/utils/coredump/gdb.go +++ b/tools/coredump/gdb.go @@ -9,6 +9,7 @@ package main import ( "context" "debug/elf" + "errors" "flag" "fmt" "os" @@ -17,11 +18,12 @@ import ( "path/filepath" "strings" - "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/elastic/otel-profiling-agent/libpf/process" - "github.com/elastic/otel-profiling-agent/utils/coredump/modulestore" "github.com/peterbourgon/ff/v3/ffcli" log "github.com/sirupsen/logrus" + + "github.com/elastic/otel-profiling-agent/libpf/pfelf" + "github.com/elastic/otel-profiling-agent/process" + "github.com/elastic/otel-profiling-agent/tools/coredump/modulestore" ) type gdbCmd struct { @@ -55,7 +57,7 @@ const sysrootBaseDir = "gdb-sysroot" func (cmd *gdbCmd) exec(context.Context, []string) (err error) { // Validate arguments. if cmd.casePath == "" { - return fmt.Errorf("please specify `-case`") + return errors.New("please specify `-case`") } var test *CoredumpTestCase @@ -104,7 +106,7 @@ func (cmd *gdbCmd) exec(context.Context, []string) (err error) { defer cd.Close() executable := cd.MainExecutable() if executable == "" { - return fmt.Errorf("failed to find main executable") + return errors.New("failed to find main executable") } // Unfortunately gdb doesn't use the mapping path and instead reads DSO diff --git a/utils/coredump/json.go b/tools/coredump/json.go similarity index 97% rename from utils/coredump/json.go rename to tools/coredump/json.go index e0c39b2f..042adc2e 100644 --- a/utils/coredump/json.go +++ b/tools/coredump/json.go @@ -15,7 +15,7 @@ import ( "path/filepath" "runtime" - "github.com/elastic/otel-profiling-agent/utils/coredump/modulestore" + "github.com/elastic/otel-profiling-agent/tools/coredump/modulestore" ) // CoredumpTestCase is the data structure generated from the core dump. diff --git a/utils/coredump/main.go b/tools/coredump/main.go similarity index 92% rename from utils/coredump/main.go rename to tools/coredump/main.go index c3d2c577..1c9a87c4 100644 --- a/utils/coredump/main.go +++ b/tools/coredump/main.go @@ -5,7 +5,7 @@ */ // coredump provides a tool for extracting stack traces from coredumps. -// It also includes a test suite to unit test pf-host-agent components against +// It also includes a test suite to unit test profiling agent components against // a set of coredumps to validate stack extraction code. package main @@ -19,7 +19,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" - "github.com/elastic/otel-profiling-agent/utils/coredump/modulestore" + "github.com/elastic/otel-profiling-agent/tools/coredump/modulestore" "github.com/peterbourgon/ff/v3/ffcli" log "github.com/sirupsen/logrus" ) diff --git a/utils/coredump/modulestore/id.go b/tools/coredump/modulestore/id.go similarity index 100% rename from utils/coredump/modulestore/id.go rename to tools/coredump/modulestore/id.go diff --git a/utils/coredump/modulestore/reader.go b/tools/coredump/modulestore/reader.go similarity index 100% rename from utils/coredump/modulestore/reader.go rename to tools/coredump/modulestore/reader.go diff --git a/utils/coredump/modulestore/store.go b/tools/coredump/modulestore/store.go similarity index 98% rename from utils/coredump/modulestore/store.go rename to tools/coredump/modulestore/store.go index d0df8f37..1a5c9109 100644 --- a/utils/coredump/modulestore/store.go +++ b/tools/coredump/modulestore/store.go @@ -28,7 +28,7 @@ import ( "github.com/elastic/otel-profiling-agent/libpf" "github.com/elastic/otel-profiling-agent/libpf/readatbuf" - zstpak "github.com/elastic/otel-profiling-agent/utils/zstpak/lib" + zstpak "github.com/elastic/otel-profiling-agent/tools/zstpak/lib" ) const ( @@ -57,7 +57,6 @@ type Store struct { s3client *s3.S3 bucket string localCachePath string - online bool } // New creates a new module storage. The modules present in the local cache are inspected and a @@ -86,7 +85,7 @@ func (store *Store) InsertModuleLocally(localPath string) (id ID, isNew bool, er } _, err = in.Seek(0, io.SeekStart) if err != nil { - return ID{}, false, fmt.Errorf("failed to seek file back to start") + return ID{}, false, errors.New("failed to seek file back to start") } present, err := store.IsPresentLocally(id) @@ -348,7 +347,7 @@ func (store *Store) ListRemoteModules() (map[ID]time.Time, error) { modules := map[ID]time.Time{} for _, object := range objectList { if object.Key == nil || object.LastModified == nil { - return nil, fmt.Errorf("s3 object lacks required field") + return nil, errors.New("s3 object lacks required field") } idText := strings.TrimPrefix(*object.Key, s3KeyPrefix) @@ -409,15 +408,10 @@ func (store *Store) ensurePresentLocally(id ID) (string, error) { if err != nil { return "", err } - if present { return localPath, nil } - if !store.online { - return "", fmt.Errorf("module store is operating in offline mode") - } - // Download the file to a temporary location to prevent half-complete modules on crashes. file, err := os.CreateTemp(store.localCachePath, localTempPrefix) if err != nil { diff --git a/utils/coredump/modulestore/util.go b/tools/coredump/modulestore/util.go similarity index 93% rename from utils/coredump/modulestore/util.go rename to tools/coredump/modulestore/util.go index 974f95e0..e86d7af1 100644 --- a/utils/coredump/modulestore/util.go +++ b/tools/coredump/modulestore/util.go @@ -7,6 +7,7 @@ package modulestore import ( + "errors" "fmt" "github.com/aws/aws-sdk-go/service/s3" @@ -36,7 +37,7 @@ func getS3ObjectList(client *s3.S3, bucket, prefix string, itemLimit int) ([]*s3 break } if len(resp.Contents) > itemLimit { - return nil, fmt.Errorf("too many matching items in bucket") + return nil, errors.New("too many matching items in bucket") } contToken = resp.ContinuationToken diff --git a/utils/coredump/new.go b/tools/coredump/new.go similarity index 93% rename from utils/coredump/new.go rename to tools/coredump/new.go index 3bece5df..fcebed88 100644 --- a/utils/coredump/new.go +++ b/tools/coredump/new.go @@ -8,18 +8,20 @@ package main import ( "context" + "errors" "flag" "fmt" "os" "os/exec" "strconv" - "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/elastic/otel-profiling-agent/libpf/process" - "github.com/elastic/otel-profiling-agent/utils/coredump/modulestore" "github.com/peterbourgon/ff/v3/ffcli" log "github.com/sirupsen/logrus" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/libpf/pfelf" + "github.com/elastic/otel-profiling-agent/process" + "github.com/elastic/otel-profiling-agent/tools/coredump/modulestore" ) // gcorePathPrefix specifies the path prefix we ask gcore to use when creating coredumps. @@ -57,12 +59,12 @@ func newTrackedCoredump(corePath, filePrefix string) (*trackedCoredump, error) { }, nil } -func (tc *trackedCoredump) GetMappingFile(_ *process.Mapping) string { - return "" +func (tc *trackedCoredump) GetMappingFileLastModified(_ *process.Mapping) int64 { + return 0 } func (tc *trackedCoredump) CalculateMappingFileID(m *process.Mapping) (libpf.FileID, error) { - fid, err := pfelf.CalculateID(tc.prefix + m.Path) + fid, err := libpf.FileIDFromExecutableFile(tc.prefix + m.Path) if err == nil { tc.seen[m.Path] = libpf.Void{} } @@ -112,10 +114,10 @@ func newNewCmd(store *modulestore.Store) *ffcli.Command { func (cmd *newCmd) exec(context.Context, []string) (err error) { // Validate arguments. if (cmd.coredumpPath == "") != (cmd.pid != 0) { - return fmt.Errorf("please specify either `-core` or `-pid` (but not both)") + return errors.New("please specify either `-core` or `-pid` (but not both)") } if cmd.name == "" { - return fmt.Errorf("missing required argument `-name`") + return errors.New("missing required argument `-name`") } var corePath string diff --git a/utils/coredump/rebase.go b/tools/coredump/rebase.go similarity index 93% rename from utils/coredump/rebase.go rename to tools/coredump/rebase.go index 423dca2b..6a300d8c 100644 --- a/utils/coredump/rebase.go +++ b/tools/coredump/rebase.go @@ -8,12 +8,14 @@ package main import ( "context" + "errors" "flag" "fmt" "os/exec" - "github.com/elastic/otel-profiling-agent/utils/coredump/modulestore" "github.com/peterbourgon/ff/v3/ffcli" + + "github.com/elastic/otel-profiling-agent/tools/coredump/modulestore" ) type rebaseCmd struct { @@ -45,7 +47,7 @@ func (cmd *rebaseCmd) exec(context.Context, []string) (err error) { if !cmd.allowDirty { if err = exec.Command("git", "diff", "--quiet").Run(); err != nil { - return fmt.Errorf("refusing to work on a dirty source tree. " + + return errors.New("refusing to work on a dirty source tree. " + "please commit your changes first or pass `-allow-dirty` to ignore") } } diff --git a/utils/coredump/storecoredump.go b/tools/coredump/storecoredump.go similarity index 95% rename from utils/coredump/storecoredump.go rename to tools/coredump/storecoredump.go index 0f160772..5c9bdcc0 100644 --- a/utils/coredump/storecoredump.go +++ b/tools/coredump/storecoredump.go @@ -12,8 +12,8 @@ import ( "os" "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/elastic/otel-profiling-agent/libpf/process" - "github.com/elastic/otel-profiling-agent/utils/coredump/modulestore" + "github.com/elastic/otel-profiling-agent/process" + "github.com/elastic/otel-profiling-agent/tools/coredump/modulestore" log "github.com/sirupsen/logrus" ) diff --git a/tools/coredump/testdata/amd64/.gitkeep b/tools/coredump/testdata/amd64/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/utils/coredump/testdata/amd64/brokenstack.json b/tools/coredump/testdata/amd64/brokenstack.json similarity index 100% rename from utils/coredump/testdata/amd64/brokenstack.json rename to tools/coredump/testdata/amd64/brokenstack.json diff --git a/tools/coredump/testdata/amd64/dotnet7-helloworld-alpine.json b/tools/coredump/testdata/amd64/dotnet7-helloworld-alpine.json new file mode 100644 index 00000000..7d2cc819 --- /dev/null +++ b/tools/coredump/testdata/amd64/dotnet7-helloworld-alpine.json @@ -0,0 +1,282 @@ +{ + "coredump-ref": "73ac1e25a02a0cd1f7df2960ba90fc85e2fac5d851dba9a1d4778e5b66a1b1fd", + "threads": [ + { + "lwp": 18275, + "frames": [ + "ld-musl-x86_64.so.1+0x5e862", + "ld-musl-x86_64.so.1+0x5b4fb", + "ld-musl-x86_64.so.1+0x62943", + "libSystem.Native.so+0xe12b", + "Interop/Sys.g____PInvoke|37_0+0 in System.Console.dll:0", + "Interop/Sys.Write+0 in System.Console.dll:0", + "System.ConsolePal.Write+0 in System.Console.dll:0", + "System.IO.StreamWriter.Flush+0 in System.Private.CoreLib.dll:0", + "System.IO.StreamWriter.WriteLine+0 in System.Private.CoreLib.dll:0", + "System.IO.TextWriter/SyncTextWriter.WriteLine+0 in System.Private.CoreLib.dll:0", + "System.Console.WriteLine+0 in System.Console.dll:0", + "foo.bar+6 in helloworld.dll:0", + "main.Main+11 in helloworld.dll:0", + "libcoreclr.so+0x4c23d6", + "libcoreclr.so+0x2f05ff", + "libcoreclr.so+0x1cfbc1", + "libcoreclr.so+0x1cff63", + "libcoreclr.so+0x1fdaba", + "libcoreclr.so+0x1bbd44", + "libhostpolicy.so+0x26bb0", + "libhostpolicy.so+0x279fa", + "libhostfxr.so+0x214b2", + "libhostfxr.so+0x20483", + "libhostfxr.so+0x1c74b", + "helloworld+0x13db8", + "helloworld+0x140a1", + "ld-musl-x86_64.so.1+0x1c6d0", + "helloworld+0x73e5", + "" + ] + }, + { + "lwp": 18276, + "frames": [ + "ld-musl-x86_64.so.1+0x3907a", + "liblttng-ust.so.1.0.0+0xa53e", + "ld-musl-x86_64.so.1+0x5c22d", + "ld-musl-x86_64.so.1+0x5e82e" + ] + }, + { + "lwp": 18277, + "frames": [ + "ld-musl-x86_64.so.1+0x3907a", + "liblttng-ust.so.1.0.0+0xa53e", + "ld-musl-x86_64.so.1+0x5c22d", + "ld-musl-x86_64.so.1+0x5e82e" + ] + }, + { + "lwp": 18278, + "frames": [ + "ld-musl-x86_64.so.1+0x5e862", + "ld-musl-x86_64.so.1+0x5b4fb", + "ld-musl-x86_64.so.1+0x4d3da", + "libcoreclr.so+0x63ca5e", + "libcoreclr.so+0x63c062", + "libcoreclr.so+0x64679a", + "ld-musl-x86_64.so.1+0x5c22d", + "ld-musl-x86_64.so.1+0x5e82e" + ] + }, + { + "lwp": 18279, + "frames": [ + "ld-musl-x86_64.so.1+0x5e862", + "ld-musl-x86_64.so.1+0x5b4fb", + "ld-musl-x86_64.so.1+0x4d3da", + "libcoreclr.so+0x52d57b", + "libcoreclr.so+0x5eab6b", + "libcoreclr.so+0x5e84c5", + "libcoreclr.so+0x64679a", + "ld-musl-x86_64.so.1+0x5c22d", + "ld-musl-x86_64.so.1+0x5e82e" + ] + }, + { + "lwp": 18280, + "frames": [ + "ld-musl-x86_64.so.1+0x5e862", + "ld-musl-x86_64.so.1+0x5b4fb", + "ld-musl-x86_64.so.1+0x1d02f", + "libcoreclr.so+0x52e09f", + "libcoreclr.so+0x527e17", + "libcoreclr.so+0x526fa8", + "libcoreclr.so+0x64679a", + "ld-musl-x86_64.so.1+0x5c22d", + "ld-musl-x86_64.so.1+0x5e82e" + ] + }, + { + "lwp": 18281, + "frames": [ + "ld-musl-x86_64.so.1+0x5e862", + "ld-musl-x86_64.so.1+0x5b4fb", + "ld-musl-x86_64.so.1+0x5aa2d", + "ld-musl-x86_64.so.1+0x5b893", + "libcoreclr.so+0x63a40e", + "libcoreclr.so+0x639ff2", + "libcoreclr.so+0x63f0c5", + "libcoreclr.so+0x63f394", + "libcoreclr.so+0x52565d", + "libcoreclr.so+0x5254d1", + "libcoreclr.so+0x5251bd", + "libcoreclr.so+0x64679a", + "ld-musl-x86_64.so.1+0x5c22d", + "ld-musl-x86_64.so.1+0x5e82e" + ] + }, + { + "lwp": 18282, + "frames": [ + "ld-musl-x86_64.so.1+0x5e862", + "ld-musl-x86_64.so.1+0x5b4fb", + "ld-musl-x86_64.so.1+0x5aa2d", + "ld-musl-x86_64.so.1+0x5b893", + "libcoreclr.so+0x63a3a5", + "libcoreclr.so+0x639ff2", + "libcoreclr.so+0x63f0c5", + "libcoreclr.so+0x63f2ba", + "libcoreclr.so+0x3c66d0", + "libcoreclr.so+0x33169f", + "libcoreclr.so+0x3318b0", + "libcoreclr.so+0x2b9847", + "libcoreclr.so+0x2b9edc", + "libcoreclr.so+0x331b35", + "libcoreclr.so+0x64679a", + "ld-musl-x86_64.so.1+0x5c22d", + "ld-musl-x86_64.so.1+0x5e82e" + ] + }, + { + "lwp": 18283, + "frames": [ + "ld-musl-x86_64.so.1+0x5e862", + "ld-musl-x86_64.so.1+0x5b4fb", + "ld-musl-x86_64.so.1+0x5aa2d", + "ld-musl-x86_64.so.1+0x5b893", + "libcoreclr.so+0x63a3a5", + "libcoreclr.so+0x639ff2", + "libcoreclr.so+0x63f0c5", + "libcoreclr.so+0x63f2ba", + "libcoreclr.so+0x3c66d0", + "libcoreclr.so+0x2bd2cd", + "libcoreclr.so+0x2bd0cf", + "libcoreclr.so+0x2b9847", + "libcoreclr.so+0x2b9e3c", + "libcoreclr.so+0x2bcfe1", + "libcoreclr.so+0x64679a", + "ld-musl-x86_64.so.1+0x5c22d", + "ld-musl-x86_64.so.1+0x5e82e" + ] + }, + { + "lwp": 18284, + "frames": [ + "ld-musl-x86_64.so.1+0x5e862", + "ld-musl-x86_64.so.1+0x5b4fb", + "ld-musl-x86_64.so.1+0x62453", + "libSystem.Native.so+0x13eac", + "ld-musl-x86_64.so.1+0x5c22d", + "ld-musl-x86_64.so.1+0x5e82e" + ] + } + ], + "modules": [ + { + "ref": "776d90789fa9fd5c4f81a3f6e2a3ed554ae166b953e0f770e9c9fd9038337b3e", + "local-path": "/usr/lib/dotnet/shared/Microsoft.NETCore.App/7.0.15/libSystem.Native.so" + }, + { + "ref": "e210d2570148b34a6dfee7d7a7be445692349e560e62c9c387a057c695d807b0", + "local-path": "/usr/lib/dotnet/shared/Microsoft.NETCore.App/7.0.15/libhostpolicy.so" + }, + { + "ref": "60c291ca39bb3f74875082734d471b13d2e1eea77b92bb4118e93eaded0d0ae3", + "local-path": "/usr/lib/liblttng-ust.so.1.0.0" + }, + { + "ref": "23fc12a14cb747b47a15a51408f6a268830b1e85bf1c153cc1cc75f450bea22e", + "local-path": "/usr/lib/liblttng-ust-tracepoint.so.1.0.0" + }, + { + "ref": "f6b52d2850dbefeafafae2a68387ab046e67b41d43b533b597010ae144f76c71", + "local-path": "/usr/lib/dotnet/shared/Microsoft.NETCore.App/7.0.15/System.Threading.dll" + }, + { + "ref": "b924d78213249ab613053fa70010e0441cf9043a85e9a86f1ffc5f91ce075a71", + "local-path": "/usr/lib/dotnet/shared/Microsoft.NETCore.App/7.0.15/System.Memory.dll" + }, + { + "ref": "05993db7e47374e7d0f982de9a71d75ee2b80299277ac41c6159e6f59531602e", + "local-path": "/usr/lib/liblttng-ust-common.so.1.0.0" + }, + { + "ref": "16c1e108145e26ee4b2c693548216428cadbcc8499b1a9993442c490f4aa3590", + "local-path": "/lib/ld-musl-x86_64.so.1" + }, + { + "ref": "2c4b0e8c9baf016bcb2a0e27fa09cacdd1adae09457527687289b9cfdd4670a5", + "local-path": "/usr/lib/liblzma.so.5.4.6" + }, + { + "ref": "1887c9cc2480ef8637295b7879d10ea8c6b35a039d8e96b45ed6e57a230308d5", + "local-path": "/usr/lib/dotnet/shared/Microsoft.NETCore.App/7.0.15/libclrjit.so" + }, + { + "ref": "87e8acb3465797c17ead6d93dccb4d13a29f3f7278d99e42b74db19ce6d6de53", + "local-path": "/usr/lib/dotnet/shared/Microsoft.NETCore.App/7.0.15/System.Collections.dll" + }, + { + "ref": "de31deb2d73597dad01419b929b8a1db447be4a32fb8b0fe67beadb2fc18a765", + "local-path": "/usr/lib/dotnet/shared/Microsoft.NETCore.App/7.0.15/System.Runtime.dll" + }, + { + "ref": "df6ac74769e875f7a3e1e6de0bec966abdf6c7f2e39a11d07e1733c935dd774a", + "local-path": "/usr/lib/dotnet/shared/Microsoft.NETCore.App/7.0.15/libcoreclr.so" + }, + { + "ref": "f5fc0d187e101bc067c1886855dafffe243944642af21aac05792d9ae98fc35c", + "local-path": "/usr/lib/dotnet/shared/Microsoft.NETCore.App/7.0.15/System.Console.dll" + }, + { + "ref": "d982c7bec1da3d41b0060e7c539f2c0c50a585f4000cf32df248f7563b17b3ad", + "local-path": "/usr/lib/dotnet/shared/Microsoft.NETCore.App/7.0.15/System.Runtime.InteropServices.dll" + }, + { + "ref": "496613c0201dcb5f15a4e0b778a3928d8a5286713392d5b78a21b7845ebc3589", + "local-path": "/usr/lib/libstdc++.so.6.0.32" + }, + { + "ref": "7ed71f8fef614293ab64edf3d3486098bcc4465a210a6a6e83670485e644425a", + "local-path": "/home/tteras/work/elastic/prodfiler/utils/coredump/testsources/dotnet/helloworld/bin/Debug/net7.0/helloworld" + }, + { + "ref": "bd8f795ebdbdaf2fa51f153938853a95329e3fbbb399f123d9a06282c0a77189", + "local-path": "/usr/lib/libicuuc.so.74.2" + }, + { + "ref": "40df770f2f067ace6c24e8c222c62b491b73cfcab4a8145b2ebc100b6f854bb5", + "local-path": "/usr/lib/dotnet/shared/Microsoft.NETCore.App/7.0.15/System.Private.CoreLib.dll" + }, + { + "ref": "c3f27a1d6769d417cffd9a5cdf8b27e5609c36484bc9eda2dcf3c164e43eda6a", + "local-path": "/usr/lib/libunwind.so.8.1.0" + }, + { + "ref": "3278ecac090c946e5c772bfe8c8d9f692bdd2cca9d51fdf4c6fab8ac7387872a", + "local-path": "/usr/lib/dotnet/shared/Microsoft.NETCore.App/7.0.15/Microsoft.Win32.Primitives.dll" + }, + { + "ref": "01b9aeddca8894e89d84d613b438542c3ecadb84cc1da29abecf8d8c622eb6cb", + "local-path": "/home/tteras/work/elastic/prodfiler/utils/coredump/testsources/dotnet/helloworld/bin/Debug/net7.0/helloworld.dll" + }, + { + "ref": "28f353a9b42e06850e522ee57326c3bc08d71cd1721d09bca87985f83f819b6d", + "local-path": "/usr/lib/libicui18n.so.74.2" + }, + { + "ref": "f9adede2520e1107db25974bdf362d953352521ab86ae79fc836f713e9e2d3c6", + "local-path": "/usr/lib/dotnet/host/fxr/7.0.15/libhostfxr.so" + }, + { + "ref": "a3a07607d41a4475162c5d53834944b6ec1dfbda4fcd41eb7d9d6740f328deb1", + "local-path": "/usr/lib/libgcc_s.so.1" + }, + { + "ref": "9191078a52463a0c2e7c20b241e6605efe812ff6a831bb56f04c789f82701ee0", + "local-path": "/usr/lib/dotnet/shared/Microsoft.NETCore.App/7.0.15/libcoreclrtraceptprovider.so" + }, + { + "ref": "ca95248c3b9da61b945c031c8bd0a97740f27d1681141e24bd3a791606863ad0", + "local-path": "/usr/lib/debug/lib/ld-musl-x86_64.so.1.debug" + } + ] +} diff --git a/utils/coredump/testdata/amd64/glibc-signalframe.json b/tools/coredump/testdata/amd64/glibc-signalframe.json similarity index 96% rename from utils/coredump/testdata/amd64/glibc-signalframe.json rename to tools/coredump/testdata/amd64/glibc-signalframe.json index 0952e2d0..f8c90a1b 100644 --- a/utils/coredump/testdata/amd64/glibc-signalframe.json +++ b/tools/coredump/testdata/amd64/glibc-signalframe.json @@ -9,7 +9,7 @@ "libc.so.6+0xd3b89", "sig+0x1182", "libc.so.6+0x3bf8f", - "libc.so.6+0xcf302", + "libc.so.6+0xcf303", "libc.so.6+0xd3c52", "libc.so.6+0xd3b89", "sig+0x11bc", diff --git a/utils/coredump/testdata/amd64/graalvm-native.json b/tools/coredump/testdata/amd64/graalvm-native.json similarity index 100% rename from utils/coredump/testdata/amd64/graalvm-native.json rename to tools/coredump/testdata/amd64/graalvm-native.json diff --git a/utils/coredump/testdata/amd64/java-17.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256msg2.json b/tools/coredump/testdata/amd64/java-17.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256msg2.json similarity index 100% rename from utils/coredump/testdata/amd64/java-17.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256msg2.json rename to tools/coredump/testdata/amd64/java-17.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256msg2.json diff --git a/utils/coredump/testdata/amd64/java11.25812.json b/tools/coredump/testdata/amd64/java11.25812.json similarity index 100% rename from utils/coredump/testdata/amd64/java11.25812.json rename to tools/coredump/testdata/amd64/java11.25812.json diff --git a/utils/coredump/testdata/amd64/java12.20153.json b/tools/coredump/testdata/amd64/java12.20153.json similarity index 100% rename from utils/coredump/testdata/amd64/java12.20153.json rename to tools/coredump/testdata/amd64/java12.20153.json diff --git a/utils/coredump/testdata/amd64/java13.11581.json b/tools/coredump/testdata/amd64/java13.11581.json similarity index 100% rename from utils/coredump/testdata/amd64/java13.11581.json rename to tools/coredump/testdata/amd64/java13.11581.json diff --git a/utils/coredump/testdata/amd64/java14.3576.json b/tools/coredump/testdata/amd64/java14.3576.json similarity index 100% rename from utils/coredump/testdata/amd64/java14.3576.json rename to tools/coredump/testdata/amd64/java14.3576.json diff --git a/utils/coredump/testdata/amd64/java16.43723.json b/tools/coredump/testdata/amd64/java16.43723.json similarity index 99% rename from utils/coredump/testdata/amd64/java16.43723.json rename to tools/coredump/testdata/amd64/java16.43723.json index 854dc02f..56ece50b 100644 --- a/utils/coredump/testdata/amd64/java16.43723.json +++ b/tools/coredump/testdata/amd64/java16.43723.json @@ -12,7 +12,7 @@ "libjvm.so+0xf303a1", "libjvm.so+0xdcd7fd", "libc-2.31.so+0x4620f", - "libpthread-2.31.so+0xacd4", + "libpthread-2.31.so+0xacd5", "libjli.so+0x904e", "libjli.so+0x5d80", "libjli.so+0x76d4", diff --git a/utils/coredump/testdata/amd64/java16.43768.json b/tools/coredump/testdata/amd64/java16.43768.json similarity index 99% rename from utils/coredump/testdata/amd64/java16.43768.json rename to tools/coredump/testdata/amd64/java16.43768.json index 268a85a7..c660dca1 100644 --- a/utils/coredump/testdata/amd64/java16.43768.json +++ b/tools/coredump/testdata/amd64/java16.43768.json @@ -12,7 +12,7 @@ "libjvm.so+0xf303a1", "libjvm.so+0xdcd7fd", "libc-2.31.so+0x4620f", - "libpthread-2.31.so+0xacd4", + "libpthread-2.31.so+0xacd5", "libjli.so+0x904e", "libjli.so+0x5d80", "libjli.so+0x76d4", diff --git a/utils/coredump/testdata/amd64/java20-helloworld.json b/tools/coredump/testdata/amd64/java20-helloworld.json similarity index 100% rename from utils/coredump/testdata/amd64/java20-helloworld.json rename to tools/coredump/testdata/amd64/java20-helloworld.json diff --git a/utils/coredump/testdata/amd64/java7.10958.json b/tools/coredump/testdata/amd64/java7.10958.json similarity index 100% rename from utils/coredump/testdata/amd64/java7.10958.json rename to tools/coredump/testdata/amd64/java7.10958.json diff --git a/utils/coredump/testdata/amd64/java8.10171.json b/tools/coredump/testdata/amd64/java8.10171.json similarity index 100% rename from utils/coredump/testdata/amd64/java8.10171.json rename to tools/coredump/testdata/amd64/java8.10171.json diff --git a/utils/coredump/testdata/amd64/musl-signalframe.json b/tools/coredump/testdata/amd64/musl-signalframe.json similarity index 96% rename from utils/coredump/testdata/amd64/musl-signalframe.json rename to tools/coredump/testdata/amd64/musl-signalframe.json index 1df2dac8..a0a52e6d 100644 --- a/utils/coredump/testdata/amd64/musl-signalframe.json +++ b/tools/coredump/testdata/amd64/musl-signalframe.json @@ -11,7 +11,7 @@ "ld-musl-x86_64.so.1+0x5846a", "sig+0x11be", "ld-musl-x86_64.so.1+0x4662b", - "ld-musl-x86_64.so.1+0x54f09", + "ld-musl-x86_64.so.1+0x54f0a", "ld-musl-x86_64.so.1+0x520ed", "ld-musl-x86_64.so.1+0x561dd", "ld-musl-x86_64.so.1+0x56546", diff --git a/utils/coredump/testdata/amd64/node1610.26681.json b/tools/coredump/testdata/amd64/node1610.26681.json similarity index 100% rename from utils/coredump/testdata/amd64/node1610.26681.json rename to tools/coredump/testdata/amd64/node1610.26681.json diff --git a/utils/coredump/testdata/amd64/node1617-inlining.json b/tools/coredump/testdata/amd64/node1617-inlining.json similarity index 100% rename from utils/coredump/testdata/amd64/node1617-inlining.json rename to tools/coredump/testdata/amd64/node1617-inlining.json diff --git a/utils/coredump/testdata/amd64/node1815-baseline.json b/tools/coredump/testdata/amd64/node1815-baseline.json similarity index 100% rename from utils/coredump/testdata/amd64/node1815-baseline.json rename to tools/coredump/testdata/amd64/node1815-baseline.json diff --git a/utils/coredump/testdata/amd64/node1816-async.json b/tools/coredump/testdata/amd64/node1816-async.json similarity index 100% rename from utils/coredump/testdata/amd64/node1816-async.json rename to tools/coredump/testdata/amd64/node1816-async.json diff --git a/utils/coredump/testdata/amd64/node189-inlining.json b/tools/coredump/testdata/amd64/node189-inlining.json similarity index 100% rename from utils/coredump/testdata/amd64/node189-inlining.json rename to tools/coredump/testdata/amd64/node189-inlining.json diff --git a/utils/coredump/testdata/amd64/node211.json b/tools/coredump/testdata/amd64/node211.json similarity index 100% rename from utils/coredump/testdata/amd64/node211.json rename to tools/coredump/testdata/amd64/node211.json diff --git a/tools/coredump/testdata/amd64/openssl-gcm.json b/tools/coredump/testdata/amd64/openssl-gcm.json new file mode 100644 index 00000000..8e11deef --- /dev/null +++ b/tools/coredump/testdata/amd64/openssl-gcm.json @@ -0,0 +1,45 @@ +{ + "coredump-ref": "8cda7773f5eb71c6a4039c215dc80575810061a3ddeb998eec1a70a43538d6ba", + "threads": [ + { + "lwp": 17068, + "frames": [ + "libcrypto.so.3+0x575c0", + "libcrypto.so.3+0x22542f", + "libcrypto.so.3+0x2b7ec8", + "libcrypto.so.3+0x2ed50c", + "libcrypto.so.3+0x2ede24", + "libcrypto.so.3+0x14b6d5", + "openssl+0x54967", + "openssl+0x55595", + "openssl+0x57d15", + "openssl+0x3a5bb", + "openssl+0x1c240", + "ld-musl-x86_64.so.1+0x1c6d0", + "openssl+0x1c2ed" + ] + } + ], + "modules": [ + { + "ref": "ca95248c3b9da61b945c031c8bd0a97740f27d1681141e24bd3a791606863ad0", + "local-path": "/usr/lib/debug/lib/ld-musl-x86_64.so.1.debug" + }, + { + "ref": "85ae4badf742d07f643e644d1b9f63e03b26ce7bcd9a39166af37e0ca90883d8", + "local-path": "/lib/libcrypto.so.3" + }, + { + "ref": "898a5f8b56c3c24554799885201c9c8aa1e3ed8c7b71b2cb298615d136fcbdb2", + "local-path": "/lib/libssl.so.3" + }, + { + "ref": "a5efce0d5ad293660ba30a4b4b7e059dbf61ab902e04353cfc0510e34118f375", + "local-path": "/usr/bin/openssl" + }, + { + "ref": "16c1e108145e26ee4b2c693548216428cadbcc8499b1a9993442c490f4aa3590", + "local-path": "/lib/ld-musl-x86_64.so.1" + } + ] +} diff --git a/utils/coredump/testdata/amd64/openssl.14327.json b/tools/coredump/testdata/amd64/openssl.14327.json similarity index 100% rename from utils/coredump/testdata/amd64/openssl.14327.json rename to tools/coredump/testdata/amd64/openssl.14327.json diff --git a/utils/coredump/testdata/amd64/perl528.14.json b/tools/coredump/testdata/amd64/perl528.14.json similarity index 100% rename from utils/coredump/testdata/amd64/perl528.14.json rename to tools/coredump/testdata/amd64/perl528.14.json diff --git a/utils/coredump/testdata/amd64/perl528.151.json b/tools/coredump/testdata/amd64/perl528.151.json similarity index 100% rename from utils/coredump/testdata/amd64/perl528.151.json rename to tools/coredump/testdata/amd64/perl528.151.json diff --git a/utils/coredump/testdata/amd64/perl528.206.json b/tools/coredump/testdata/amd64/perl528.206.json similarity index 100% rename from utils/coredump/testdata/amd64/perl528.206.json rename to tools/coredump/testdata/amd64/perl528.206.json diff --git a/utils/coredump/testdata/amd64/perl528bking.13.json b/tools/coredump/testdata/amd64/perl528bking.13.json similarity index 100% rename from utils/coredump/testdata/amd64/perl528bking.13.json rename to tools/coredump/testdata/amd64/perl528bking.13.json diff --git a/utils/coredump/testdata/amd64/perl534.220234.json b/tools/coredump/testdata/amd64/perl534.220234.json similarity index 100% rename from utils/coredump/testdata/amd64/perl534.220234.json rename to tools/coredump/testdata/amd64/perl534.220234.json diff --git a/utils/coredump/testdata/amd64/perl536-a.json b/tools/coredump/testdata/amd64/perl536-a.json similarity index 100% rename from utils/coredump/testdata/amd64/perl536-a.json rename to tools/coredump/testdata/amd64/perl536-a.json diff --git a/utils/coredump/testdata/amd64/perl536-helloWorld.json b/tools/coredump/testdata/amd64/perl536-helloWorld.json similarity index 100% rename from utils/coredump/testdata/amd64/perl536-helloWorld.json rename to tools/coredump/testdata/amd64/perl536-helloWorld.json diff --git a/utils/coredump/testdata/amd64/php-8.2.10-prime.json b/tools/coredump/testdata/amd64/php-8.2.10-prime.json similarity index 100% rename from utils/coredump/testdata/amd64/php-8.2.10-prime.json rename to tools/coredump/testdata/amd64/php-8.2.10-prime.json diff --git a/tools/coredump/testdata/amd64/php-8.3.6-prime.json b/tools/coredump/testdata/amd64/php-8.3.6-prime.json new file mode 100644 index 00000000..c8626289 --- /dev/null +++ b/tools/coredump/testdata/amd64/php-8.3.6-prime.json @@ -0,0 +1,88 @@ +{ + "coredump-ref": "b1affa87ffe2fa89c1338493c80fb2cd252d00c60a59a8fe69345e974922c1e0", + "threads": [ + { + "lwp": 215616, + "frames": [ + "php8.3+0x338210", + "is_prime+13 in /home/paplo/sources/prodfiler/utils/coredump/testsources/php/prime.php:16", + "+33 in /home/paplo/sources/prodfiler/utils/coredump/testsources/php/prime.php:34", + "php8.3+0x3780e6", + "php8.3+0x381c24", + "php8.3+0x30c62f", + "php8.3+0x2a0a69", + "php8.3+0x3fa312", + "php8.3+0x12faa5", + "libc.so.6+0x29d8f", + "libc.so.6+0x29e3f", + "php8.3+0x130b24" + ] + } + ], + "modules": [ + { + "ref": "2361eaf7645b2014106c0c05644875b28476fe14d21d9af9d2d94d61d3da5fef", + "local-path": "/usr/lib/x86_64-linux-gnu/libsodium.so.23.3.0" + }, + { + "ref": "64c206f0146cc58bbddc4f22054436f4ff278f5a554aa3ce6921ddf7e9133370", + "local-path": "/usr/lib/x86_64-linux-gnu/libz.so.1.2.11" + }, + { + "ref": "a4a774ebdd25c5e25087215d0a2f1a428bd05c761b9f77cd14916e62d7ba01ed", + "local-path": "/usr/lib/x86_64-linux-gnu/libargon2.so.1" + }, + { + "ref": "c1404396288e178c8db2f30203cb8150e4d2c14cacee9a2883e0092f45399cc8", + "local-path": "/usr/lib/x86_64-linux-gnu/libicudata.so.70.1" + }, + { + "ref": "cced7fead25da92624ffc0076b8dc971e7362433eefd4b8d658d358737d64ac9", + "local-path": "/usr/lib/x86_64-linux-gnu/libm.so.6" + }, + { + "ref": "39c9bb846d8491ec41e89b7d34b824e7b6e7ff37d7b9de549305a15c2f7a6cf7", + "local-path": "/usr/lib/x86_64-linux-gnu/libgcc_s.so.1" + }, + { + "ref": "801de193ceab2e1a7ee297cfbe3405052a17ad063a7fa6dfa059e43a9678df1d", + "local-path": "/usr/lib/x86_64-linux-gnu/libcrypto.so.3" + }, + { + "ref": "189fad5d58df5b9583578931c00f74277fbd9f82ab479cd207c6bd09c5410ef6", + "local-path": "/usr/lib/x86_64-linux-gnu/libpcre2-8.so.0.11.2" + }, + { + "ref": "6c5c22f8b27740d4f71ad0f9a0465f51c3f6a3ff6132a9a56cde2286d127f06b", + "local-path": "/usr/bin/php8.3" + }, + { + "ref": "491c2eb04e0b72ac00beaebadf181ce5c9e9545e223c58924cfc65511d8ffe6a", + "local-path": "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" + }, + { + "ref": "091d763cd9b90610062aa6bad6350922d1e97886e5ee242aca617140bf3db631", + "local-path": "/usr/lib/x86_64-linux-gnu/libssl.so.3" + }, + { + "ref": "67d9e00d38d59674367ca4591666c67e5dfad9e4fdd3861a59d6f26ffea87f65", + "local-path": "/usr/lib/x86_64-linux-gnu/libc.so.6" + }, + { + "ref": "4eb214d9b743a54462ac30b1c6af0676f275081290d5409f06664964504b2638", + "local-path": "/usr/lib/x86_64-linux-gnu/libxml2.so.2.9.14" + }, + { + "ref": "41c4cd10be11160dec736958a59eb0553357ad77008e9ba56250d4fd8698b8ec", + "local-path": "/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30" + }, + { + "ref": "4683f3623dc47a92e862dc3c8770caff81c9f4c7e116bb672fc439737b06d88a", + "local-path": "/usr/lib/x86_64-linux-gnu/libicuuc.so.70.1" + }, + { + "ref": "493cb401ab4aa3bba611ca464d12996afb3b327940d29476f535f999e167439b", + "local-path": "/usr/lib/x86_64-linux-gnu/liblzma.so.5.2.5" + } + ] +} diff --git a/utils/coredump/testdata/amd64/php74.7893.json b/tools/coredump/testdata/amd64/php74.7893.json similarity index 100% rename from utils/coredump/testdata/amd64/php74.7893.json rename to tools/coredump/testdata/amd64/php74.7893.json diff --git a/tools/coredump/testdata/amd64/php8.103871.json b/tools/coredump/testdata/amd64/php8.103871.json new file mode 100644 index 00000000..52b26e7c --- /dev/null +++ b/tools/coredump/testdata/amd64/php8.103871.json @@ -0,0 +1,26 @@ +{ + "coredump-ref": "565f0a1cc5634a769d422821d2ac8659dbad90892d475451b784506402ccc206", + "threads": [ + { + "lwp": 103871, + "frames": [ + "libc-2.31.so+0xeef33", + "php8.0+0x34f0f0", + "php8.0+0x34f16a", + "php8.0+0x26ce30", + "php8.0+0x26cf64", + "php8.0+0x2e3140", + "+5 in /home/j/run_forever.php:6", + "php8.0+0x32070b", + "php8.0+0x32969c", + "php8.0+0x2bfc34", + "php8.0+0x25b5bc", + "php8.0+0x34efb4", + "php8.0+0x116d30", + "libc-2.31.so+0x26d09", + "php8.0+0x116ea9" + ] + } + ], + "modules": null +} diff --git a/tools/coredump/testdata/amd64/python3.12-expat-debian-unstable.json b/tools/coredump/testdata/amd64/python3.12-expat-debian-unstable.json new file mode 100644 index 00000000..2193c3fc --- /dev/null +++ b/tools/coredump/testdata/amd64/python3.12-expat-debian-unstable.json @@ -0,0 +1,64 @@ +{ + "coredump-ref": "621a8bb8244237e37620d87955107cbcc309b400ade2ec386b41eddc28412942", + "threads": [ + { + "lwp": 15646, + "frames": [ + "test+1 in /root/expat.py:4", + "start_element+2 in /root/expat.py:10", + "+0 in :1", + "python3.12+0x53f6c1", + "python3.12+0x6d9571", + "python3.12+0x6d98c9", + "libexpat.so.1.8.10+0xd2ab", + "libexpat.so.1.8.10+0xe983", + "libexpat.so.1.8.10+0x10d9c", + "libexpat.so.1.8.10+0x8959", + "python3.12+0x6d8f2f", + "python3.12+0x5d0c6b", + "python3.12+0x559d51", + "main+7 in /root/expat.py:23", + "+27 in /root/expat.py:28", + "+0 in :1", + "python3.12+0x53f7c7", + "python3.12+0x6369fb", + "python3.12+0x6555ca", + "python3.12+0x6512e4", + "python3.12+0x664e15", + "python3.12+0x664ac7", + "python3.12+0x664897", + "python3.12+0x662e98", + "python3.12+0x6232ea", + "libc.so.6+0x276c9", + "libc.so.6+0x27784", + "python3.12+0x623170" + ] + } + ], + "modules": [ + { + "ref": "800f3150ab353d8c6692fb8fbcec5443819cae58ee0cafec0bb9ea1a09730fa0", + "local-path": "/usr/lib/x86_64-linux-gnu/libz.so.1.3" + }, + { + "ref": "afa44f24fdebf76a7f986e8b02885345ae1eeb9c09a212270b6ee37e9da547bd", + "local-path": "/usr/lib/x86_64-linux-gnu/libm.so.6" + }, + { + "ref": "28efa296e90e70ffce89122bbb32db9ea3389833edf9f9903ebfdde09b47180a", + "local-path": "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" + }, + { + "ref": "115cd5896c5f9719252094143ff82b0a83ee0eb6d8c295dcb724baf6ea904844", + "local-path": "/usr/lib/x86_64-linux-gnu/libc.so.6" + }, + { + "ref": "34fd674657f264a0d4b65a9d61d3ec26da1b7a64bf24981b37538e471d5c2b35", + "local-path": "/usr/bin/python3.12" + }, + { + "ref": "80b6ff17c18761318c5b3f2d65bbe0309dd2511f37d79233461c89ef59ec6f06", + "local-path": "/usr/lib/x86_64-linux-gnu/libexpat.so.1.8.10" + } + ] +} diff --git a/utils/coredump/testdata/amd64/python310.stringbench.20086.json b/tools/coredump/testdata/amd64/python310.stringbench.20086.json similarity index 100% rename from utils/coredump/testdata/amd64/python310.stringbench.20086.json rename to tools/coredump/testdata/amd64/python310.stringbench.20086.json diff --git a/utils/coredump/testdata/amd64/python310.stringbench.2576.json b/tools/coredump/testdata/amd64/python310.stringbench.2576.json similarity index 100% rename from utils/coredump/testdata/amd64/python310.stringbench.2576.json rename to tools/coredump/testdata/amd64/python310.stringbench.2576.json diff --git a/utils/coredump/testdata/amd64/python311-expat.json b/tools/coredump/testdata/amd64/python311-expat.json similarity index 98% rename from utils/coredump/testdata/amd64/python311-expat.json rename to tools/coredump/testdata/amd64/python311-expat.json index 7d99a604..dbd9cafd 100644 --- a/utils/coredump/testdata/amd64/python311-expat.json +++ b/tools/coredump/testdata/amd64/python311-expat.json @@ -6,7 +6,7 @@ "frames": [ "test+1 in /home/tteras/work/elastic/python/expat.py:4", "start_element+2 in /home/tteras/work/elastic/python/expat.py:10", - "libpython3.11.so.1.0+0x1c0c9b", + "libpython3.11.so.1.0+0x1c0c9c", "libpython3.11.so.1.0+0x1bd1c9", "pyexpat.cpython-311-x86_64-linux-musl.so+0x6f2e", "libexpat.so.1.8.10+0xb51c", diff --git a/tools/coredump/testdata/amd64/python311.50282.json b/tools/coredump/testdata/amd64/python311.50282.json new file mode 100644 index 00000000..35521371 --- /dev/null +++ b/tools/coredump/testdata/amd64/python311.50282.json @@ -0,0 +1,57 @@ +{ + "coredump-ref": "07b722f7560748f27cb9f7bc5822a3054f817e60c44a36b77f51a1e9ad3875bc", + "threads": [ + { + "lwp": 50282, + "frames": [ + "recur_fibo+1 in /home/flehner/fib.py:4", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "recur_fibo+4 in /home/flehner/fib.py:7", + "+9 in /home/flehner/fib.py:10", + "libpython3.11.so.1.0+0x1c8838", + "libpython3.11.so.1.0+0x1bd491", + "libpython3.11.so.1.0+0x24abfb", + "libpython3.11.so.1.0+0x26a1f3", + "libpython3.11.so.1.0+0x266755", + "libpython3.11.so.1.0+0x27d0f0", + "libpython3.11.so.1.0+0x27c8b1", + "libpython3.11.so.1.0+0x27c576", + "libpython3.11.so.1.0+0x275f40", + "libpython3.11.so.1.0+0x23886c", + "libc.so.6+0x3feaf", + "libc.so.6+0x3ff5f", + "python3.11+0x1094" + ] + } + ], + "modules": null +} diff --git a/utils/coredump/testdata/amd64/python37.26320.json b/tools/coredump/testdata/amd64/python37.26320.json similarity index 100% rename from utils/coredump/testdata/amd64/python37.26320.json rename to tools/coredump/testdata/amd64/python37.26320.json diff --git a/utils/coredump/testdata/amd64/ruby-2.7.8p225-loop.json b/tools/coredump/testdata/amd64/ruby-2.7.8p225-loop.json similarity index 100% rename from utils/coredump/testdata/amd64/ruby-2.7.8p225-loop.json rename to tools/coredump/testdata/amd64/ruby-2.7.8p225-loop.json diff --git a/utils/coredump/testdata/amd64/ruby-3.0.4p208-loop.json b/tools/coredump/testdata/amd64/ruby-3.0.4p208-loop.json similarity index 100% rename from utils/coredump/testdata/amd64/ruby-3.0.4p208-loop.json rename to tools/coredump/testdata/amd64/ruby-3.0.4p208-loop.json diff --git a/utils/coredump/testdata/amd64/ruby-3.0.6p216-loop.json b/tools/coredump/testdata/amd64/ruby-3.0.6p216-loop.json similarity index 100% rename from utils/coredump/testdata/amd64/ruby-3.0.6p216-loop.json rename to tools/coredump/testdata/amd64/ruby-3.0.6p216-loop.json diff --git a/utils/coredump/testdata/amd64/ruby-3.1.0p0-loop.json b/tools/coredump/testdata/amd64/ruby-3.1.0p0-loop.json similarity index 100% rename from utils/coredump/testdata/amd64/ruby-3.1.0p0-loop.json rename to tools/coredump/testdata/amd64/ruby-3.1.0p0-loop.json diff --git a/utils/coredump/testdata/amd64/ruby-3.1.4p223-loop.json b/tools/coredump/testdata/amd64/ruby-3.1.4p223-loop.json similarity index 100% rename from utils/coredump/testdata/amd64/ruby-3.1.4p223-loop.json rename to tools/coredump/testdata/amd64/ruby-3.1.4p223-loop.json diff --git a/utils/coredump/testdata/amd64/ruby-3.2.2-loop.json b/tools/coredump/testdata/amd64/ruby-3.2.2-loop.json similarity index 100% rename from utils/coredump/testdata/amd64/ruby-3.2.2-loop.json rename to tools/coredump/testdata/amd64/ruby-3.2.2-loop.json diff --git a/utils/coredump/testdata/amd64/ruby25.20836.json b/tools/coredump/testdata/amd64/ruby25.20836.json similarity index 100% rename from utils/coredump/testdata/amd64/ruby25.20836.json rename to tools/coredump/testdata/amd64/ruby25.20836.json diff --git a/utils/coredump/testdata/amd64/ruby25.benchmark-serialization.10811.json b/tools/coredump/testdata/amd64/ruby25.benchmark-serialization.10811.json similarity index 100% rename from utils/coredump/testdata/amd64/ruby25.benchmark-serialization.10811.json rename to tools/coredump/testdata/amd64/ruby25.benchmark-serialization.10811.json diff --git a/utils/coredump/testdata/amd64/ruby27.2186.json b/tools/coredump/testdata/amd64/ruby27.2186.json similarity index 100% rename from utils/coredump/testdata/amd64/ruby27.2186.json rename to tools/coredump/testdata/amd64/ruby27.2186.json diff --git a/utils/coredump/testdata/amd64/ruby30.253432.json b/tools/coredump/testdata/amd64/ruby30.253432.json similarity index 100% rename from utils/coredump/testdata/amd64/ruby30.253432.json rename to tools/coredump/testdata/amd64/ruby30.253432.json diff --git a/utils/coredump/testdata/amd64/stackalign.4040.json b/tools/coredump/testdata/amd64/stackalign.4040.json similarity index 100% rename from utils/coredump/testdata/amd64/stackalign.4040.json rename to tools/coredump/testdata/amd64/stackalign.4040.json diff --git a/utils/coredump/testdata/amd64/stackdeltas.11629.json b/tools/coredump/testdata/amd64/stackdeltas.11629.json similarity index 94% rename from utils/coredump/testdata/amd64/stackdeltas.11629.json rename to tools/coredump/testdata/amd64/stackdeltas.11629.json index 378121db..52844995 100644 --- a/utils/coredump/testdata/amd64/stackdeltas.11629.json +++ b/tools/coredump/testdata/amd64/stackdeltas.11629.json @@ -10,14 +10,14 @@ "stackdeltas+0x449ed3", "stackdeltas+0x469ba2", "ld-musl-x86_64.so.1+0x46c4c", - "stackdeltas+0x469840", + "stackdeltas+0x469841", "stackdeltas+0x44b05c", "stackdeltas+0x44b219", "stackdeltas+0x44a83a", "stackdeltas+0x44a044", "stackdeltas+0x469ba2", "ld-musl-x86_64.so.1+0x46c4c", - "stackdeltas+0x469e40", + "stackdeltas+0x469e41", "stackdeltas+0x4325a5", "stackdeltas+0x40d53e", "stackdeltas+0x43b7b8", @@ -37,7 +37,7 @@ "stackdeltas+0x44a044", "stackdeltas+0x469ba2", "ld-musl-x86_64.so.1+0x46c4c", - "stackdeltas+0x469e42", + "stackdeltas+0x469e43", "stackdeltas+0x432616", "stackdeltas+0x40d6d8", "stackdeltas+0x40d7b0", @@ -55,7 +55,7 @@ "stackdeltas+0x44a044", "stackdeltas+0x469ba2", "ld-musl-x86_64.so.1+0x46c4c", - "stackdeltas+0x469e40", + "stackdeltas+0x469e41", "stackdeltas+0x4325a5", "stackdeltas+0x40d53e", "stackdeltas+0x43b7b8", @@ -75,7 +75,7 @@ "stackdeltas+0x44a044", "stackdeltas+0x469ba2", "ld-musl-x86_64.so.1+0x46c4c", - "stackdeltas+0x4f7761", + "stackdeltas+0x4f7762", "stackdeltas+0x4eebf2", "stackdeltas+0x4effd0", "stackdeltas+0x4f6937", @@ -103,7 +103,7 @@ "stackdeltas+0x44a044", "stackdeltas+0x469ba2", "ld-musl-x86_64.so.1+0x46c4c", - "stackdeltas+0x469e40", + "stackdeltas+0x469e41", "stackdeltas+0x4325a5", "stackdeltas+0x40d53e", "stackdeltas+0x43b7b8", @@ -123,7 +123,7 @@ "stackdeltas+0x44a044", "stackdeltas+0x469ba2", "ld-musl-x86_64.so.1+0x46c4c", - "stackdeltas+0x469e40", + "stackdeltas+0x469e41", "stackdeltas+0x4325a5", "stackdeltas+0x40d53e", "stackdeltas+0x43cdf9", @@ -140,7 +140,7 @@ "stackdeltas+0x44a044", "stackdeltas+0x469ba2", "ld-musl-x86_64.so.1+0x46c4c", - "stackdeltas+0x469e40", + "stackdeltas+0x469e41", "stackdeltas+0x4325a5", "stackdeltas+0x40d53e", "stackdeltas+0x43b7b8", @@ -160,7 +160,7 @@ "stackdeltas+0x44a044", "stackdeltas+0x469ba2", "ld-musl-x86_64.so.1+0x46c4c", - "stackdeltas+0x469e40", + "stackdeltas+0x469e41", "stackdeltas+0x4325a5", "stackdeltas+0x40d53e", "stackdeltas+0x43b7b8", @@ -180,7 +180,7 @@ "stackdeltas+0x44a044", "stackdeltas+0x469ba2", "ld-musl-x86_64.so.1+0x46c4c", - "stackdeltas+0x469e40", + "stackdeltas+0x469e41", "stackdeltas+0x4325a5", "stackdeltas+0x40d53e", "stackdeltas+0x43b7b8", @@ -200,7 +200,7 @@ "stackdeltas+0x44a044", "stackdeltas+0x469ba2", "ld-musl-x86_64.so.1+0x46c4c", - "stackdeltas+0x469e40", + "stackdeltas+0x469e41", "stackdeltas+0x4325a5", "stackdeltas+0x40d53e", "stackdeltas+0x43b7b8", diff --git a/tools/coredump/testdata/arm64/.gitkeep b/tools/coredump/testdata/arm64/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/utils/coredump/testdata/arm64/brokenstack.json b/tools/coredump/testdata/arm64/brokenstack.json similarity index 100% rename from utils/coredump/testdata/arm64/brokenstack.json rename to tools/coredump/testdata/arm64/brokenstack.json diff --git a/utils/coredump/testdata/arm64/glibc-signal-arm.json b/tools/coredump/testdata/arm64/glibc-signal-arm.json similarity index 96% rename from utils/coredump/testdata/arm64/glibc-signal-arm.json rename to tools/coredump/testdata/arm64/glibc-signal-arm.json index acd2ab5e..2afdae18 100644 --- a/utils/coredump/testdata/arm64/glibc-signal-arm.json +++ b/tools/coredump/testdata/arm64/glibc-signal-arm.json @@ -9,7 +9,7 @@ "libc.so.6+0xc5b0f", "sig+0x4101f7", "linux-vdso.1.so+0x803", - "libc.so.6+0xc090f", + "libc.so.6+0xc0910", "libc.so.6+0xc5c5f", "libc.so.6+0xc5b0f", "sig+0x410237", diff --git a/utils/coredump/testdata/arm64/go.symbhack.readheader.json b/tools/coredump/testdata/arm64/go.symbhack.readheader.json similarity index 100% rename from utils/coredump/testdata/arm64/go.symbhack.readheader.json rename to tools/coredump/testdata/arm64/go.symbhack.readheader.json diff --git a/utils/coredump/testdata/arm64/hello.3345.hello3.body.stp-after-bl.json b/tools/coredump/testdata/arm64/hello.3345.hello3.body.stp-after-bl.json similarity index 100% rename from utils/coredump/testdata/arm64/hello.3345.hello3.body.stp-after-bl.json rename to tools/coredump/testdata/arm64/hello.3345.hello3.body.stp-after-bl.json diff --git a/utils/coredump/testdata/arm64/hello.3345.hello4.epi.add.json b/tools/coredump/testdata/arm64/hello.3345.hello4.epi.add.json similarity index 100% rename from utils/coredump/testdata/arm64/hello.3345.hello4.epi.add.json rename to tools/coredump/testdata/arm64/hello.3345.hello4.epi.add.json diff --git a/utils/coredump/testdata/arm64/hello.3345.hello4.epi.ret.json b/tools/coredump/testdata/arm64/hello.3345.hello4.epi.ret.json similarity index 100% rename from utils/coredump/testdata/arm64/hello.3345.hello4.epi.ret.json rename to tools/coredump/testdata/arm64/hello.3345.hello4.epi.ret.json diff --git a/utils/coredump/testdata/arm64/hello.3345.hello5.body.adrp.json b/tools/coredump/testdata/arm64/hello.3345.hello5.body.adrp.json similarity index 100% rename from utils/coredump/testdata/arm64/hello.3345.hello5.body.adrp.json rename to tools/coredump/testdata/arm64/hello.3345.hello5.body.adrp.json diff --git a/utils/coredump/testdata/arm64/hello.3345.hello5.epi.add.json b/tools/coredump/testdata/arm64/hello.3345.hello5.epi.add.json similarity index 100% rename from utils/coredump/testdata/arm64/hello.3345.hello5.epi.add.json rename to tools/coredump/testdata/arm64/hello.3345.hello5.epi.add.json diff --git a/utils/coredump/testdata/arm64/hello.3345.hello5.epi.ret.json b/tools/coredump/testdata/arm64/hello.3345.hello5.epi.ret.json similarity index 100% rename from utils/coredump/testdata/arm64/hello.3345.hello5.epi.ret.json rename to tools/coredump/testdata/arm64/hello.3345.hello5.epi.ret.json diff --git a/utils/coredump/testdata/arm64/hello.3345.hello5.pro.add.json b/tools/coredump/testdata/arm64/hello.3345.hello5.pro.add.json similarity index 100% rename from utils/coredump/testdata/arm64/hello.3345.hello5.pro.add.json rename to tools/coredump/testdata/arm64/hello.3345.hello5.pro.add.json diff --git a/utils/coredump/testdata/arm64/hello.3345.hello5.pro.stp.json b/tools/coredump/testdata/arm64/hello.3345.hello5.pro.stp.json similarity index 100% rename from utils/coredump/testdata/arm64/hello.3345.hello5.pro.stp.json rename to tools/coredump/testdata/arm64/hello.3345.hello5.pro.stp.json diff --git a/utils/coredump/testdata/arm64/hello.3345.hello5.pro.str.json b/tools/coredump/testdata/arm64/hello.3345.hello5.pro.str.json similarity index 100% rename from utils/coredump/testdata/arm64/hello.3345.hello5.pro.str.json rename to tools/coredump/testdata/arm64/hello.3345.hello5.pro.str.json diff --git a/utils/coredump/testdata/arm64/hello.3345.hello5.pro.stur.json b/tools/coredump/testdata/arm64/hello.3345.hello5.pro.stur.json similarity index 100% rename from utils/coredump/testdata/arm64/hello.3345.hello5.pro.stur.json rename to tools/coredump/testdata/arm64/hello.3345.hello5.pro.stur.json diff --git a/utils/coredump/testdata/arm64/hello.3345.hello5.pro.sub.json b/tools/coredump/testdata/arm64/hello.3345.hello5.pro.sub.json similarity index 100% rename from utils/coredump/testdata/arm64/hello.3345.hello5.pro.sub.json rename to tools/coredump/testdata/arm64/hello.3345.hello5.pro.sub.json diff --git a/utils/coredump/testdata/arm64/hello.3345.leaf.ret.json b/tools/coredump/testdata/arm64/hello.3345.leaf.ret.json similarity index 100% rename from utils/coredump/testdata/arm64/hello.3345.leaf.ret.json rename to tools/coredump/testdata/arm64/hello.3345.leaf.ret.json diff --git a/utils/coredump/testdata/arm64/hello.345.hello5.body.add.json b/tools/coredump/testdata/arm64/hello.345.hello5.body.add.json similarity index 100% rename from utils/coredump/testdata/arm64/hello.345.hello5.body.add.json rename to tools/coredump/testdata/arm64/hello.345.hello5.body.add.json diff --git a/utils/coredump/testdata/arm64/java-17.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256h.json b/tools/coredump/testdata/arm64/java-17.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256h.json similarity index 100% rename from utils/coredump/testdata/arm64/java-17.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256h.json rename to tools/coredump/testdata/arm64/java-17.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256h.json diff --git a/utils/coredump/testdata/arm64/java-21.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256h2.json b/tools/coredump/testdata/arm64/java-21.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256h2.json similarity index 100% rename from utils/coredump/testdata/arm64/java-21.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256h2.json rename to tools/coredump/testdata/arm64/java-21.ShaShenanigans.StubRoutines.sha256_implCompressMB.sha256h2.json diff --git a/utils/coredump/testdata/arm64/java.PrologueEpilogue.epi.add-sp-sp.377026.json b/tools/coredump/testdata/arm64/java.PrologueEpilogue.epi.add-sp-sp.377026.json similarity index 100% rename from utils/coredump/testdata/arm64/java.PrologueEpilogue.epi.add-sp-sp.377026.json rename to tools/coredump/testdata/arm64/java.PrologueEpilogue.epi.add-sp-sp.377026.json diff --git a/utils/coredump/testdata/arm64/java.PrologueEpilogue.epi.ldp-x29-x30.376761.json b/tools/coredump/testdata/arm64/java.PrologueEpilogue.epi.ldp-x29-x30.376761.json similarity index 100% rename from utils/coredump/testdata/arm64/java.PrologueEpilogue.epi.ldp-x29-x30.376761.json rename to tools/coredump/testdata/arm64/java.PrologueEpilogue.epi.ldp-x29-x30.376761.json diff --git a/utils/coredump/testdata/arm64/java.PrologueEpilogue.epi.ret.377192.json b/tools/coredump/testdata/arm64/java.PrologueEpilogue.epi.ret.377192.json similarity index 100% rename from utils/coredump/testdata/arm64/java.PrologueEpilogue.epi.ret.377192.json rename to tools/coredump/testdata/arm64/java.PrologueEpilogue.epi.ret.377192.json diff --git a/utils/coredump/testdata/arm64/java.PrologueEpilogue.stp-fp-lr.json b/tools/coredump/testdata/arm64/java.PrologueEpilogue.stp-fp-lr.json similarity index 100% rename from utils/coredump/testdata/arm64/java.PrologueEpilogue.stp-fp-lr.json rename to tools/coredump/testdata/arm64/java.PrologueEpilogue.stp-fp-lr.json diff --git a/utils/coredump/testdata/arm64/java.PrologueEpilogue.sub-sp-sp.json b/tools/coredump/testdata/arm64/java.PrologueEpilogue.sub-sp-sp.json similarity index 100% rename from utils/coredump/testdata/arm64/java.PrologueEpilogue.sub-sp-sp.json rename to tools/coredump/testdata/arm64/java.PrologueEpilogue.sub-sp-sp.json diff --git a/utils/coredump/testdata/arm64/java.VdsoPressure.osJavaTimeNanos.649996.json b/tools/coredump/testdata/arm64/java.VdsoPressure.osJavaTimeNanos.649996.json similarity index 100% rename from utils/coredump/testdata/arm64/java.VdsoPressure.osJavaTimeNanos.649996.json rename to tools/coredump/testdata/arm64/java.VdsoPressure.osJavaTimeNanos.649996.json diff --git a/utils/coredump/testdata/arm64/perl-5.38-debian.json b/tools/coredump/testdata/arm64/perl-5.38-debian.json similarity index 100% rename from utils/coredump/testdata/arm64/perl-5.38-debian.json rename to tools/coredump/testdata/arm64/perl-5.38-debian.json diff --git a/utils/coredump/testdata/arm64/php-8.2.10-prime.json b/tools/coredump/testdata/arm64/php-8.2.10-prime.json similarity index 100% rename from utils/coredump/testdata/arm64/php-8.2.10-prime.json rename to tools/coredump/testdata/arm64/php-8.2.10-prime.json diff --git a/utils/coredump/testdata/arm64/php-8.2.7-prime.json b/tools/coredump/testdata/arm64/php-8.2.7-prime.json similarity index 99% rename from utils/coredump/testdata/arm64/php-8.2.7-prime.json rename to tools/coredump/testdata/arm64/php-8.2.7-prime.json index 42c11cc0..a02fd0f7 100644 --- a/utils/coredump/testdata/arm64/php-8.2.7-prime.json +++ b/tools/coredump/testdata/arm64/php-8.2.7-prime.json @@ -6,7 +6,7 @@ "frames": [ "is_prime+13 in /pwd/testsources/php/prime.php:16", "+33 in /pwd/testsources/php/prime.php:34", - "php8.2+0x3637af", + "php8.2+0x3637b0", "php8.2+0x36b7cb", "php8.2+0x2e7c13", "php8.2+0x28156f", diff --git a/utils/coredump/testdata/arm64/php81.571415.json b/tools/coredump/testdata/arm64/php81.571415.json similarity index 95% rename from utils/coredump/testdata/arm64/php81.571415.json rename to tools/coredump/testdata/arm64/php81.571415.json index 68fa09b4..b56788d7 100644 --- a/utils/coredump/testdata/arm64/php81.571415.json +++ b/tools/coredump/testdata/arm64/php81.571415.json @@ -8,7 +8,7 @@ "shuffle+0 in :0", "run_forever+3 in /home/admin/php_forever.php:6", "+19 in /home/admin/php_forever.php:20", - "php8.1+0x33e72f", + "php8.1+0x33e730", "php8.1+0x349d77", "php8.1+0x2cb883", "php8.1+0x2666fb", diff --git a/utils/coredump/testdata/arm64/python-3.10.12-nix-fib.json b/tools/coredump/testdata/arm64/python-3.10.12-nix-fib.json similarity index 100% rename from utils/coredump/testdata/arm64/python-3.10.12-nix-fib.json rename to tools/coredump/testdata/arm64/python-3.10.12-nix-fib.json diff --git a/utils/coredump/testdata/arm64/python37.stringbench.10656.json b/tools/coredump/testdata/arm64/python37.stringbench.10656.json similarity index 100% rename from utils/coredump/testdata/arm64/python37.stringbench.10656.json rename to tools/coredump/testdata/arm64/python37.stringbench.10656.json diff --git a/utils/coredump/testdata/arm64/ruby-2.7.8p225-loop.json b/tools/coredump/testdata/arm64/ruby-2.7.8p225-loop.json similarity index 100% rename from utils/coredump/testdata/arm64/ruby-2.7.8p225-loop.json rename to tools/coredump/testdata/arm64/ruby-2.7.8p225-loop.json diff --git a/utils/coredump/testdata/arm64/ruby-3.0.4p208-loop.json b/tools/coredump/testdata/arm64/ruby-3.0.4p208-loop.json similarity index 100% rename from utils/coredump/testdata/arm64/ruby-3.0.4p208-loop.json rename to tools/coredump/testdata/arm64/ruby-3.0.4p208-loop.json diff --git a/utils/coredump/testdata/arm64/ruby-3.0.6p216-loop.json b/tools/coredump/testdata/arm64/ruby-3.0.6p216-loop.json similarity index 100% rename from utils/coredump/testdata/arm64/ruby-3.0.6p216-loop.json rename to tools/coredump/testdata/arm64/ruby-3.0.6p216-loop.json diff --git a/utils/coredump/testdata/arm64/ruby-3.1.0p0-loop.json b/tools/coredump/testdata/arm64/ruby-3.1.0p0-loop.json similarity index 100% rename from utils/coredump/testdata/arm64/ruby-3.1.0p0-loop.json rename to tools/coredump/testdata/arm64/ruby-3.1.0p0-loop.json diff --git a/utils/coredump/testdata/arm64/ruby-3.1.4p223-loop.json b/tools/coredump/testdata/arm64/ruby-3.1.4p223-loop.json similarity index 100% rename from utils/coredump/testdata/arm64/ruby-3.1.4p223-loop.json rename to tools/coredump/testdata/arm64/ruby-3.1.4p223-loop.json diff --git a/utils/coredump/testdata/arm64/ruby-3.2.2-loop.json b/tools/coredump/testdata/arm64/ruby-3.2.2-loop.json similarity index 100% rename from utils/coredump/testdata/arm64/ruby-3.2.2-loop.json rename to tools/coredump/testdata/arm64/ruby-3.2.2-loop.json diff --git a/utils/coredump/testdata/arm64/stackalign.19272.json b/tools/coredump/testdata/arm64/stackalign.19272.json similarity index 100% rename from utils/coredump/testdata/arm64/stackalign.19272.json rename to tools/coredump/testdata/arm64/stackalign.19272.json diff --git a/utils/coredump/testdata/arm64/stackalign.67475.json b/tools/coredump/testdata/arm64/stackalign.67475.json similarity index 100% rename from utils/coredump/testdata/arm64/stackalign.67475.json rename to tools/coredump/testdata/arm64/stackalign.67475.json diff --git a/utils/coredump/testdata/arm64/stackalign.7265.json b/tools/coredump/testdata/arm64/stackalign.7265.json similarity index 100% rename from utils/coredump/testdata/arm64/stackalign.7265.json rename to tools/coredump/testdata/arm64/stackalign.7265.json diff --git a/utils/coredump/testsources/c/brokenstack.c b/tools/coredump/testsources/c/brokenstack.c similarity index 100% rename from utils/coredump/testsources/c/brokenstack.c rename to tools/coredump/testsources/c/brokenstack.c diff --git a/utils/coredump/testsources/c/sig.c b/tools/coredump/testsources/c/sig.c similarity index 100% rename from utils/coredump/testsources/c/sig.c rename to tools/coredump/testsources/c/sig.c diff --git a/utils/coredump/testsources/c/stackalign.c b/tools/coredump/testsources/c/stackalign.c similarity index 100% rename from utils/coredump/testsources/c/stackalign.c rename to tools/coredump/testsources/c/stackalign.c diff --git a/tools/coredump/testsources/dotnet/helloworld/Program.cs b/tools/coredump/testsources/dotnet/helloworld/Program.cs new file mode 100644 index 00000000..66aece8a --- /dev/null +++ b/tools/coredump/testsources/dotnet/helloworld/Program.cs @@ -0,0 +1,15 @@ + +class foo { + public void bar() { + Console.WriteLine("Hello, World!"); + } +}; + +class main { + public static void Main() { + foo bar = new foo(); + while (true) { + bar.bar(); + } + } +} diff --git a/tools/coredump/testsources/dotnet/helloworld/helloworld.csproj b/tools/coredump/testsources/dotnet/helloworld/helloworld.csproj new file mode 100644 index 00000000..f2028cae --- /dev/null +++ b/tools/coredump/testsources/dotnet/helloworld/helloworld.csproj @@ -0,0 +1,10 @@ + + + + Exe + net6.0;net7.0;net8.0 + enable + enable + + + diff --git a/utils/coredump/testsources/go/hello.go b/tools/coredump/testsources/go/hello.go similarity index 100% rename from utils/coredump/testsources/go/hello.go rename to tools/coredump/testsources/go/hello.go diff --git a/utils/coredump/testsources/graalvm/.gitignore b/tools/coredump/testsources/graalvm/.gitignore similarity index 100% rename from utils/coredump/testsources/graalvm/.gitignore rename to tools/coredump/testsources/graalvm/.gitignore diff --git a/utils/coredump/testsources/graalvm/HelloGraal.java b/tools/coredump/testsources/graalvm/HelloGraal.java similarity index 100% rename from utils/coredump/testsources/graalvm/HelloGraal.java rename to tools/coredump/testsources/graalvm/HelloGraal.java diff --git a/utils/coredump/testsources/graalvm/Makefile b/tools/coredump/testsources/graalvm/Makefile similarity index 100% rename from utils/coredump/testsources/graalvm/Makefile rename to tools/coredump/testsources/graalvm/Makefile diff --git a/utils/coredump/testsources/graalvm/README.md b/tools/coredump/testsources/graalvm/README.md similarity index 100% rename from utils/coredump/testsources/graalvm/README.md rename to tools/coredump/testsources/graalvm/README.md diff --git a/utils/coredump/testsources/java/Deopt.java b/tools/coredump/testsources/java/Deopt.java similarity index 100% rename from utils/coredump/testsources/java/Deopt.java rename to tools/coredump/testsources/java/Deopt.java diff --git a/utils/coredump/testsources/java/DeoptFoo.java b/tools/coredump/testsources/java/DeoptFoo.java similarity index 100% rename from utils/coredump/testsources/java/DeoptFoo.java rename to tools/coredump/testsources/java/DeoptFoo.java diff --git a/utils/coredump/testsources/java/HelloWorld.java b/tools/coredump/testsources/java/HelloWorld.java similarity index 100% rename from utils/coredump/testsources/java/HelloWorld.java rename to tools/coredump/testsources/java/HelloWorld.java diff --git a/utils/coredump/testsources/java/Lambda1.java b/tools/coredump/testsources/java/Lambda1.java similarity index 100% rename from utils/coredump/testsources/java/Lambda1.java rename to tools/coredump/testsources/java/Lambda1.java diff --git a/utils/coredump/testsources/java/Prof1.java b/tools/coredump/testsources/java/Prof1.java similarity index 100% rename from utils/coredump/testsources/java/Prof1.java rename to tools/coredump/testsources/java/Prof1.java diff --git a/utils/coredump/testsources/java/Prof2.java b/tools/coredump/testsources/java/Prof2.java similarity index 100% rename from utils/coredump/testsources/java/Prof2.java rename to tools/coredump/testsources/java/Prof2.java diff --git a/utils/coredump/testsources/java/PrologueEpilogue.java b/tools/coredump/testsources/java/PrologueEpilogue.java similarity index 100% rename from utils/coredump/testsources/java/PrologueEpilogue.java rename to tools/coredump/testsources/java/PrologueEpilogue.java diff --git a/utils/coredump/testsources/java/ShaShenanigans.java b/tools/coredump/testsources/java/ShaShenanigans.java similarity index 100% rename from utils/coredump/testsources/java/ShaShenanigans.java rename to tools/coredump/testsources/java/ShaShenanigans.java diff --git a/utils/coredump/testsources/java/VdsoPressure.java b/tools/coredump/testsources/java/VdsoPressure.java similarity index 100% rename from utils/coredump/testsources/java/VdsoPressure.java rename to tools/coredump/testsources/java/VdsoPressure.java diff --git a/utils/coredump/testsources/java/javagdbinit b/tools/coredump/testsources/java/javagdbinit similarity index 100% rename from utils/coredump/testsources/java/javagdbinit rename to tools/coredump/testsources/java/javagdbinit diff --git a/utils/coredump/testsources/node/async.js b/tools/coredump/testsources/node/async.js similarity index 100% rename from utils/coredump/testsources/node/async.js rename to tools/coredump/testsources/node/async.js diff --git a/utils/coredump/testsources/node/hello.js b/tools/coredump/testsources/node/hello.js similarity index 100% rename from utils/coredump/testsources/node/hello.js rename to tools/coredump/testsources/node/hello.js diff --git a/utils/coredump/testsources/node/hello2.js b/tools/coredump/testsources/node/hello2.js similarity index 100% rename from utils/coredump/testsources/node/hello2.js rename to tools/coredump/testsources/node/hello2.js diff --git a/utils/coredump/testsources/node/inlining.js b/tools/coredump/testsources/node/inlining.js similarity index 100% rename from utils/coredump/testsources/node/inlining.js rename to tools/coredump/testsources/node/inlining.js diff --git a/utils/coredump/testsources/node/test.js b/tools/coredump/testsources/node/test.js similarity index 100% rename from utils/coredump/testsources/node/test.js rename to tools/coredump/testsources/node/test.js diff --git a/utils/coredump/testsources/perl/a.pl b/tools/coredump/testsources/perl/a.pl similarity index 100% rename from utils/coredump/testsources/perl/a.pl rename to tools/coredump/testsources/perl/a.pl diff --git a/utils/coredump/testsources/perl/hi.pl b/tools/coredump/testsources/perl/hi.pl similarity index 100% rename from utils/coredump/testsources/perl/hi.pl rename to tools/coredump/testsources/perl/hi.pl diff --git a/utils/coredump/testsources/php/gdb-dump-offsets.py b/tools/coredump/testsources/php/gdb-dump-offsets.py similarity index 97% rename from utils/coredump/testsources/php/gdb-dump-offsets.py rename to tools/coredump/testsources/php/gdb-dump-offsets.py index 79bc1541..3b614f53 100644 --- a/utils/coredump/testsources/php/gdb-dump-offsets.py +++ b/tools/coredump/testsources/php/gdb-dump-offsets.py @@ -1,5 +1,5 @@ """ -gdb python script for dumping the Ruby vmStruct offsets. +gdb python script for dumping the PHP engine offsets. """ def no_member_to_none(fn): diff --git a/utils/coredump/testsources/php/php_forever.php b/tools/coredump/testsources/php/php_forever.php similarity index 100% rename from utils/coredump/testsources/php/php_forever.php rename to tools/coredump/testsources/php/php_forever.php diff --git a/utils/coredump/testsources/php/prime.php b/tools/coredump/testsources/php/prime.php similarity index 100% rename from utils/coredump/testsources/php/prime.php rename to tools/coredump/testsources/php/prime.php diff --git a/utils/coredump/testsources/python/expat.py b/tools/coredump/testsources/python/expat.py similarity index 100% rename from utils/coredump/testsources/python/expat.py rename to tools/coredump/testsources/python/expat.py diff --git a/utils/coredump/testsources/python/fib.py b/tools/coredump/testsources/python/fib.py similarity index 100% rename from utils/coredump/testsources/python/fib.py rename to tools/coredump/testsources/python/fib.py diff --git a/utils/coredump/testsources/ruby/gdb-dump-offsets.py b/tools/coredump/testsources/ruby/gdb-dump-offsets.py similarity index 100% rename from utils/coredump/testsources/ruby/gdb-dump-offsets.py rename to tools/coredump/testsources/ruby/gdb-dump-offsets.py diff --git a/utils/coredump/testsources/ruby/loop.rb b/tools/coredump/testsources/ruby/loop.rb similarity index 100% rename from utils/coredump/testsources/ruby/loop.rb rename to tools/coredump/testsources/ruby/loop.rb diff --git a/utils/coredump/upload.go b/tools/coredump/upload.go similarity index 91% rename from utils/coredump/upload.go rename to tools/coredump/upload.go index a51abba8..d333c141 100644 --- a/utils/coredump/upload.go +++ b/tools/coredump/upload.go @@ -8,6 +8,7 @@ package main import ( "context" + "errors" "flag" "fmt" @@ -15,7 +16,7 @@ import ( log "github.com/sirupsen/logrus" - "github.com/elastic/otel-profiling-agent/utils/coredump/modulestore" + "github.com/elastic/otel-profiling-agent/tools/coredump/modulestore" ) type uploadCmd struct { @@ -42,14 +43,14 @@ func newUploadCmd(store *modulestore.Store) *ffcli.Command { func (cmd *uploadCmd) exec(context.Context, []string) (err error) { if (cmd.all && cmd.path != "") || (!cmd.all && cmd.path == "") { - return fmt.Errorf("please pass either `-path` or `-all` (but not both)") + return errors.New("please pass either `-path` or `-all` (but not both)") } var paths []string if cmd.all { paths, err = findTestCases(false) if err != nil { - return fmt.Errorf("failed to scan for test cases") + return errors.New("failed to scan for test cases") } } else { paths = []string{cmd.path} diff --git a/utils/errors-codegen/bpf.h.template b/tools/errors-codegen/bpf.h.template similarity index 100% rename from utils/errors-codegen/bpf.h.template rename to tools/errors-codegen/bpf.h.template diff --git a/utils/errors-codegen/errors.json b/tools/errors-codegen/errors.json similarity index 92% rename from utils/errors-codegen/errors.json rename to tools/errors-codegen/errors.json index c43e307f..b312b6e2 100644 --- a/utils/errors-codegen/errors.json +++ b/tools/errors-codegen/errors.json @@ -244,5 +244,25 @@ "id": 5002, "name": "v8_no_proc_info", "description": "V8: No entry for this process exists in the V8 process info array" + }, + { + "id": 6000, + "name": "dotnet_no_proc_info", + "description": "Dotnet: No entry for this process exists in the dotnet process info array" + }, + { + "id": 6001, + "name": "dotnet_bad_fp", + "description": "Dotnet: Encountered a bad frame pointer during dotnet unwinding" + }, + { + "id": 6002, + "name": "dotnet_code_header", + "description": "Dotnet: Failed to find or read CodeHeader" + }, + { + "id": 6003, + "name": "dotnet_code_too_large", + "description": "Dotnet: Code object was too large to unwind in eBPF" } ] diff --git a/utils/errors-codegen/main.go b/tools/errors-codegen/main.go similarity index 100% rename from utils/errors-codegen/main.go rename to tools/errors-codegen/main.go diff --git a/tools/fake-apm-agent/.gitignore b/tools/fake-apm-agent/.gitignore new file mode 100644 index 00000000..f739a084 --- /dev/null +++ b/tools/fake-apm-agent/.gitignore @@ -0,0 +1,3 @@ +/fake-apm-agent +/elastic-jvmti-linux-*.so + diff --git a/tools/fake-apm-agent/Makefile b/tools/fake-apm-agent/Makefile new file mode 100644 index 00000000..dc592903 --- /dev/null +++ b/tools/fake-apm-agent/Makefile @@ -0,0 +1,22 @@ +.PHONY: clean all + +ARCH=$(shell uname -m) +LIB_NAME=elastic-jvmti-linux-$(ARCH).so + +ifeq ($(ARCH),aarch64) +TLS_DIALECT?=desc +else +TLS_DIALECT?=gnu2 +endif + +all: fake-apm-agent $(LIB_NAME) + +$(LIB_NAME): fake-apm-agent-lib.c + cc $< -g -shared -fPIC -mtls-dialect=$(TLS_DIALECT) -ftls-model=global-dynamic -o $@ + +fake-apm-agent: fake-apm-agent.c + cc $< -g -DLIB_NAME='"$(LIB_NAME)"' -o $@ + +clean: + rm elastic-jvmti-linux-$(ARCH).so + rm fake-apm-agent diff --git a/tools/fake-apm-agent/README.md b/tools/fake-apm-agent/README.md new file mode 100644 index 00000000..db4e02b1 --- /dev/null +++ b/tools/fake-apm-agent/README.md @@ -0,0 +1,7 @@ +fake-apm-agent +============== + +Small tool implementing the [APM integration protocol specification][spec]. Allows debugging the +APM integration code in the host agent without actually having to spin up a real APM agent. + +[spec]: https://github.com/elastic/apm/blob/main/specs/agents/universal-profiling-integration.md diff --git a/tools/fake-apm-agent/fake-apm-agent-lib.c b/tools/fake-apm-agent/fake-apm-agent-lib.c new file mode 100644 index 00000000..32f66bc9 --- /dev/null +++ b/tools/fake-apm-agent/fake-apm-agent-lib.c @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define PACKED __attribute__((packed)) + +static const char* SERVICE_NAME = "fake-apm-service"; +static const char* SERVICE_ENV = "fake-apm-env"; +static const char* SOCK_PATH_FMT = "/tmp/apm-corr-test-socket-%d"; + +thread_local void* elastic_apm_profiling_correlation_tls_v1 = NULL; +void* elastic_apm_profiling_correlation_process_storage_v1 = NULL; + +typedef union ID128 { + uint8_t raw[16]; + struct { + uint64_t lo; + uint64_t hi; + } as_int; +} ID128; + +typedef union ID64 { + uint8_t raw[8]; + uint64_t as_int; +} ID64; + +typedef ID64 ApmSpanID; + +typedef ID128 ApmTraceID; + +typedef ID128 UPTraceID; + +typedef struct PACKED ApmCorrelationBuf { + uint16_t layout_minor_version; + uint8_t valid; + uint8_t trace_present; + uint8_t trace_flags; + ApmTraceID trace_id; + ApmSpanID span_id; + ApmSpanID tx_id; +} ApmCorrelationBuf; + +typedef struct PACKED ApmSocketMessage { + uint16_t message_type; + uint16_t minor_version; + ApmTraceID apm_trace_id; + ApmSpanID apm_tx_id; + UPTraceID up_trace_id; + uint16_t count; +} ApmSocketMessage; + +static void* fake_java_apm_agent_recv_thread(void* fd_ptr) { + int fd = *(int*)fd_ptr; + + for (;;) { + ApmSocketMessage msg = {0}; + int n = recv(fd, &msg, sizeof msg, 0); + + if (n != sizeof msg) { + printf("Received truncated message (%d bytes)\n", n); + continue; + } + + printf( + "Received trace mapping from profiler:\n" + " APM trace ID: %016" PRIX64 ":%016" PRIX64 "\n" + " APM TX: %016" PRIX64 "\n" + " UP trace ID: %016" PRIX64 ":%016" PRIX64 "\n" + " Sample count: %" PRIu16 "\n\n", + msg.apm_trace_id.as_int.hi, + msg.apm_trace_id.as_int.lo, + msg.apm_tx_id.as_int, + msg.up_trace_id.as_int.hi, + msg.up_trace_id.as_int.lo, + msg.count + ); + } + + return NULL; +} + +static ApmCorrelationBuf* alloc_correlation_buf(ApmSpanID tx_id) { + ApmCorrelationBuf* corr_buf = malloc(sizeof(ApmCorrelationBuf)); + + *corr_buf = (ApmCorrelationBuf) { + .layout_minor_version = 1, + .valid = 1, + .trace_present = 1, + .trace_flags = 0, + .trace_id.raw = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + .span_id.raw = {0, 1, 2, 3, 4, 5, 6, 7}, + .tx_id = tx_id + }; + + return corr_buf; +} + +static void put_str(uint8_t** write_ptr, const char* str) { + uint32_t len = strlen(str); + memcpy(*write_ptr, &len, sizeof len); + *write_ptr += sizeof len; + memcpy(*write_ptr, str, (size_t)len); + *write_ptr += len; +} + +int run_fake_apm_agent() { + // + // Create and bind socket + // + + int fd = socket(PF_UNIX, SOCK_DGRAM, 0); + if (fd == -1) { + return 1; + } + + struct sockaddr_un addr = { .sun_family = AF_UNIX }; + srand(time(NULL)); + int n = snprintf(addr.sun_path, sizeof addr.sun_path, SOCK_PATH_FMT, rand()); + if (n > sizeof addr.sun_path) { + return 2; + } + + if (unlink(addr.sun_path) == -1 && errno != ENOENT) { + return 3; + } + + if (bind(fd, (struct sockaddr*)&addr, sizeof addr) < 0) { + return 4; + } + + // + // Allocate and populate two correlation buffers. + // + + ApmCorrelationBuf* corr_buf_1 = alloc_correlation_buf( + (ApmSpanID) { .as_int = 0x0011223344556677ULL } + ); + ApmCorrelationBuf* corr_buf_2 = alloc_correlation_buf( + (ApmSpanID) { .as_int = 0x8899AABBCCDDEEFFULL } + ); + + // + // Allocate and populate process storage + // + + uint8_t* process_storage = malloc(256); + uint8_t* write_ptr = process_storage; + + *(uint16_t*)write_ptr = 1; // Layout minor version + write_ptr += sizeof(uint16_t); + + put_str(&write_ptr, SERVICE_NAME); + put_str(&write_ptr, SERVICE_ENV); + put_str(&write_ptr, addr.sun_path); + + elastic_apm_profiling_correlation_process_storage_v1 = process_storage; + + // + // Spawn thread reading messages from the socket + // + + pthread_t thread; + pthread_attr_t thread_attr; + pthread_attr_init(&thread_attr); + if (pthread_create(&thread, &thread_attr, fake_java_apm_agent_recv_thread, &fd)) { + return 5; + } + + // + // Generate samples by spinning in an infinite loop. + // + + printf("Socket bound to `%s`. Spinning & waiting for messages from UP.\n", addr.sun_path); + + for (uint16_t x; ; ++x) { + switch (x) { + case 0: elastic_apm_profiling_correlation_tls_v1 = corr_buf_1; break; + case 1 << 15: elastic_apm_profiling_correlation_tls_v1 = corr_buf_2; break; + } + } + + return 0; +} diff --git a/tools/fake-apm-agent/fake-apm-agent.c b/tools/fake-apm-agent/fake-apm-agent.c new file mode 100644 index 00000000..135cff49 --- /dev/null +++ b/tools/fake-apm-agent/fake-apm-agent.c @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +#include +#include + +typedef int(*run_fake_apm_agent_t)(); + +int main() { + // NOTE: `./` is necessary to make dlopen consider searching in local paths. + void* handle = dlopen("./" LIB_NAME, RTLD_LAZY); + if (!handle) { + return 101; + } + + run_fake_apm_agent_t fn = dlsym(handle, "run_fake_apm_agent"); + if (dlerror() != NULL) { + return 102; + } + + return fn(); +} diff --git a/tools/file_id.sh b/tools/file_id.sh new file mode 100755 index 00000000..8d4d8c20 --- /dev/null +++ b/tools/file_id.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -eu + +# This script computes the FileID as we do in Go code. + +# We must resolve symlinks before we can lookup the size of the file via `stat` +file=$(readlink -f "$1") +filesize=$(stat --printf="%s" "$file") + +hash=$(cat \ + <(head -c 4096 "$file") \ + <(tail -c 4096 "$file") \ + <(printf $(printf "%.16x" $filesize | sed 's/\(..\)/\\x\1/g')) \ + | sha256sum) + +echo "$hash" | head -c 32 diff --git a/tools/stackdeltas/.gitignore b/tools/stackdeltas/.gitignore new file mode 100644 index 00000000..f42de0d2 --- /dev/null +++ b/tools/stackdeltas/.gitignore @@ -0,0 +1 @@ +stackdeltas diff --git a/tools/stackdeltas/stackdeltas.go b/tools/stackdeltas/stackdeltas.go new file mode 100644 index 00000000..fb54c9d8 --- /dev/null +++ b/tools/stackdeltas/stackdeltas.go @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +// A command-line tool to parse stack deltas from given ELF files. This tool +// can generate statistics on number of stack deltas and different stack delta +// values are seen, or a full listing of stack deltas from a file given with +// -target option. +package main + +import ( + "flag" + "fmt" + "path/filepath" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/nativeunwind/elfunwindinfo" + sdtypes "github.com/elastic/otel-profiling-agent/nativeunwind/stackdeltatypes" + "github.com/elastic/otel-profiling-agent/support" +) + +var ( + target = flag.String("target", "", "The target executable to operate on.") + + mergeDistance = flag.Uint("mergeDistance", 2, + "Maximum distance of stack deltas that can be merged") +) + +type stats struct { + seenDeltas libpf.Set[sdtypes.UnwindInfo] + + numDeltas, numMerged uint +} + +func getOpcode(opcode uint8, param int32) string { + str := "" + switch opcode &^ sdtypes.UnwindOpcodeFlagDeref { + case sdtypes.UnwindOpcodeCommand: + switch param { + case sdtypes.UnwindCommandInvalid: + return "invalid" + case sdtypes.UnwindCommandStop: + return "stop" + case sdtypes.UnwindCommandPLT: + return "plt" + case sdtypes.UnwindCommandSignal: + return "signal" + default: + return "?" + } + case sdtypes.UnwindOpcodeBaseCFA: + str = "cfa" + case sdtypes.UnwindOpcodeBaseFP: + str = "fp" + case sdtypes.UnwindOpcodeBaseSP: + str = "sp" + default: + return "?" + } + if opcode&sdtypes.UnwindOpcodeFlagDeref != 0 { + preDeref, postDeref := sdtypes.UnpackDerefParam(param) + if postDeref != 0 { + str = fmt.Sprintf("*(%s%+x)%+x", str, preDeref, postDeref) + } else { + str = fmt.Sprintf("*(%s%+x)", str, preDeref) + } + } else { + str = fmt.Sprintf("%s%+x", str, param) + } + return str +} + +func canMerge(delta, nextDelta sdtypes.StackDelta) bool { + if nextDelta.Address-delta.Address > uint64(*mergeDistance) { + return false + } + if nextDelta.Info.Opcode != delta.Info.Opcode || + nextDelta.Info.FPOpcode != delta.Info.FPOpcode || + nextDelta.Info.FPParam != delta.Info.FPParam { + return false + } + deltaDiff := nextDelta.Info.Param - delta.Info.Param + return deltaDiff >= -8 && deltaDiff <= 8 +} + +func analyzeFile(filename string, s *stats, dump bool) error { + var data sdtypes.IntervalData + + absPath, err := filepath.Abs(filename) + if err != nil { + return fmt.Errorf("failed to get absolute path for %v: %v", + filename, err) + } + + if err := elfunwindinfo.Extract(absPath, &data); err != nil { + return fmt.Errorf("failed to extract stack deltas: %v", err) + } + + if dump { + fmt.Printf("%-16v %-16v%-16v %v\n", "# addr", "cfa", "fp", "flags") + } + + var merged bool + var numMerged uint + for index, delta := range data.Deltas { + if dump { + cfa := getOpcode(delta.Info.Opcode, delta.Info.Param) + fp := getOpcode(delta.Info.FPOpcode, delta.Info.FPParam) + comment := "" + if delta.Hints&sdtypes.UnwindHintKeep != 0 { + comment += " keep" + } + if delta.Hints&sdtypes.UnwindHintGap != 0 { + comment += " gap" + } + if merged { + comment += " merged" + } + fmt.Printf("%016x %-16s%-16s%s\n", delta.Address, cfa, fp, comment) + } + if merged { + merged = false + continue + } + info := delta.Info + if index+1 < len(data.Deltas) && canMerge(delta, data.Deltas[index+1]) { + nextDelta := data.Deltas[index+1] + merged = true + numMerged++ + info.MergeOpcode = uint8(nextDelta.Address - delta.Address) + if nextDelta.Info.Param-delta.Info.Param < 0 { + info.MergeOpcode |= support.MergeOpcodeNegative + } + } + s.seenDeltas[info] = libpf.Void{} + } + numDeltas := uint(len(data.Deltas)) + s.numDeltas += numDeltas + s.numMerged += numMerged + + fmt.Printf("# %v: %v deltas, %d (%.1f%%) merged\n", + filename, + numDeltas, + numDeltas-numMerged, + 100*float32(numDeltas-numMerged)/float32(numDeltas)) + + return nil +} + +func main() { + s := stats{ + seenDeltas: make(libpf.Set[sdtypes.UnwindInfo]), + } + + flag.Parse() + + if *target != "" { + if err := analyzeFile(*target, &s, true); err != nil { + fmt.Printf("# %s: %s\n", *target, err) + } + } + for _, f := range flag.Args() { + if err := analyzeFile(f, &s, false); err != nil { + fmt.Printf("# %s: %s\n", f, err) + } + } + fmt.Printf("# %v deltas, %v (%.1f%%) merged, %v unique\n", + s.numDeltas, + s.numDeltas-s.numMerged, + 100*float32(s.numDeltas-s.numMerged)/float32(s.numDeltas), + len(s.seenDeltas)) +} diff --git a/utils/zstpak/.gitignore b/tools/zstpak/.gitignore similarity index 100% rename from utils/zstpak/.gitignore rename to tools/zstpak/.gitignore diff --git a/utils/zstpak/lib/zstpak.go b/tools/zstpak/lib/zstpak.go similarity index 90% rename from utils/zstpak/lib/zstpak.go rename to tools/zstpak/lib/zstpak.go index 55d591e0..d73a4fd3 100644 --- a/utils/zstpak/lib/zstpak.go +++ b/tools/zstpak/lib/zstpak.go @@ -27,11 +27,12 @@ package zstpak import ( "bytes" "encoding/binary" + "errors" "fmt" "io" "os" - "github.com/DataDog/zstd" + "github.com/klauspost/compress/zstd" ) // footerSize is the size of the static portion of the footer (without the index data). @@ -51,13 +52,13 @@ func readFooter(input io.ReaderAt, fileSize uint64) (*footer, error) { var buf [footerSize]byte if fileSize < footerSize { - return nil, fmt.Errorf("file is too small to be a valid zstpak file") + return nil, errors.New("file is too small to be a valid zstpak file") } if _, err := input.ReadAt(buf[:], int64(fileSize-footerSize)); err != nil { return nil, fmt.Errorf("failed to read footer: %w", err) } if !bytes.Equal(buf[24:], []byte(magic)) { - return nil, fmt.Errorf("file doesn't appear to be in zstpak format (bad magic)") + return nil, errors.New("file doesn't appear to be in zstpak format (bad magic)") } chunkSize := binary.LittleEndian.Uint64(buf[16:]) @@ -66,7 +67,7 @@ func readFooter(input io.ReaderAt, fileSize uint64) (*footer, error) { // Read raw index from file. if fileSize < footerSize+numberOfChunks*8 { - return nil, fmt.Errorf("file too small to hold index table") + return nil, errors.New("file too small to hold index table") } rawIndex := make([]byte, numberOfChunks*8) indexOffset := fileSize - footerSize - numberOfChunks*8 @@ -79,7 +80,7 @@ func readFooter(input io.ReaderAt, fileSize uint64) (*footer, error) { for i := uint64(0); i < numberOfChunks; i++ { entry := binary.LittleEndian.Uint64(rawIndex[i*8:]) if i > 0 && entry < index[i-1] { - return nil, fmt.Errorf("index entries aren't monotonically increasing") + return nil, errors.New("index entries aren't monotonically increasing") } index = append(index, entry) } @@ -125,12 +126,18 @@ func CompressInto(in io.Reader, out io.Writer, chunkSize uint64) error { index := []uint64{0} writeOffset := uint64(0) uncompressedSize := uint64(0) + + enc, err := zstd.NewWriter(nil) + if err != nil { + return fmt.Errorf("failed to create encoder: %w", err) + } for { n, err := io.ReadFull(in, readBuf) if err != nil { if err == io.EOF { break - } else if err == io.ErrUnexpectedEOF { + } + if err == io.ErrUnexpectedEOF { // Last chunk: truncate our buffer and continue. Next read will // return EOF and thus break the loop. readBuf = readBuf[:n] @@ -139,10 +146,7 @@ func CompressInto(in io.Reader, out io.Writer, chunkSize uint64) error { } } - compressed, err := zstd.Compress(compressBuf, readBuf) - if err != nil { - return fmt.Errorf("failed to compress buffer: %w", err) - } + compressed := enc.EncodeAll(readBuf, compressBuf[:0]) uncompressedSize += uint64(n) writeOffset += uint64(len(compressed)) @@ -228,7 +232,7 @@ func (reader *Reader) ReadAt(p []byte, off int64) (n int, err error) { // Copy data to output buffer. if skipOffset > len(decompressed) { - return 0, fmt.Errorf("corrupted chunk data") + return 0, errors.New("corrupted chunk data") } copyLen := min(remaining, len(decompressed)-skipOffset) copy(p[writeOffset:][:copyLen], decompressed[skipOffset:][:copyLen]) @@ -251,7 +255,12 @@ func (reader *Reader) getDecompressedChunk(start, length uint64) ([]byte, error) return nil, fmt.Errorf("failed to read chunk data: %w", err) } - decompressed, err := zstd.Decompress(nil, compressedChunk) + dec, err := zstd.NewReader(nil) + if err != nil { + return nil, fmt.Errorf("failed to create decoder: %w", err) + } + + decompressed, err := dec.DecodeAll(compressedChunk, nil) if err != nil { return nil, fmt.Errorf("failed to decompress chunk: %w", err) } diff --git a/tools/zstpak/lib/zstpak_test.go b/tools/zstpak/lib/zstpak_test.go new file mode 100644 index 00000000..4e89fb3a --- /dev/null +++ b/tools/zstpak/lib/zstpak_test.go @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package zstpak_test + +import ( + "bytes" + "os" + "testing" + + "github.com/elastic/otel-profiling-agent/testsupport" + zstpak "github.com/elastic/otel-profiling-agent/tools/zstpak/lib" + "github.com/stretchr/testify/require" +) + +func generateInputFile(seqLen uint8, outputSize uint64) []byte { + out := make([]byte, 0, outputSize) + for i := uint64(0); i < outputSize; i++ { + out = append(out, byte(i%uint64(seqLen))) + } + + return out +} + +func testRandomAccesses(t *testing.T, seqLen uint8, fileSize uint64, + chunkSize uint64) { + file := generateInputFile(seqLen, fileSize) + reader := bytes.NewReader(file) + + temp, err := os.CreateTemp("", "") + require.NoError(t, err) + defer os.Remove(temp.Name()) + + err = zstpak.CompressInto(reader, temp, chunkSize) + require.NoError(t, err) + err = temp.Close() + require.NoError(t, err) + + zstReader, err := zstpak.Open(temp.Name()) + require.NoError(t, err) + + testsupport.ValidateReadAtWrapperTransparency(t, 1000, file, zstReader) +} + +func TestRandomAccess(t *testing.T) { + // Repeat with 3 sets of mostly arbitrarily chosen parameters. + testRandomAccesses(t, 128, 1024, 64) + testRandomAccesses(t, 43, 1424, 444) + testRandomAccesses(t, 13, 1049454, 8543) +} diff --git a/utils/zstpak/main.go b/tools/zstpak/main.go similarity index 88% rename from utils/zstpak/main.go rename to tools/zstpak/main.go index 2d0553d9..1f1e842a 100644 --- a/utils/zstpak/main.go +++ b/tools/zstpak/main.go @@ -9,11 +9,12 @@ package main import ( + "errors" "flag" "fmt" "os" - zstpak "github.com/elastic/otel-profiling-agent/utils/zstpak/lib" + zstpak "github.com/elastic/otel-profiling-agent/tools/zstpak/lib" ) func tryMain() error { @@ -29,13 +30,13 @@ func tryMain() error { flag.Parse() if compress == decompress { - return fmt.Errorf("must specify either `-c` or `-d`") + return errors.New("must specify either `-c` or `-d`") } if in == "" { - return fmt.Errorf("missing required argument `i`") + return errors.New("missing required argument `i`") } if out == "" { - return fmt.Errorf("missing required argument `o`") + return errors.New("missing required argument `o`") } outputFile, err := os.Create(out) diff --git a/tpbase/assembly_decode.go b/tpbase/assembly_decode.go index 403514df..9f3679c8 100644 --- a/tpbase/assembly_decode.go +++ b/tpbase/assembly_decode.go @@ -10,7 +10,7 @@ import ( "errors" "fmt" - ah "github.com/elastic/otel-profiling-agent/libpf/armhelpers" + ah "github.com/elastic/otel-profiling-agent/armhelpers" aa "golang.org/x/arch/arm64/arm64asm" ) diff --git a/tpbase/assembly_decode_amd64.go b/tpbase/assembly_decode_amd64.go index b998dee8..daf0be39 100644 --- a/tpbase/assembly_decode_amd64.go +++ b/tpbase/assembly_decode_amd64.go @@ -11,7 +11,7 @@ package tpbase import ( "bytes" "encoding/binary" - "fmt" + "errors" "unsafe" ) @@ -36,7 +36,7 @@ func GetAnalyzers() []Analyzer { // kernel in order to compute the offset of `fsbase` into `task_struct`. func AnalyzeAoutDumpDebugregs(code []byte) (uint32, error) { if len(code) == 0 { - return 0, fmt.Errorf("empty code blob passed to getFSBaseOffset") + return 0, errors.New("empty code blob passed to getFSBaseOffset") } // Because different compilers generate code that looks different enough, we disassemble the @@ -46,7 +46,7 @@ func AnalyzeAoutDumpDebugregs(code []byte) (uint32, error) { (*C.uint8_t)(unsafe.Pointer(&code[0])), C.size_t(len(code)))) if offset == 0 { - return 0, fmt.Errorf("unable to determine fsbase offset") + return 0, errors.New("unable to determine fsbase offset") } return offset, nil @@ -70,7 +70,7 @@ func AnalyzeX86fsbaseWriteTask(code []byte) (uint32, error) { // See https://elixir.bootlin.com/linux/latest/source/arch/x86/kernel/process_64.c#L466 idx := bytes.Index(code, []byte{0x48, 0x89, 0xb7}) if idx == -1 || idx+7 > len(code) { - return 0, fmt.Errorf("unexpected x86_fsbase_write_task (mov not found)") + return 0, errors.New("unexpected x86_fsbase_write_task (mov not found)") } offset := binary.LittleEndian.Uint32(code[idx+3:]) return offset, nil diff --git a/tpbase/assembly_decode_arm64.go b/tpbase/assembly_decode_arm64.go index 3d703d0e..67383554 100644 --- a/tpbase/assembly_decode_arm64.go +++ b/tpbase/assembly_decode_arm64.go @@ -8,10 +8,6 @@ package tpbase -func x86GetAnalyzers() []Analyzer { - return nil -} - func GetAnalyzers() []Analyzer { return arm64GetAnalyzers() } diff --git a/tpbase/libc.go b/tpbase/libc.go index 10ea8f45..877ab720 100644 --- a/tpbase/libc.go +++ b/tpbase/libc.go @@ -11,9 +11,9 @@ import ( "fmt" "regexp" - ah "github.com/elastic/otel-profiling-agent/libpf/armhelpers" + ah "github.com/elastic/otel-profiling-agent/armhelpers" "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/elastic/otel-profiling-agent/libpf/stringutil" + "github.com/elastic/otel-profiling-agent/stringutil" aa "golang.org/x/arch/arm64/arm64asm" ) diff --git a/tpbase/libc_arm64.go b/tpbase/libc_arm64.go index 5ef17753..145f7b65 100644 --- a/tpbase/libc_arm64.go +++ b/tpbase/libc_arm64.go @@ -8,7 +8,7 @@ package tpbase -func ExtractTSDInfoX64_64(code []byte) (TSDInfo, error) { +func ExtractTSDInfoX64_64(_ []byte) (TSDInfo, error) { return TSDInfo{}, errArchNotImplemented } diff --git a/tracehandler/tracehandler.go b/tracehandler/tracehandler.go index fbf63a91..5ac9980d 100644 --- a/tracehandler/tracehandler.go +++ b/tracehandler/tracehandler.go @@ -14,14 +14,15 @@ import ( "time" lru "github.com/elastic/go-freelru" + log "github.com/sirupsen/logrus" + "github.com/elastic/otel-profiling-agent/config" "github.com/elastic/otel-profiling-agent/containermetadata" "github.com/elastic/otel-profiling-agent/host" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/memorydebug" + "github.com/elastic/otel-profiling-agent/memorydebug" "github.com/elastic/otel-profiling-agent/reporter" - "github.com/elastic/otel-profiling-agent/tracer" - log "github.com/sirupsen/logrus" + "github.com/elastic/otel-profiling-agent/util" ) // metadataWarnInhibDuration defines the minimum duration between warnings printed @@ -34,12 +35,18 @@ var _ Times = (*config.Times)(nil) // Times is a subset of config.IntervalsAndTimers. type Times interface { MonitorInterval() time.Duration + BootTimeUnixNano() int64 } // TraceProcessor is an interface used by traceHandler to convert traces // from a form received from eBPF to the form we wish to dispatch to the // collection agent. type TraceProcessor interface { + // MaybeNotifyAPMAgent notifies a potentially existing connected APM agent + // that a stack trace was collected in their process. If an APM agent is + // listening, the service name is returned. + MaybeNotifyAPMAgent(rawTrace *host.Trace, umTraceHash libpf.TraceHash, count uint16) string + // ConvertTrace converts a trace from eBPF into the form we want to send to // the collection agent. Depending on the frame type it will attempt to symbolize // the frame and send the associated metadata to the collection agent. @@ -49,12 +56,9 @@ type TraceProcessor interface { // It gets the timestamp of when the Traces (if any) were captured. The timestamp // is in essence an indicator that all Traces until that time have been now processed, // and any events up to this time can be processed. - SymbolizationComplete(traceCaptureKTime libpf.KTime) + SymbolizationComplete(traceCaptureKTime util.KTime) } -// Compile time check to make sure Tracer satisfies the interfaces. -var _ TraceProcessor = (*tracer.Tracer)(nil) - // traceHandler provides functions for handling new traces and trace count updates // from the eBPF components. type traceHandler struct { @@ -79,18 +83,18 @@ type traceHandler struct { reporter reporter.TraceReporter // containerMetadataHandler retrieves the metadata associated with the pod or container. - containerMetadataHandler *containermetadata.Handler + containerMetadataHandler containermetadata.Handler // metadataWarnInhib tracks inhibitions for warnings printed about failure to // update container metadata (rate-limiting). - metadataWarnInhib *lru.LRU[libpf.PID, libpf.Void] + metadataWarnInhib *lru.LRU[util.PID, libpf.Void] times Times } // newTraceHandler creates a new traceHandler -func newTraceHandler(ctx context.Context, rep reporter.TraceReporter, - traceProcessor TraceProcessor, times Times) ( +func newTraceHandler(containerMetadataHandler containermetadata.Handler, + rep reporter.TraceReporter, traceProcessor TraceProcessor, times Times) ( *traceHandler, error) { cacheSize := config.TraceCacheEntries() @@ -106,18 +110,12 @@ func newTraceHandler(ctx context.Context, rep reporter.TraceReporter, return nil, err } - pidHash := func(x libpf.PID) uint32 { return uint32(x) } - metadataWarnInhib, err := lru.New[libpf.PID, libpf.Void](64, pidHash) + metadataWarnInhib, err := lru.New[util.PID, libpf.Void](64, util.PID.Hash32) if err != nil { return nil, fmt.Errorf("failed to create metadata warning inhibitor LRU: %v", err) } metadataWarnInhib.SetLifetime(metadataWarnInhibDuration) - containerMetadataHandler, err := containermetadata.GetHandler(ctx, times.MonitorInterval()) - if err != nil { - return nil, fmt.Errorf("failed to create container metadata handler: %v", err) - } - t := &traceHandler{ traceProcessor: traceProcessor, bpfTraceCache: bpfTraceCache, @@ -132,30 +130,40 @@ func newTraceHandler(ctx context.Context, rep reporter.TraceReporter, } func (m *traceHandler) HandleTrace(bpfTrace *host.Trace) { - timestamp := libpf.UnixTime32(libpf.NowAsUInt32()) defer m.traceProcessor.SymbolizationComplete(bpfTrace.KTime) + timestamp := libpf.UnixTime64(m.times.BootTimeUnixNano() + int64(bpfTrace.KTime)) meta, err := m.containerMetadataHandler.GetContainerMetadata(bpfTrace.PID) if err != nil { log.Warnf("Failed to determine container info for trace: %v", err) } - // Fast path: if the trace is already known remotely, we just send a counter update. - postConvHash, traceKnown := m.bpfTraceCache.Get(bpfTrace.Hash) - if traceKnown { - m.bpfTraceCacheHit++ - m.reporter.ReportCountForTrace(postConvHash, timestamp, 1, - bpfTrace.Comm, meta.PodName, meta.ContainerName) - return + if !m.reporter.SupportsReportTraceEvent() { + // Fast path: if the trace is already known remotely, we just send a counter update. + postConvHash, traceKnown := m.bpfTraceCache.Get(bpfTrace.Hash) + if traceKnown { + m.bpfTraceCacheHit++ + svcName := m.traceProcessor.MaybeNotifyAPMAgent(bpfTrace, postConvHash, 1) + m.reporter.ReportCountForTrace(postConvHash, timestamp, 1, + bpfTrace.Comm, meta.PodName, meta.ContainerName, svcName) + return + } + m.bpfTraceCacheMiss++ } - m.bpfTraceCacheMiss++ // Slow path: convert trace. umTrace := m.traceProcessor.ConvertTrace(bpfTrace) log.Debugf("Trace hash remap 0x%x -> 0x%x", bpfTrace.Hash, umTrace.Hash) m.bpfTraceCache.Add(bpfTrace.Hash, umTrace.Hash) + + svcName := m.traceProcessor.MaybeNotifyAPMAgent(bpfTrace, umTrace.Hash, 1) + if m.reporter.SupportsReportTraceEvent() { + m.reporter.ReportTraceEvent(umTrace, timestamp, + bpfTrace.Comm, meta.PodName, meta.ContainerName, svcName) + return + } m.reporter.ReportCountForTrace(umTrace.Hash, timestamp, 1, - bpfTrace.Comm, meta.PodName, meta.ContainerName) + bpfTrace.Comm, meta.PodName, meta.ContainerName, svcName) // Trace already known to collector by UM hash? if _, known := m.umTraceCache.Get(umTrace.Hash); known { @@ -171,15 +179,22 @@ func (m *traceHandler) HandleTrace(bpfTrace *host.Trace) { // Start starts a goroutine that receives and processes trace updates over // the given channel. Updates are sent periodically to the collection agent. -func Start(ctx context.Context, rep reporter.TraceReporter, traceProcessor TraceProcessor, +// The returned channel allows the caller to wait for the background worker +// to exit after a cancellation through the context. +func Start(ctx context.Context, containerMetadataHandler containermetadata.Handler, + rep reporter.TraceReporter, traceProcessor TraceProcessor, traceInChan <-chan *host.Trace, times Times, -) error { - handler, err := newTraceHandler(ctx, rep, traceProcessor, times) +) (workerExited <-chan libpf.Void, err error) { + handler, err := newTraceHandler(containerMetadataHandler, rep, traceProcessor, times) if err != nil { - return fmt.Errorf("failed to create traceHandler: %v", err) + return nil, fmt.Errorf("failed to create traceHandler: %v", err) } + exitChan := make(chan libpf.Void) + go func() { + defer close(exitChan) + metricsTicker := time.NewTicker(times.MonitorInterval()) defer metricsTicker.Stop() @@ -198,5 +213,5 @@ func Start(ctx context.Context, rep reporter.TraceReporter, traceProcessor Trace } }() - return nil + return exitChan, nil } diff --git a/tracehandler/tracehandler_test.go b/tracehandler/tracehandler_test.go index 4e03cb8e..a7635173 100644 --- a/tracehandler/tracehandler_test.go +++ b/tracehandler/tracehandler_test.go @@ -4,17 +4,21 @@ * See the file "LICENSE" for details. */ -package tracehandler +package tracehandler_test import ( + "context" "testing" "time" - "github.com/elastic/go-freelru" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/elastic/otel-profiling-agent/containermetadata" "github.com/elastic/otel-profiling-agent/host" "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/tracehandler" + "github.com/elastic/otel-profiling-agent/util" ) type fakeTimes struct { @@ -26,12 +30,13 @@ func defaultTimes() *fakeTimes { } func (ft *fakeTimes) MonitorInterval() time.Duration { return ft.monitorInterval } +func (ft *fakeTimes) BootTimeUnixNano() int64 { return 0 } // fakeTraceProcessor implements a fake TraceProcessor used only within the test scope. type fakeTraceProcessor struct{} // Compile time check to make sure fakeTraceProcessor satisfies the interfaces. -var _ TraceProcessor = (*fakeTraceProcessor)(nil) +var _ tracehandler.TraceProcessor = (*fakeTraceProcessor)(nil) func (f *fakeTraceProcessor) ConvertTrace(trace *host.Trace) *libpf.Trace { var newTrace libpf.Trace @@ -39,15 +44,16 @@ func (f *fakeTraceProcessor) ConvertTrace(trace *host.Trace) *libpf.Trace { return &newTrace } -func (f *fakeTraceProcessor) SymbolizationComplete(libpf.KTime) { +func (f *fakeTraceProcessor) SymbolizationComplete(util.KTime) {} + +func (f *fakeTraceProcessor) MaybeNotifyAPMAgent(*host.Trace, libpf.TraceHash, uint16) string { + return "" } // arguments holds the inputs to test the appropriate functions. type arguments struct { // trace holds the arguments for the function HandleTrace(). trace *host.Trace - // delay specifies a time delay after input has been processed - delay time.Duration } // reportedCount / reportedTrace hold the information reported from traceHandler @@ -73,7 +79,7 @@ func (m *mockReporter) ReportFramesForTrace(trace *libpf.Trace) { } func (m *mockReporter) ReportCountForTrace(traceHash libpf.TraceHash, - _ libpf.UnixTime32, count uint16, _, _, _ string) { + _ libpf.UnixTime64, count uint16, _, _, _, _ string) { m.reportedCounts = append(m.reportedCounts, reportedCount{ traceHash: traceHash, count: count, @@ -81,6 +87,19 @@ func (m *mockReporter) ReportCountForTrace(traceHash libpf.TraceHash, m.t.Logf("reportCountForTrace: 0x%x count: %d", traceHash, count) } +func (m *mockReporter) SupportsReportTraceEvent() bool { return false } + +func (m *mockReporter) ReportTraceEvent(_ *libpf.Trace, + _ libpf.UnixTime64, _, _, _, _ string) { +} + +type mockContainerMetadataHandler struct{} + +func (m mockContainerMetadataHandler) GetContainerMetadata(util.PID) ( + containermetadata.ContainerMetadata, error) { + return containermetadata.ContainerMetadata{}, nil +} + func TestTraceHandler(t *testing.T) { tests := map[string]struct { input []arguments @@ -121,45 +140,26 @@ func TestTraceHandler(t *testing.T) { t.Run(name, func(t *testing.T) { r := &mockReporter{t: t} - bpfTraceCache, err := freelru.New[host.TraceHash, libpf.TraceHash]( - 1024, func(k host.TraceHash) uint32 { return uint32(k) }) - require.Nil(t, err) - require.NotNil(t, t, bpfTraceCache) - - umTraceCache, err := freelru.New[libpf.TraceHash, libpf.Void]( - 1024, libpf.TraceHash.Hash32) - require.Nil(t, err) - require.NotNil(t, t, umTraceCache) - - tuh := &traceHandler{ - traceProcessor: &fakeTraceProcessor{}, - bpfTraceCache: bpfTraceCache, - umTraceCache: umTraceCache, - reporter: r, - times: defaultTimes(), - } + traceChan := make(chan *host.Trace) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + exitNotify, err := tracehandler.Start(ctx, &mockContainerMetadataHandler{}, r, + &fakeTraceProcessor{}, traceChan, defaultTimes()) + require.NoError(t, err) for _, input := range test.input { - tuh.HandleTrace(input.trace) - time.Sleep(input.delay) + traceChan <- input.trace } - if len(r.reportedCounts) != len(test.expectedCounts) { - t.Fatalf("Expected %d reported counts but got %d", - len(test.expectedCounts), len(r.reportedCounts)) - } - if len(r.reportedTraces) != len(test.expectedTraces) { - t.Fatalf("Expected %d reported traces but got %d", - len(test.expectedTraces), len(r.reportedTraces)) - } + cancel() + <-exitNotify + + assert.Equal(t, len(test.expectedCounts), len(r.reportedCounts)) + assert.Equal(t, len(test.expectedTraces), len(r.reportedTraces)) + + // Expected and reported traces order should match. + assert.Equal(t, test.expectedTraces, r.reportedTraces) - for idx, trace := range test.expectedTraces { - // Expected and reported traces order should match. - if r.reportedTraces[idx] != trace { - t.Fatalf("Expected trace 0x%x, got 0x%x", - trace.traceHash, r.reportedTraces[idx].traceHash) - } - } for _, expCount := range test.expectedCounts { // Expected and reported count order doesn't necessarily match. found := false @@ -169,10 +169,8 @@ func TestTraceHandler(t *testing.T) { break } } - if !found { - t.Fatalf("Expected count %d for trace 0x%x not found", - expCount.count, expCount.traceHash) - } + assert.True(t, found, "Expected count %d for trace 0x%x not found", + expCount.count, expCount.traceHash) } }) } diff --git a/tracer/ebpf_integration_test.go b/tracer/ebpf_integration_test.go index 5fc287d0..b4624fc2 100644 --- a/tracer/ebpf_integration_test.go +++ b/tracer/ebpf_integration_test.go @@ -8,4 +8,245 @@ package tracer -// +import ( + "context" + "os" + "runtime" + "sync" + "testing" + "time" + + cebpf "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" + + "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/config" + "github.com/elastic/otel-profiling-agent/host" + hostmeta "github.com/elastic/otel-profiling-agent/hostmetadata/host" + "github.com/elastic/otel-profiling-agent/rlimit" + "github.com/elastic/otel-profiling-agent/support" + "github.com/elastic/otel-profiling-agent/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// forceContextSwitch makes sure two Go threads are running concurrently +// and that there will be a context switch between those two. +func forceContextSwitch() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + wg := &sync.WaitGroup{} + wg.Add(1) + go func(wg *sync.WaitGroup) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + wg.Done() + }(wg) + wg.Wait() +} + +// runKernelFrameProbe executes a perf event on the sched/sched_switch tracepoint +// that sends a selection of hand-crafted, predictable traces. +func runKernelFrameProbe(t *testing.T, tracer *Tracer) { + coll, err := support.LoadCollectionSpec() + require.NoError(t, err) + + err = coll.RewriteMaps(tracer.ebpfMaps) //nolint:staticcheck + require.NoError(t, err) + + restoreRlimit, err := rlimit.MaximizeMemlock() + require.NoError(t, err) + defer restoreRlimit() + + prog, err := cebpf.NewProgram(coll.Programs["tracepoint__sched_switch"]) + require.NoError(t, err) + defer prog.Close() + + ev, err := link.Tracepoint("sched", "sched_switch", prog, nil) + require.NoError(t, err) + t.Logf("probe for Kernel frames installed on sched/sched_switch") + + // Manually trigger the tracepoint on sched/sched_switch. + forceContextSwitch() + + t.Logf("tracepoint sched_switch triggered") + err = ev.Close() + require.NoError(t, err) +} + +func validateTrace(t *testing.T, numKernelFrames int, expected, returned *host.Trace) { + t.Helper() + + assert.Equal(t, len(expected.Frames), len(returned.Frames)-numKernelFrames) + + for i, expFrame := range expected.Frames { + retFrame := returned.Frames[numKernelFrames+i] + assert.Equal(t, expFrame.File, retFrame.File) + assert.Equal(t, expFrame.Lineno, retFrame.Lineno) + assert.Equal(t, expFrame.Type, retFrame.Type) + } +} + +type mockIntervals struct{} + +func (f mockIntervals) MonitorInterval() time.Duration { return 1 * time.Second } +func (f mockIntervals) TracePollInterval() time.Duration { return 250 * time.Millisecond } +func (f mockIntervals) PIDCleanupInterval() time.Duration { return 1 * time.Second } + +type mockReporter struct{} + +func (f mockReporter) ExecutableMetadata(_ context.Context, _ libpf.FileID, _, _ string) {} +func (f mockReporter) ReportFallbackSymbol(_ libpf.FrameID, _ string) {} +func (f mockReporter) FrameMetadata(_ libpf.FileID, _ libpf.AddressOrLineno, _ util.SourceLineno, + _ uint32, _, _ string) { +} + +func generateMaxLengthTrace() host.Trace { + var trace host.Trace + for i := 0; i < support.MaxFrameUnwinds; i++ { + trace.Frames = append(trace.Frames, host.Frame{ + File: ^host.FileID(i), + Lineno: libpf.AddressOrLineno(i), + Type: support.FrameMarkerNative, + }) + } + return trace +} + +func TestTraceTransmissionAndParsing(t *testing.T) { + ctx := context.Background() + + dir, err := os.MkdirTemp("", "tracer-integration-test-cache") + require.NoError(t, err) + + var presentCores uint16 + presentCores, err = hostmeta.PresentCPUCores() + require.NoError(t, err) + + err = config.SetConfiguration(&config.Config{ + ProjectID: 42, + CacheDirectory: dir, + SecretToken: "secret", + PresentCPUCores: presentCores, + SamplesPerSecond: 20, + }) + require.NoError(t, err) + + enabledTracers, _ := config.ParseTracers("") + enabledTracers.Enable(config.PythonTracer) + tracer, err := NewTracer(ctx, &mockReporter{}, &mockIntervals{}, enabledTracers, false) + require.NoError(t, err) + + traceChan := make(chan *host.Trace, 16) + err = tracer.StartMapMonitors(ctx, traceChan) + require.NoError(t, err) + + runKernelFrameProbe(t, tracer) + + traces := make(map[uint8]*host.Trace) + timeout := time.NewTimer(1 * time.Second) + + // Wait 1 second for traces to arrive. +Loop: + for { + select { + case <-timeout.C: + break Loop + case trace := <-traceChan: + require.GreaterOrEqual(t, len(trace.Comm), 4) + require.Equal(t, "\xAA\xBB\xCC", trace.Comm[0:3]) + traces[trace.Comm[3]] = trace + } + } + + tests := map[string]struct { + // id identifies the trace to inspect (encoded in COMM[3]). + id uint8 + // hasKernelFrames indicates if the trace should contain kernel frames. + hasKernelFrames bool + // userSpaceTrace holds a single Trace with just the user-space portion of the trace + // that will be verified against the returned Trace. + userSpaceTrace host.Trace + }{ + "Single Native Frame": { + id: 1, + userSpaceTrace: host.Trace{ + Frames: []host.Frame{{ + File: 1337, + Lineno: 21, + Type: support.FrameMarkerNative, + }}, + }, + }, + "Single Native Frame with Kernel Frames": { + id: 2, + hasKernelFrames: true, + userSpaceTrace: host.Trace{ + Frames: []host.Frame{{ + File: 1337, + Lineno: 21, + Type: support.FrameMarkerNative, + }}, + }, + }, + "Three Python Frames": { + id: 3, + userSpaceTrace: host.Trace{ + Frames: []host.Frame{{ + File: 1337, + Lineno: 42, + Type: support.FrameMarkerNative, + }, { + File: 1338, + Lineno: 21, + Type: support.FrameMarkerNative, + }, { + File: 1339, + Lineno: 22, + Type: support.FrameMarkerPython, + }}, + }, + }, + "Maximum Length Trace": { + id: 4, + hasKernelFrames: true, + userSpaceTrace: generateMaxLengthTrace(), + }, + } + + for name, testcase := range tests { + testcase := testcase + t.Run(name, func(t *testing.T) { + trace, ok := traces[testcase.id] + require.Truef(t, ok, "trace ID %d not received", testcase.id) + + var numKernelFrames int + for _, frame := range trace.Frames { + if frame.Type == support.FrameMarkerKernel { + numKernelFrames++ + } + } + + userspaceFrameCount := len(trace.Frames) - numKernelFrames + assert.Equal(t, len(testcase.userSpaceTrace.Frames), userspaceFrameCount) + assert.False(t, !testcase.hasKernelFrames && numKernelFrames > 0, + "unexpected kernel frames") + + // If this check fails it _could_ be a false positive, in that there is not + // in fact anything wrong with the code being tested. We hope that the + // kernel stack we capture has at least two frames, but it is possible that + // it does not. If this happens frequently we should consider if there is a + // different approach to checking this property without the possibility of + // false positives. + assert.Falsef(t, testcase.hasKernelFrames && numKernelFrames < 2, + "expected at least 2 kernel frames, but got %d", numKernelFrames) + + t.Logf("Received %d user frames and %d kernel frames", + userspaceFrameCount, numKernelFrames) + + validateTrace(t, numKernelFrames, &testcase.userSpaceTrace, trace) + }) + } +} diff --git a/tracer/events.go b/tracer/events.go index a656afc5..4a86f560 100644 --- a/tracer/events.go +++ b/tracer/events.go @@ -18,8 +18,8 @@ import ( "github.com/cilium/ebpf/perf" log "github.com/sirupsen/logrus" - "github.com/elastic/otel-profiling-agent/libpf/process" "github.com/elastic/otel-profiling-agent/metrics" + "github.com/elastic/otel-profiling-agent/process" "github.com/elastic/otel-profiling-agent/support" ) diff --git a/tracer/maccess.go b/tracer/maccess.go index 27db17af..78753009 100644 --- a/tracer/maccess.go +++ b/tracer/maccess.go @@ -36,6 +36,7 @@ func checkForMaccessPatch(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map newCheckFunc, err := kernelSymbols.LookupSymbol( libpf.SymbolName("nmi_uaccess_okay")) 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. diff --git a/tracer/probe_linux.go b/tracer/probe_linux.go index 4a660e8e..afd96cf6 100644 --- a/tracer/probe_linux.go +++ b/tracer/probe_linux.go @@ -11,17 +11,18 @@ package tracer import ( "bytes" + "errors" "fmt" "os" "strings" + "github.com/elastic/otel-profiling-agent/rlimit" + "github.com/elastic/otel-profiling-agent/util" + "golang.org/x/sys/unix" cebpf "github.com/cilium/ebpf" "github.com/cilium/ebpf/asm" - "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/rlimit" - log "github.com/sirupsen/logrus" ) @@ -29,7 +30,7 @@ import ( func ProbeBPFSyscall() error { _, _, errNo := unix.Syscall(unix.SYS_BPF, uintptr(unix.BPF_PROG_TYPE_UNSPEC), uintptr(0), 0) if errNo == unix.ENOSYS { - return fmt.Errorf("eBPF syscall is not available on your system") + return errors.New("eBPF syscall is not available on your system") } return nil } @@ -40,7 +41,7 @@ func getTracepointID(tracepoint string) (uint64, error) { if err != nil { return 0, fmt.Errorf("failed to read tracepoint ID for %s: %v", tracepoint, err) } - tid := libpf.DecToUint64(strings.TrimSpace(string(id))) + tid := util.DecToUint64(strings.TrimSpace(string(id))) return tid, nil } @@ -71,7 +72,7 @@ func ProbeTracepoint() error { if err != nil { return err } - kernelVersion := libpf.VersionUint(major, minor, patch) + kernelVersion := util.VersionUint(major, minor, patch) restoreRlimit, err := rlimit.MaximizeMemlock() if err != nil { return fmt.Errorf("failed to increase rlimit: %v", err) diff --git a/tracer/systemconfig.go b/tracer/systemconfig.go index cf9a36d0..e5a33420 100644 --- a/tracer/systemconfig.go +++ b/tracer/systemconfig.go @@ -6,49 +6,269 @@ package tracer -// #include "../support/ebpf/types.h" -import "C" - import ( + "encoding/binary" + "errors" + "fmt" + "os" + "runtime" + "strings" "unsafe" "github.com/elastic/otel-profiling-agent/config" + "github.com/elastic/otel-profiling-agent/rlimit" cebpf "github.com/cilium/ebpf" + "github.com/cilium/ebpf/btf" + "github.com/cilium/ebpf/link" + log "github.com/sirupsen/logrus" + "github.com/elastic/otel-profiling-agent/libpf" "github.com/elastic/otel-profiling-agent/pacmask" - log "github.com/sirupsen/logrus" ) +// #include "../support/ebpf/types.h" +import "C" + +// memberByName resolves btf Member from a Struct with given name +func memberByName(t *btf.Struct, field string) (*btf.Member, error) { + for i, m := range t.Members { + if m.Name == field { + return &t.Members[i], nil + } + } + return nil, fmt.Errorf("member '%s' not found", field) +} + +// calculateFieldOffset calculates the offset for given fieldSpec which +// can refer to field within nested structs. +func calculateFieldOffset(t btf.Type, fieldSpec string) (uint, error) { + offset := uint(0) + for _, field := range strings.Split(fieldSpec, ".") { + st, ok := t.(*btf.Struct) + if !ok { + return 0, fmt.Errorf("field '%s' is not a struct", field) + } + + member, err := memberByName(st, field) + if err != nil { + return 0, err + } + offset += uint(member.Offset.Bytes()) + t = member.Type + } + return offset, nil +} + +// getTSDBaseFieldSpec returns the architecture specific name of the `task_struct` +// member that contains base address for thread specific data. +func getTSDBaseFieldSpec() string { + // nolint:goconst + switch runtime.GOARCH { + case "amd64": + return "thread.fsbase" + case "arm64": + return "thread.uw.tp_value" + default: + panic("not supported") + } +} + +// parseBTF resolves the SystemConfig data from kernel BTF +func parseBTF(syscfg *C.SystemConfig) error { + fh, err := os.Open("/sys/kernel/btf/vmlinux") + if err != nil { + return err + } + defer fh.Close() + + spec, err := btf.LoadSplitSpecFromReader(fh, nil) + if err != nil { + return err + } + + var taskStruct *btf.Struct + err = spec.TypeByName("task_struct", &taskStruct) + if err != nil { + return err + } + + stackOffset, err := calculateFieldOffset(taskStruct, "stack") + if err != nil { + return err + } + syscfg.task_stack_offset = C.u32(stackOffset) + + tpbaseOffset, err := calculateFieldOffset(taskStruct, getTSDBaseFieldSpec()) + if err != nil { + return err + } + syscfg.tpbase_offset = C.u64(tpbaseOffset) + + return nil +} + +// executeSystemAnalysisBpfCode will execute given analysis program with the address argument. +func executeSystemAnalysisBpfCode(progSpec *cebpf.ProgramSpec, maps map[string]*cebpf.Map, + address libpf.SymbolValue) (code []byte, addr uint64, err error) { + systemAnalysis := maps["system_analysis"] + + key0 := uint32(0) + data := C.SystemAnalysis{ + pid: C.uint(os.Getpid()), + address: C.u64(address), + } + + if err = systemAnalysis.Update(unsafe.Pointer(&key0), unsafe.Pointer(&data), + cebpf.UpdateAny); err != nil { + return nil, 0, fmt.Errorf("failed to write system_analysis 0x%x: %v", + address, err) + } + + restoreRlimit, err := rlimit.MaximizeMemlock() + if err != nil { + return nil, 0, fmt.Errorf("failed to adjust rlimit: %v", err) + } + defer restoreRlimit() + + // Load a BPF program to load the function code in systemAnalysis. + // It attaches to raw tracepoint of entering syscall and triggers + // when running in our PID context. + prog, err := cebpf.NewProgram(progSpec) + if err != nil { + return nil, 0, fmt.Errorf("failed to load read_kernel_function_or_task_struct: %v", err) + } + defer prog.Close() + + var progLink link.Link + switch prog.Type() { + case cebpf.RawTracepoint: + progLink, err = link.AttachRawTracepoint(link.RawTracepointOptions{ + Name: "sys_enter", + Program: prog}) + case cebpf.TracePoint: + progLink, err = link.Tracepoint("syscalls", "sys_enter_bpf", prog, nil) + default: + err = fmt.Errorf("invalid system analysis program type '%v'", prog.Type()) + } + if err != nil { + return nil, 0, fmt.Errorf("failed to configure tracepoint: %v", err) + } + err = systemAnalysis.Lookup(unsafe.Pointer(&key0), unsafe.Pointer(&data)) + progLink.Close() + if err != nil { + return nil, 0, fmt.Errorf("failed to get analysis data: %v", err) + } + + // nolint:gocritic + return C.GoBytes(unsafe.Pointer(&data.code[0]), C.int(len(data.code))), + uint64(data.address), nil +} + +// loadKernelCode will request the ebpf code to read the first X bytes from given address. +func loadKernelCode(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, + address libpf.SymbolValue) ([]byte, error) { + code, _, err := executeSystemAnalysisBpfCode(coll.Programs["read_kernel_memory"], maps, address) + return code, err +} + +// readTaskStruct will request the ebpf code to read bytes from the given offset from +// the current task_struct. +func readTaskStruct(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, + address libpf.SymbolValue) (code []byte, addr uint64, err error) { + return executeSystemAnalysisBpfCode(coll.Programs["read_task_struct"], maps, address) +} + +// determineStackPtregs determines the offset of `struct pt_regs` within the entry stack +// when the `stack` field offset within `task_struct` is already known. +func determineStackPtregs(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, + syscfg *C.SystemConfig) error { + data, ptregs, err := readTaskStruct(coll, maps, libpf.SymbolValue(syscfg.task_stack_offset)) + if err != nil { + return err + } + stackBase := binary.LittleEndian.Uint64(data) + syscfg.stack_ptregs_offset = C.u32(ptregs - stackBase) + return nil +} + +// determineStackLayout scans `task_struct` for offset of the `stack` field, and using +// its value determines the offset of `struct pt_regs` within the entry stack. +func determineStackLayout(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, + syscfg *C.SystemConfig) error { + const maxTaskStructSize = 8 * 1024 + const maxStackSize = 64 * 1024 + + pageSizeMinusOne := uint64(os.Getpagesize() - 1) + + for offs := 0; offs < maxTaskStructSize; { + data, ptregs, err := readTaskStruct(coll, maps, libpf.SymbolValue(offs)) + if err != nil { + return err + } + + for i := 0; i < len(data); i += 8 { + stackBase := binary.LittleEndian.Uint64(data[i:]) + // Stack base should be page aligned + if stackBase&pageSizeMinusOne != 0 { + continue + } + if ptregs > stackBase && ptregs < stackBase+maxStackSize { + syscfg.task_stack_offset = C.u32(offs + i) + syscfg.stack_ptregs_offset = C.u32(ptregs - stackBase) + return nil + } + } + offs += len(data) + } + return errors.New("unable to find task stack offset") +} + func loadSystemConfig(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, - kernelSymbols *libpf.SymbolMap, includeTracers []bool) error { + kernelSymbols *libpf.SymbolMap, includeTracers config.IncludedTracers, + filterErrorFrames bool) error { pacMask := pacmask.GetPACMask() - - if pacMask != uint64(0) { + if pacMask != 0 { log.Infof("Determined PAC mask to be 0x%016X", pacMask) } else { log.Debug("PAC is not enabled on the system.") } + syscfg := C.SystemConfig{ + inverse_pac_mask: ^C.u64(pacMask), + drop_error_only_traces: C.bool(filterErrorFrames), + } - // In eBPF, we need the mask to AND off the PAC bits, so we invert it. - invPacMask := ^pacMask + if err := parseBTF(&syscfg); err != nil { + log.Infof("Using binary analysis (BTF not available: %s)", err) - var tpbaseOffset uint64 - if includeTracers[config.PerlTracer] || includeTracers[config.PythonTracer] { - var err error - tpbaseOffset, err = loadTPBaseOffset(coll, maps, kernelSymbols) - if err != nil { + if err = determineStackLayout(coll, maps, &syscfg); err != nil { return err } - } - cfg := C.SystemConfig{ - inverse_pac_mask: C.u64(invPacMask), - tpbase_offset: C.u64(tpbaseOffset), - drop_error_only_traces: C.bool(true), + if includeTracers.Has(config.PerlTracer) || includeTracers.Has(config.PythonTracer) { + var tpbaseOffset uint64 + tpbaseOffset, err = loadTPBaseOffset(coll, maps, kernelSymbols) + if err != nil { + return err + } + syscfg.tpbase_offset = C.u64(tpbaseOffset) + } + } else { + // Sadly BTF does not currently include THREAD_SIZE which is needed + // to calculate the offset of struct pt_regs in the entry stack. + // The value also depends of some kernel configurations, so lets + // analyze it dynamically for now. + if err = determineStackPtregs(coll, maps, &syscfg); err != nil { + return err + } } + log.Infof("Found offsets: task stack %#x, pt_regs %#x, tpbase %#x", + syscfg.task_stack_offset, + syscfg.stack_ptregs_offset, + syscfg.tpbase_offset) + key0 := uint32(0) - return maps["system_config"].Update(unsafe.Pointer(&key0), unsafe.Pointer(&cfg), + return maps["system_config"].Update(unsafe.Pointer(&key0), unsafe.Pointer(&syscfg), cebpf.UpdateAny) } diff --git a/tracer/tpbase.go b/tracer/tpbase.go index cac94dd1..66d252c5 100644 --- a/tracer/tpbase.go +++ b/tracer/tpbase.go @@ -10,18 +10,12 @@ import ( "encoding/hex" "errors" "fmt" - "unsafe" cebpf "github.com/cilium/ebpf" - "github.com/cilium/ebpf/link" - - "github.com/elastic/otel-profiling-agent/libpf/rlimit" - "github.com/elastic/otel-profiling-agent/support" - "github.com/elastic/otel-profiling-agent/tpbase" - log "github.com/sirupsen/logrus" "github.com/elastic/otel-profiling-agent/libpf" + "github.com/elastic/otel-profiling-agent/tpbase" ) // This file contains code to extract the offset of the thread pointer base variable in @@ -40,58 +34,6 @@ import ( // 3) Disassemble the kernel ELF starting at that address: // objdump -S --start-address=0x$address kernel.elf | head -20 -// loadKernelCode will request the ebpf code read the first X bytes from given address. -func loadKernelCode(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, - functionAddress libpf.SymbolValue) ([]byte, error) { - funcAddressMap := maps["codedump_addr"] - functionCode := maps["codedump_code"] - - key0 := uint32(0) - funcAddr := uint64(functionAddress) - - if err := funcAddressMap.Update(unsafe.Pointer(&key0), unsafe.Pointer(&funcAddr), - cebpf.UpdateAny); err != nil { - return nil, fmt.Errorf("failed to write codedump_addr 0x%x: %v", - functionAddress, err) - } - - restoreRlimit, err := rlimit.MaximizeMemlock() - if err != nil { - return nil, fmt.Errorf("failed to adjust rlimit: %v", err) - } - defer restoreRlimit() - - // Load a BPF program to load the function code in functionCode. - // Trigger it via a sys_enter_bpf tracepoint so we can easily ensure the code is run at - // least once before we read the map for the result. Hacky? Maybe... - prog, err := cebpf.NewProgram(coll.Programs["tracepoint__sys_enter_bpf"]) - if err != nil { - return nil, fmt.Errorf("failed to load tracepoint__sys_enter_bpf: %v", err) - } - defer prog.Close() - - perfEvent, err := link.Tracepoint("syscalls", "sys_enter_bpf", prog, nil) - if err != nil { - return nil, fmt.Errorf("failed to configure tracepoint: %v", err) - } - defer perfEvent.Close() - - codeDump := make([]byte, support.CodedumpBytes) - - if err := functionCode.Lookup(unsafe.Pointer(&key0), &codeDump); err != nil { - return nil, fmt.Errorf("failed to get codedump: %v", err) - } - - // Make sure the map is cleared for reuse. - value0 := uint32(0) - if err := functionCode.Update(unsafe.Pointer(&key0), unsafe.Pointer(&value0), - cebpf.UpdateAny); err != nil { - return nil, fmt.Errorf("failed to delete element from codedump_code: %v", err) - } - - return codeDump, nil -} - // loadTPBaseOffset extracts the offset of the thread pointer base variable in the `task_struct` // kernel struct. This offset varies depending on kernel configuration, so we have to learn // it dynamically at runtime. @@ -113,7 +55,7 @@ func loadTPBaseOffset(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, if err != nil { return 0, fmt.Errorf("%w: %s", err, hex.Dump(code)) } - log.Infof("Found tpbase offset: %v (via %s)", tpbaseOffset, analyzer.FunctionName) + log.Debugf("Found tpbase offset: %v (via %s)", tpbaseOffset, analyzer.FunctionName) break } diff --git a/tracer/tracepoints.go b/tracer/tracepoints.go index bc9520e6..a13a905e 100644 --- a/tracer/tracepoints.go +++ b/tracer/tracepoints.go @@ -11,8 +11,7 @@ import ( "github.com/cilium/ebpf" "github.com/cilium/ebpf/link" - - "github.com/elastic/otel-profiling-agent/libpf/rlimit" + "github.com/elastic/otel-profiling-agent/rlimit" ) // attachToTracepoint attaches an eBPF program of type tracepoint to a tracepoint in the kernel diff --git a/tracer/tracer.go b/tracer/tracer.go index 9df11a9a..cb29444b 100644 --- a/tracer/tracer.go +++ b/tracer/tracer.go @@ -12,7 +12,10 @@ import ( "context" "errors" "fmt" + "hash/fnv" + "math" "math/rand" + "sort" "strings" "sync/atomic" "time" @@ -29,19 +32,19 @@ import ( "github.com/elastic/otel-profiling-agent/host" hostcpu "github.com/elastic/otel-profiling-agent/hostmetadata/host" "github.com/elastic/otel-profiling-agent/libpf" - "github.com/elastic/otel-profiling-agent/libpf/nativeunwind" - "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/localintervalcache" - "github.com/elastic/otel-profiling-agent/libpf/nativeunwind/localstackdeltaprovider" - "github.com/elastic/otel-profiling-agent/libpf/periodiccaller" "github.com/elastic/otel-profiling-agent/libpf/pfelf" - "github.com/elastic/otel-profiling-agent/libpf/rlimit" "github.com/elastic/otel-profiling-agent/libpf/xsync" "github.com/elastic/otel-profiling-agent/metrics" + "github.com/elastic/otel-profiling-agent/nativeunwind/elfunwindinfo" + "github.com/elastic/otel-profiling-agent/periodiccaller" "github.com/elastic/otel-profiling-agent/proc" pm "github.com/elastic/otel-profiling-agent/processmanager" pmebpf "github.com/elastic/otel-profiling-agent/processmanager/ebpf" "github.com/elastic/otel-profiling-agent/reporter" + "github.com/elastic/otel-profiling-agent/rlimit" "github.com/elastic/otel-profiling-agent/support" + "github.com/elastic/otel-profiling-agent/tracehandler" + "github.com/elastic/otel-profiling-agent/util" ) /* @@ -112,7 +115,7 @@ type Tracer struct { // pidEvents notifies the tracer of new PID events. // It needs to be buffered to avoid locking the writers and stacking up resources when we // read new PIDs at startup or notified via eBPF. - pidEvents chan libpf.PID + pidEvents chan util.PID // intervals provides access to globally configured timers and counters. intervals Intervals @@ -134,11 +137,12 @@ type hookPoint struct { // processKernelModulesMetadata computes the FileID of kernel files and reports executable metadata // for all kernel modules and the vmlinux image. func processKernelModulesMetadata(ctx context.Context, - rep reporter.SymbolReporter, kernelModules *libpf.SymbolMap) (map[string]libpf.FileID, error) { + rep reporter.SymbolReporter, kernelModules *libpf.SymbolMap, + kernelSymbols *libpf.SymbolMap) (map[string]libpf.FileID, error) { result := make(map[string]libpf.FileID, kernelModules.Len()) - kernelModules.ScanAllNames(func(name libpf.SymbolName) { - nameStr := string(name) - if !libpf.IsValidString(nameStr) { + 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 @@ -158,77 +162,88 @@ func processKernelModulesMetadata(ctx context.Context, // 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 = pfelf.CalculateKernelFileID(buildID) - result[nameStr] = fileID - rep.ExecutableMetadata(ctx, fileID, nameStr, buildID) + fileID = libpf.FileIDFromKernelBuildID(buildID) } else { - log.Errorf("Failed to get GNU BuildID for kernel module %s: '%s' (%v)", - nameStr, buildID, err) + fileID = calcFallbackModuleID(moduleSym, kernelSymbols) + buildID = "" } + + result[nameStr] = fileID + rep.ExecutableMetadata(ctx, fileID, nameStr, buildID) }) return result, nil } -// collectIntervalCacheMetrics starts collecting the metrics of cache every monitorInterval. -func collectIntervalCacheMetrics(ctx context.Context, cache nativeunwind.IntervalCache, - monitorInterval time.Duration) { - periodiccaller.Start(ctx, monitorInterval, func() { - size, err := cache.GetCurrentCacheSize() - if err != nil { - log.Errorf("Failed to determine size of cache: %v", err) - return +// 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) } - hit, miss := cache.GetAndResetHitMissCounters() + }) - metrics.AddSlice([]metrics.Metric{ - { - ID: metrics.IDLocalIntervalCacheSize, - Value: metrics.MetricValue(size), - }, - { - ID: metrics.IDLocalIntervalCacheHit, - Value: metrics.MetricValue(hit), - }, - { - ID: metrics.IDLocalIntervalCacheMiss, - Value: metrics.MetricValue(miss), - }, - }) + // 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])) + if err != nil { + panic("calcFallbackModuleID file ID construction is broken") + } + + 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. +// NewTracer loads eBPF code and map definitions from the ELF module at the configured path. func NewTracer(ctx context.Context, rep reporter.SymbolReporter, intervals Intervals, - includeTracers []bool, filterErrorFrames bool) (*Tracer, error) { + includeTracers config.IncludedTracers, filterErrorFrames bool) (*Tracer, error) { kernelSymbols, err := proc.GetKallsyms("/proc/kallsyms") 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(includeTracers, kernelSymbols) + ebpfMaps, ebpfProgs, err := initializeMapsAndPrograms(includeTracers, kernelSymbols, + filterErrorFrames) if err != nil { return nil, fmt.Errorf("failed to load eBPF code: %v", err) } - // Create a cache that can be used by the stack delta provider to get - // cached interval structures. - // We just started to monitor the size of the interval cache. So it is hard at - // the moment to define the maximum size of it. - // Therefore, we will start with a limit of 500 MBytes. - intervalStructureCache, err := localintervalcache.New(500 * 1024 * 1024) - if err != nil { - return nil, fmt.Errorf("failed to create local interval cache: %v", err) - } - collectIntervalCacheMetrics(ctx, intervalStructureCache, intervals.MonitorInterval()) - - // Create a stack delta provider which is used by the process manager to extract - // stack deltas from the executables. - localStackDeltaProvider := localstackdeltaprovider.New(intervalStructureCache) - - ebpfHandler, err := pmebpf.LoadMaps(ebpfMaps) + ebpfHandler, err := pmebpf.LoadMaps(ctx, ebpfMaps) if err != nil { return nil, fmt.Errorf("failed to load eBPF maps: %v", err) } @@ -236,7 +251,7 @@ func NewTracer(ctx context.Context, rep reporter.SymbolReporter, intervals Inter hasBatchOperations := ebpfHandler.SupportsGenericBatchOperations() processManager, err := pm.New(ctx, includeTracers, intervals.MonitorInterval(), ebpfHandler, - nil, rep, localStackDeltaProvider, filterErrorFrames) + nil, rep, elfunwindinfo.NewStackDeltaProvider(), filterErrorFrames) if err != nil { return nil, fmt.Errorf("failed to create processManager: %v", err) } @@ -254,7 +269,7 @@ func NewTracer(ctx context.Context, rep reporter.SymbolReporter, intervals Inter return nil, fmt.Errorf("unable to instantiate transmitted fallback symbols cache: %v", err) } - moduleFileIDs, err := processKernelModulesMetadata(ctx, rep, kernelModules) + moduleFileIDs, err := processKernelModulesMetadata(ctx, rep, kernelModules, kernelSymbols) if err != nil { return nil, fmt.Errorf("failed to extract kernel modules metadata: %v", err) } @@ -267,7 +282,7 @@ func NewTracer(ctx context.Context, rep reporter.SymbolReporter, intervals Inter kernelModules: kernelModules, transmittedFallbackSymbols: transmittedFallbackSymbols, triggerPIDProcessing: make(chan bool, 1), - pidEvents: make(chan libpf.PID, pidEventBufferSize), + pidEvents: make(chan util.PID, pidEventBufferSize), ebpfMaps: ebpfMaps, ebpfProgs: ebpfProgs, hooks: make(map[hookPoint]link.Link), @@ -326,8 +341,9 @@ 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(includeTracers []bool, kernelSymbols *libpf.SymbolMap) ( - ebpfMaps map[string]*cebpf.Map, ebpfProgs map[string]*cebpf.Program, err error) { +func initializeMapsAndPrograms(includeTracers config.IncludedTracers, + kernelSymbols *libpf.SymbolMap, filterErrorFrames bool) (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. // A collection specification holds the information about eBPF programs and maps. @@ -377,12 +393,12 @@ func initializeMapsAndPrograms(includeTracers []bool, kernelSymbols *libpf.Symbo } } - if err = loadUnwinders(coll, ebpfProgs, ebpfMaps["progs"], - includeTracers); err != nil { + if err = loadUnwinders(coll, ebpfProgs, ebpfMaps["progs"], includeTracers); err != nil { return nil, nil, fmt.Errorf("failed to load eBPF programs: %v", err) } - if err = loadSystemConfig(coll, ebpfMaps, kernelSymbols, includeTracers); err != nil { + if err = loadSystemConfig(coll, ebpfMaps, kernelSymbols, includeTracers, + filterErrorFrames); err != nil { return nil, nil, fmt.Errorf("failed to load system config: %v", err) } @@ -396,17 +412,13 @@ func initializeMapsAndPrograms(includeTracers []bool, kernelSymbols *libpf.Symbo // removeTemporaryMaps unloads and deletes eBPF maps that are only required for the // initialization. func removeTemporaryMaps(ebpfMaps map[string]*cebpf.Map) error { - // remove no longer needed eBPF maps - funcAddressMap := ebpfMaps["codedump_addr"] - functionCode := ebpfMaps["codedump_code"] - if err := funcAddressMap.Close(); err != nil { - log.Errorf("Failed to close codedump_addr: %v", err) - } - delete(ebpfMaps, "codedump_addr") - if err := functionCode.Close(); err != nil { - log.Errorf("Failed to close codedump_code: %v", err) + for _, mapName := range []string{"system_analysis"} { + if err := ebpfMaps[mapName].Close(); err != nil { + log.Errorf("Failed to close %s: %v", mapName, err) + return err + } + delete(ebpfMaps, mapName) } - delete(ebpfMaps, "codedump_code") return nil } @@ -456,19 +468,9 @@ func loadAllMaps(coll *cebpf.CollectionSpec, ebpfMaps map[string]*cebpf.Map) err return nil } -// isProgramEnabled checks if one of the given tracers in enable is set in includeTracers. -func isProgramEnabled(includeTracers []bool, enable []config.TracerType) bool { - for _, tracer := range enable { - if includeTracers[tracer] { - return true - } - } - return false -} - // loadUnwinders just satisfies the proof of concept and loads all eBPF programs func loadUnwinders(coll *cebpf.CollectionSpec, ebpfProgs map[string]*cebpf.Program, - tailcallMap *cebpf.Map, includeTracers []bool) error { + tailcallMap *cebpf.Map, includeTracers config.IncludedTracers) error { restoreRlimit, err := rlimit.MaximizeMemlock() if err != nil { return fmt.Errorf("failed to adjust rlimit: %v", err) @@ -476,9 +478,8 @@ func loadUnwinders(coll *cebpf.CollectionSpec, ebpfProgs map[string]*cebpf.Progr defer restoreRlimit() type prog struct { - // enable is a list of TracerTypes for which this eBPF program should be loaded. - // Set to `nil` / empty to always load unconditionally. - enable []config.TracerType + // enable tells whether a prog shall be loaded. + enable bool // name of the eBPF program name string // progID defines the ID for the eBPF program that is used as key in the tailcallMap. @@ -497,51 +498,60 @@ func loadUnwinders(coll *cebpf.CollectionSpec, ebpfProgs map[string]*cebpf.Progr { progID: uint32(support.ProgUnwindStop), name: "unwind_stop", + enable: true, }, { progID: uint32(support.ProgUnwindNative), name: "unwind_native", + enable: true, }, { progID: uint32(support.ProgUnwindHotspot), name: "unwind_hotspot", - enable: []config.TracerType{config.HotspotTracer}, + enable: includeTracers.Has(config.HotspotTracer), }, { progID: uint32(support.ProgUnwindPerl), name: "unwind_perl", - enable: []config.TracerType{config.PerlTracer}, + enable: includeTracers.Has(config.PerlTracer), }, { progID: uint32(support.ProgUnwindPHP), name: "unwind_php", - enable: []config.TracerType{config.PHPTracer}, + enable: includeTracers.Has(config.PHPTracer), }, { progID: uint32(support.ProgUnwindPython), name: "unwind_python", - enable: []config.TracerType{config.PythonTracer}, + enable: includeTracers.Has(config.PythonTracer), }, { progID: uint32(support.ProgUnwindRuby), name: "unwind_ruby", - enable: []config.TracerType{config.RubyTracer}, + enable: includeTracers.Has(config.RubyTracer), }, { progID: uint32(support.ProgUnwindV8), name: "unwind_v8", - enable: []config.TracerType{config.V8Tracer}, + enable: includeTracers.Has(config.V8Tracer), + }, + { + progID: uint32(support.ProgUnwindDotnet), + name: "unwind_dotnet", + enable: includeTracers.Has(config.DotnetTracer), }, { name: "tracepoint__sched_process_exit", noTailCallTarget: true, + enable: true, }, { name: "native_tracer_entry", noTailCallTarget: true, + enable: true, }, } { - if len(unwindProg.enable) > 0 && !isProgramEnabled(includeTracers, unwindProg.enable) { + if !unwindProg.enable { continue } @@ -647,13 +657,14 @@ func (t *Tracer) insertKernelFrames(trace *host.Trace, ustackLen uint32, log.Debugf(" kstack[%d] = %v+%x (%v+%x)", i, string(mod), addr, symbol, offs) - hostFileID := host.CalculateKernelFileID(fileID) + hostFileID := host.FileIDFromLibpf(fileID) t.processManager.FileIDMapper.Set(hostFileID, fileID) trace.Frames[i] = host.Frame{ - File: hostFileID, - Lineno: libpf.AddressOrLineno(addr), - Type: libpf.KernelFrame, + File: hostFileID, + Lineno: libpf.AddressOrLineno(addr), + Type: libpf.KernelFrame, + ReturnAddress: true, } // Kernel frame PCs need to be adjusted by -1. This duplicates logic done in the trace @@ -838,16 +849,20 @@ func (t *Tracer) loadBpfTrace(raw []byte) *host.Trace { } trace := &host.Trace{ - Comm: C.GoString((*C.char)(unsafe.Pointer(&ptr.comm))), - PID: libpf.PID(ptr.pid), - KTime: libpf.KTime(ptr.ktime), + Comm: C.GoString((*C.char)(unsafe.Pointer(&ptr.comm))), + APMTraceID: *(*libpf.APMTraceID)(unsafe.Pointer(&ptr.apm_trace_id)), + APMTransactionID: *(*libpf.APMTransactionID)(unsafe.Pointer(&ptr.apm_transaction_id)), + PID: util.PID(ptr.pid), + KTime: util.KTime(ptr.ktime), } // Trace fields included in the hash: - // - PID, kernel stack ID, length & frame array. + // - PID, kernel stack ID, length & frame array // Intentionally excluded: - // - ktime, COMM + // - ktime, COMM, APM trace, APM transaction ID ptr.comm = [16]C.char{} + ptr.apm_trace_id = C.ApmTraceID{} + ptr.apm_transaction_id = C.ApmSpanID{} ptr.ktime = 0 trace.Hash = host.TraceHash(xxh3.Hash128(raw).Lo) @@ -872,9 +887,10 @@ func (t *Tracer) loadBpfTrace(raw []byte) *host.Trace { for i := 0; i < int(ptr.stack_len); i++ { rawFrame := &ptr.frames[i] trace.Frames[userFrameOffs+i] = host.Frame{ - File: host.FileID(rawFrame.file_id), - Lineno: libpf.AddressOrLineno(rawFrame.addr_or_line), - Type: libpf.FrameType(rawFrame.kind), + File: host.FileID(rawFrame.file_id), + Lineno: libpf.AddressOrLineno(rawFrame.addr_or_line), + Type: libpf.FrameType(rawFrame.kind), + ReturnAddress: rawFrame.return_address != 0, } } @@ -893,13 +909,13 @@ func (t *Tracer) StartMapMonitors(ctx context.Context, traceOutChan chan *host.T pidEvents := make([]uint32, 0) periodiccaller.StartWithManualTrigger(ctx, t.intervals.MonitorInterval(), - t.triggerPIDProcessing, func(manualTrigger bool) { + t.triggerPIDProcessing, func(_ bool) { t.enableEvent(support.EventTypeGenericPID) t.monitorPIDEventsMap(&pidEvents) for _, ev := range pidEvents { log.Debugf("=> PID: %v", ev) - t.pidEvents <- libpf.PID(ev) + t.pidEvents <- util.PID(ev) } // Keep the underlying array alive to avoid GC pressure @@ -990,6 +1006,16 @@ func (t *Tracer) StartMapMonitors(ctx context.Context, traceOutChan chan *host.T C.metricID_UnwindNativeErrChaseIrqStackLink: metrics.IDUnwindNativeErrChaseIrqStackLink, C.metricID_UnwindV8ErrNoProcInfo: metrics.IDUnwindV8ErrNoProcInfo, C.metricID_UnwindNativeErrBadUnwindInfoIndex: metrics.IDUnwindNativeErrBadUnwindInfoIndex, + C.metricID_UnwindApmIntErrReadTsdBase: metrics.IDUnwindApmIntErrReadTsdBase, + C.metricID_UnwindApmIntErrReadCorrBufPtr: metrics.IDUnwindApmIntErrReadCorrBufPtr, + C.metricID_UnwindApmIntErrReadCorrBuf: metrics.IDUnwindApmIntErrReadCorrBuf, + C.metricID_UnwindApmIntReadSuccesses: metrics.IDUnwindApmIntReadSuccesses, + C.metricID_UnwindDotnetAttempts: metrics.IDUnwindDotnetAttempts, + C.metricID_UnwindDotnetFrames: metrics.IDUnwindDotnetFrames, + C.metricID_UnwindDotnetErrNoProcInfo: metrics.IDUnwindDotnetErrNoProcInfo, + C.metricID_UnwindDotnetErrBadFP: metrics.IDUnwindDotnetErrBadFP, + C.metricID_UnwindDotnetErrCodeHeader: metrics.IDUnwindDotnetErrCodeHeader, + C.metricID_UnwindDotnetErrCodeTooLarge: metrics.IDUnwindDotnetErrCodeTooLarge, } // previousMetricValue stores the previously retrieved metric values to @@ -1021,7 +1047,7 @@ func (t *Tracer) StartMapMonitors(ctx context.Context, traceOutChan chan *host.T func (t *Tracer) AttachTracer(sampleFreq int) error { tracerProg, ok := t.ebpfProgs["native_tracer_entry"] if !ok { - return fmt.Errorf("entry program is not available") + return errors.New("entry program is not available") } perfAttribute := new(perf.Attr) @@ -1055,7 +1081,7 @@ func (t *Tracer) EnableProfiling() error { events := t.perfEntrypoints.WLock() defer t.perfEntrypoints.WUnlock(&events) if len(*events) == 0 { - return fmt.Errorf("no perf events available to enable for profiling") + return errors.New("no perf events available to enable for profiling") } for id, event := range *events { if err := event.Enable(); err != nil { @@ -1124,10 +1150,7 @@ func (t *Tracer) StartProbabilisticProfiling(ctx context.Context, }) } -func (t *Tracer) ConvertTrace(trace *host.Trace) *libpf.Trace { - return t.processManager.ConvertTrace(trace) -} - -func (t *Tracer) SymbolizationComplete(traceCaptureKTime libpf.KTime) { - t.processManager.SymbolizationComplete(traceCaptureKTime) +// TraceProcessor gets the trace processor. +func (t *Tracer) TraceProcessor() tracehandler.TraceProcessor { + return t.processManager } diff --git a/libpf/traceutil/traceutil.go b/traceutil/traceutil.go similarity index 100% rename from libpf/traceutil/traceutil.go rename to traceutil/traceutil.go diff --git a/libpf/traceutil/traceutil_test.go b/traceutil/traceutil_test.go similarity index 68% rename from libpf/traceutil/traceutil_test.go rename to traceutil/traceutil_test.go index a4d7ffff..7f7bda0f 100644 --- a/libpf/traceutil/traceutil_test.go +++ b/traceutil/traceutil_test.go @@ -11,18 +11,15 @@ import ( "github.com/elastic/otel-profiling-agent/libpf" "github.com/elastic/otel-profiling-agent/support" + + "github.com/stretchr/testify/assert" ) +// nolint:testifylint func TestLibpfEBPFFrameMarkerEquality(t *testing.T) { - // This test ensures that the frame markers used in eBPF are the same used in libpf. - arr0 := []libpf.FrameType{libpf.NativeFrame, libpf.PythonFrame, libpf.PHPFrame} - arr1 := []int{support.FrameMarkerNative, support.FrameMarkerPython, support.FrameMarkerPHP} - - for i := 0; i < len(arr0); i++ { - if int(arr0[i]) != arr1[i] { - t.Fatalf("Inequality at index %d : %d != %d", i, arr0[i], arr1[i]) - } - } + assert.Equal(t, int(libpf.NativeFrame), support.FrameMarkerNative) + assert.Equal(t, int(libpf.PythonFrame), support.FrameMarkerPython) + assert.Equal(t, int(libpf.PHPFrame), support.FrameMarkerPHP) } func TestHashTrace(t *testing.T) { @@ -53,10 +50,7 @@ func TestHashTrace(t *testing.T) { name := name testcase := testcase t.Run(name, func(t *testing.T) { - hash := HashTrace(testcase.trace) - if hash != testcase.result { - t.Fatalf("Expected 0x%x got 0x%x", testcase.result, hash) - } + assert.Equal(t, testcase.result, HashTrace(testcase.trace)) }) } } diff --git a/util/ktime.go b/util/ktime.go new file mode 100644 index 00000000..2b6e7f06 --- /dev/null +++ b/util/ktime.go @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package util + +import ( + _ "unsafe" // required to use //go:linkname for runtime.nanotime +) + +// KTime stores a time value, retrieved from a monotonic clock, in nanoseconds +type KTime int64 + +// GetKTime gets the current time in same nanosecond format as bpf_ktime_get_ns() eBPF call +// This relies runtime.nanotime to use CLOCK_MONOTONIC. If this changes, this needs to +// be adjusted accordingly. Using this internal is superior in performance, as it is able +// to use the vDSO to query the time without syscall. +// +//go:noescape +//go:linkname GetKTime runtime.nanotime +func GetKTime() KTime diff --git a/util/util.go b/util/util.go new file mode 100644 index 00000000..f4fa80f8 --- /dev/null +++ b/util/util.go @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Apache License 2.0. + * See the file "LICENSE" for details. + */ + +package util + +import ( + "hash/fnv" + "strconv" + "sync/atomic" + "unicode" + "unicode/utf8" + + "github.com/sirupsen/logrus" + + "github.com/elastic/otel-profiling-agent/libpf/hash" +) + +// PID represent Unix Process ID (pid_t) +type PID int32 + +func (p PID) Hash32() uint32 { + return uint32(p) +} + +// HashString turns a string into a 64-bit hash. +func HashString(s string) uint64 { + h := fnv.New64a() + if _, err := h.Write([]byte(s)); err != nil { + logrus.Fatalf("Failed to write '%v' to hash: %v", s, err) + } + + return h.Sum64() +} + +// HexToUint64 is a convenience function to extract a hex string to a uint64 and +// not worry about errors. Essentially a "mustConvertHexToUint64". +func HexToUint64(str string) uint64 { + v, err := strconv.ParseUint(str, 16, 64) + if err != nil { + logrus.Fatalf("Failure to hex-convert %s to uint64: %v", str, err) + } + return v +} + +// DecToUint64 is a convenience function to extract a decimal string to a uint64 +// and not worry about errors. Essentially a "mustConvertDecToUint64". +func DecToUint64(str string) uint64 { + v, err := strconv.ParseUint(str, 10, 64) + if err != nil { + logrus.Fatalf("Failure to dec-convert %s to uint64: %v", str, err) + } + return v +} + +// IsValidString checks if string is UTF-8-encoded and only contains expected characters. +func IsValidString(s string) bool { + if s == "" { + return false + } + if !utf8.ValidString(s) { + return false + } + for _, r := range s { + if !unicode.IsPrint(r) { + return false + } + } + return true +} + +// NextPowerOfTwo returns the next highest power of 2 for a given value v or v, +// if v is a power of 2. +func NextPowerOfTwo(v uint32) uint32 { + v-- + v |= v >> 1 + v |= v >> 2 + v |= v >> 4 + v |= v >> 8 + v |= v >> 16 + v++ + return v +} + +// AtomicUpdateMaxUint32 updates the value in store using atomic memory primitives. newValue will +// only be placed in store if newValue is larger than the current value in store. +// To avoid inconsistency parallel updates to store should be avoided. +func AtomicUpdateMaxUint32(store *atomic.Uint32, newValue uint32) { + for { + // Load the current value + oldValue := store.Load() + if newValue <= oldValue { + // No update needed. + break + } + if store.CompareAndSwap(oldValue, newValue) { + // The value was atomically updated. + break + } + // The value changed between load and update attempt. + // Retry with the new value. + } +} + +// VersionUint returns a single integer composed of major, minor, patch. +func VersionUint(major, minor, patch uint32) uint32 { + return (major << 16) + (minor << 8) + patch +} + +// Range describes a range with Start and End values. +type Range struct { + Start uint64 + End uint64 +} + +// SourceLineno represents a line number within a source file. It is intended to be used for the +// source line numbers associated with offsets in native code, or for source line numbers in +// interpreted code. +type SourceLineno uint64 + +// OnDiskFileIdentifier can be used as unique identifier for a file. +// It is a structure to identify a particular file on disk by +// deviceID and inode number. +type OnDiskFileIdentifier struct { + DeviceID uint64 // dev_t as reported by stat. + InodeNum uint64 // ino_t should fit into 64 bits +} + +func (odfi OnDiskFileIdentifier) Hash32() uint32 { + return uint32(hash.Uint64(odfi.InodeNum) + odfi.DeviceID) +} diff --git a/utils/coredump/coredump_test.go b/utils/coredump/coredump_test.go deleted file mode 100644 index c3a478b7..00000000 --- a/utils/coredump/coredump_test.go +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Apache License 2.0. - * See the file "LICENSE" for details. - */ - -package main - -import ( - "context" - "testing" - - assert "github.com/stretchr/testify/require" -) - -func TestCoreDumps(t *testing.T) { - cases, err := findTestCases(true) - assert.Nil(t, err) - assert.NotEqual(t, len(cases), 0) - - store := initModuleStore() - - for _, filename := range cases { - filename := filename - t.Run(filename, func(t *testing.T) { - testCase, err := readTestCase(filename) - assert.Nil(t, err) - - ctx := context.Background() - - core, err := OpenStoreCoredump(store, testCase.CoredumpRef, testCase.Modules) - if err != nil { - t.SkipNow() - } - - defer core.Close() - data, err := ExtractTraces(ctx, core, false, nil) - assert.Nil(t, err) - assert.Equal(t, testCase.Threads, data) - }) - } -}